diff --git a/hisscl/ast.py b/hisscl/ast.py index 49b7836..1d27de6 100644 --- a/hisscl/ast.py +++ b/hisscl/ast.py @@ -86,7 +86,13 @@ class Index: value: 'Value' index: 'Value' -Expression = BinaryExpression | UnaryExpression | Expansion | Index +@dataclasses.dataclass +class GetAttr: + pos: Position + value: 'Value' + attr: str + +Expression = BinaryExpression | UnaryExpression | Expansion | Index | GetAttr Value = Literal | Collection | Expression | Ref @dataclasses.dataclass diff --git a/hisscl/interp.py b/hisscl/interp.py index 092b361..d867282 100644 --- a/hisscl/interp.py +++ b/hisscl/interp.py @@ -65,6 +65,11 @@ class Interp: return self._eval_unary_expr(val) elif isinstance(val, ast.Index): return self._eval_index(val) + elif isinstance(val, ast.GetAttr): + obj = self._convert_value(val.value) + if not hasattr(obj, val.attr): + raise AttributeError(f'{val.pos}: no such attribute {repr(val.attr)} in object of type {type(obj).__name__}') + return getattr(obj, val.attr) elif isinstance(val, ast.Expansion): raise ValueError(f'{val.pos}: cannot use expansion operator outside of a function call') diff --git a/hisscl/lexer.py b/hisscl/lexer.py index 8d32dea..a8677cc 100644 --- a/hisscl/lexer.py +++ b/hisscl/lexer.py @@ -25,6 +25,7 @@ class Token(enum.Enum): COLON = 12 OPERATOR = 13 ELLIPSIS = 14 + DOT = 15 class ExpectedError(Exception): def __init__(self, pos: ast.Position, expected: str, got: str): @@ -228,8 +229,9 @@ class Lexer: return self.scan() case '.': if (next := self._read()) != '.': - raise ExpectedError(self.pos, '.', next) - if (next := self._read()) != '.': + self._unread(next) + return Token.DOT, self.pos, next + elif (next := self._read()) != '.': raise ExpectedError(self.pos, '.', next) return Token.ELLIPSIS, self.pos, "..." case '': diff --git a/hisscl/parser.py b/hisscl/parser.py index 3915fe8..c9eef52 100644 --- a/hisscl/parser.py +++ b/hisscl/parser.py @@ -29,15 +29,27 @@ class Parser: def _unscan(self, tok: lexer.Token, pos: ast.Position, lit: str): self._prev = tok, pos, lit - def _parse_index(self, val: ast.Value) -> ast.Index: - index = ast.Index(pos=val.pos, value=val, index=self._parse_expr()) - tok, pos, lit = self._scan() + def _parse_index(self, val: ast.Value, start_pos: ast.Position) -> ast.Index: + index = ast.Index(pos=start_pos, value=val, index=self._parse_expr()) + tok, start_pos, lit = self._scan() if tok != lexer.Token.SQUARE or lit != ']': - raise ExpectedError(pos, 'closing square bracket', lit) + raise ExpectedError(start_pos, 'closing square bracket', lit) while self.lexer._peek(1) == '[': - self._scan() - index = self._parse_index(index) + _, start_pos, _ = self._scan() + index = self._parse_index(index, start_pos) return index + + def _parse_getattr(self, val: ast.Value, start_pos: ast.Position) -> ast.Index | ast.GetAttr: + tok, pos, lit = self._scan() + if tok == lexer.Token.INTEGER: + return ast.Index(pos=start_pos, value=val, index=ast.Integer(pos=pos, value=int(lit))) + elif tok == lexer.Token.IDENT: + return ast.GetAttr(pos=start_pos, value=val, attr=lit) + else: + raise ExpectedError(pos, 'integer or identifier', lit) + while self.lexer._peek(1) == '.': + _, start_pos, _ = self._scan() + index = self._parse_getattr(index, start_pos) def _parse_expr(self) -> ast.Value: left = self._parse_value() @@ -156,9 +168,13 @@ class Parser: case _: raise ExpectedError(pos, 'value', lit) - if self.lexer._peek(1) == '[': - self._scan() - out = self._parse_index(out) + tok, pos, lit = self._scan() + if tok == lexer.Token.SQUARE and lit == '[': + out = self._parse_index(out, pos) + elif tok == lexer.Token.DOT: + out = self._parse_getattr(out, pos) + else: + self._unscan(tok, pos, lit) return out diff --git a/test/test_interp.py b/test/test_interp.py index 2087f60..73d8b3b 100644 --- a/test/test_interp.py +++ b/test/test_interp.py @@ -114,6 +114,22 @@ class TestRefs(unittest.TestCase): cfg = interp.Interp(io.StringIO('x = ["123", "456", "789"][1][2]'), "TestRefs.test_multi_index").run() self.assertIn('x', cfg) self.assertEqual(cfg['x'], '6') + + def test_index_legacy(self): + i = interp.Interp(io.StringIO('x = y.1'), "TestRefs.test_index_legacy") + i['y'] = [123, 456, 789] + cfg = i.run() + self.assertIn('x', cfg) + self.assertEqual(cfg['x'], 456) + + def test_getattr(self): + class Y: + z = 123 + i = interp.Interp(io.StringIO('x = Y.z'), "TestRefs.test_getattr") + i['Y'] = Y() + cfg = i.run() + self.assertIn('x', cfg) + self.assertEqual(cfg['x'], 123) def test_func(self): def y(a, b): diff --git a/test/test_parser.py b/test/test_parser.py index 65b981c..ea80f9e 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -205,6 +205,29 @@ class TestExpressions(unittest.TestCase): pos = ast.Position(name='TestExpressions.test_index', line=1, col=3), value = 0, )) + + def test_index_legacy(self): + val = parser.Parser(io.StringIO('x.0'), 'TestExpressions.test_index_legacy')._parse_expr() + self.assertIsInstance(val, ast.Index) + assert type(val) is ast.Index + self.assertEqual(val.value, ast.VariableRef( + pos = ast.Position(name='TestExpressions.test_index_legacy', line=1, col=1), + name = 'x', + )) + self.assertEqual(val.index, ast.Integer( + pos = ast.Position(name='TestExpressions.test_index_legacy', line=1, col=3), + value = 0, + )) + + def test_getattr(self): + val = parser.Parser(io.StringIO('x.y'), 'TestExpressions.test_getattr')._parse_expr() + self.assertIsInstance(val, ast.GetAttr) + assert type(val) is ast.GetAttr + self.assertEqual(val.value, ast.VariableRef( + pos = ast.Position(name='TestExpressions.test_getattr', line=1, col=1), + name = 'x', + )) + self.assertEqual(val.attr, 'y') def test_unary(self): val = parser.Parser(io.StringIO('!true'), 'TestExpressions.test_unary')._parse_value()