From 0dcacdc04aec396870b07de4b969d69401fd3541 Mon Sep 17 00:00:00 2001 From: Elara6331 Date: Sun, 10 Nov 2024 12:12:37 -0800 Subject: [PATCH] Implement function calls --- README.md | 1 - hisscl/ast.py | 19 ++++++++++++++++--- hisscl/interp.py | 20 ++++++++++++++++++++ hisscl/lexer.py | 9 +++++++++ hisscl/parser.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3f0151a..5030028 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,5 @@ Currently, this parser supports all HCL features except: - [For Expressions](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#for-expressions) - [Templates](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#templates) -- [Function Calls](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#functions-and-function-calls) Support for these features is planned. \ No newline at end of file diff --git a/hisscl/ast.py b/hisscl/ast.py index ca1ab30..32c8d24 100644 --- a/hisscl/ast.py +++ b/hisscl/ast.py @@ -30,13 +30,21 @@ class String: pos: Position value: str +Literal = Integer | Float | Bool | String + @dataclasses.dataclass class VariableRef: pos: Position name: str -Literal = Integer | Float | Bool | String | VariableRef +@dataclasses.dataclass +class FunctionCall: + pos: Position + name: str + args: list['Value'] +Ref = VariableRef | FunctionCall + @dataclasses.dataclass class Tuple: pos: Position @@ -67,8 +75,13 @@ class UnaryExpression: op: Operator value: 'Value' -Expression = BinaryExpression | UnaryExpression -Value = Literal | Collection | Expression +@dataclasses.dataclass +class Expansion: + pos: Position + value: 'Value' + +Expression = BinaryExpression | UnaryExpression | Expansion +Value = Literal | Collection | Expression | Ref @dataclasses.dataclass class Assignment: diff --git a/hisscl/interp.py b/hisscl/interp.py index f00333d..4314242 100644 --- a/hisscl/interp.py +++ b/hisscl/interp.py @@ -38,6 +38,8 @@ class Interp: if val.name not in self.vars: raise KeyError(f'{val.pos}: no such variable: {repr(val.name)}') return self.vars[val.name] + elif isinstance(val, ast.FunctionCall): + return self._exec_func_call(val) elif isinstance(val, ast.Literal): return val.value elif isinstance(val, ast.Tuple): @@ -48,6 +50,8 @@ class Interp: return self._eval_binary_expr(val) elif isinstance(val, ast.UnaryExpression): return self._eval_unary_expr(val) + elif isinstance(val, ast.Expansion): + raise ValueError(f'{val.pos}: cannot use expansion operator outside of a function call') def _is_numerical(self, val: typing.Any) -> bool: return isinstance(val, float | int) and type(val) is not bool @@ -55,6 +59,22 @@ class Interp: def _is_comparable(self, val: typing.Any) -> bool: return self._is_numerical(val) or isinstance(val, str) + def _exec_func_call(self, call: ast.FunctionCall) -> typing.Any: + if call.name not in self.vars: + raise KeyError(f'{call.pos}: no such function: {repr(call.name)}') + elif not callable(self.vars[call.name]): + raise ValueError(f'{call.pos}: cannot call non-callable object') + args = [] + for arg in call.args: + if isinstance(arg, ast.Expansion): + val = self._convert_value(arg.value) + if not isinstance(val, typing.Iterable): + raise ValueError(f"{arg.pos}: cannot perform expansion on non-iterable value ({type(val).__name__})") + args.extend(val) + else: + args.append(self._convert_value(arg)) + return self.vars[call.name](*args) + def _eval_unary_expr(self, expr: ast.UnaryExpression) -> float | int | bool: val = self._convert_value(expr.value) match expr.op.value: diff --git a/hisscl/lexer.py b/hisscl/lexer.py index a141234..cdef839 100644 --- a/hisscl/lexer.py +++ b/hisscl/lexer.py @@ -24,6 +24,7 @@ class Token(enum.Enum): COMMA = 11 COLON = 12 OPERATOR = 13 + ELLIPSIS = 14 class ExpectedError(Exception): def __init__(self, pos: ast.Position, expected: str, got: str): @@ -42,6 +43,8 @@ class Lexer: self.pos.name = name def _peek(self, n: int) -> str: + if self.unread != '': + return self.unread pos = self.stream.tell() text = self.stream.read(n) self.stream.seek(pos) @@ -226,6 +229,12 @@ class Lexer: # Ignore comments and return next token self._scan_comment(char) return self.scan() + case '.': + if (next := self._read()) != '.': + raise ExpectedError(self.pos, '.', next) + if (next := self._read()) != '.': + raise ExpectedError(self.pos, '.', next) + return Token.ELLIPSIS, self.pos, "..." case '': return Token.EOF, self.pos, char diff --git a/hisscl/parser.py b/hisscl/parser.py index d7c11fe..90135c7 100644 --- a/hisscl/parser.py +++ b/hisscl/parser.py @@ -76,6 +76,35 @@ class Parser: self._unscan(tok, pos, lit) return ast.Object(start_pos, items) + + def _parse_func_call(self) -> ast.FunctionCall: + id_tok, id_pos, id_lit = self._scan() + tok, pos, lit = self._scan() + if tok != lexer.Token.PAREN or lit != '(': + raise ExpectedError(pos, 'opening parentheses', lit) + + tok, pos, lit = self._scan() + if tok == lexer.Token.PAREN and lit == ')': + return ast.FunctionCall(pos=id_pos, name=id_lit, args=[]) + self._unscan(tok, pos, lit) + + args: list[ast.Value] = [] + while True: + args.append(self._parse_expr()) + tok, pos, lit = self._scan() + if tok == lexer.Token.PAREN and lit == ')': + break + elif tok == lexer.Token.COMMA: + continue + elif tok == lexer.Token.ELLIPSIS: + args[-1] = ast.Expansion(pos=args[-1].pos, value=args[-1]) + tok, pos, lit = self._scan() + if tok != lexer.Token.PAREN or lit != ')': + raise ExpectedError(pos, 'closing parentheses', lit) + break + else: + raise ExpectedError(pos, 'comma or closing parentheses', lit) + return ast.FunctionCall(pos=id_pos, name=id_lit, args=args) def _parse_value(self) -> ast.Value: tok, pos, lit = self._scan() @@ -89,6 +118,9 @@ class Parser: case lexer.Token.STRING: return ast.String(pos=pos, value=pyast.literal_eval(lit)) case lexer.Token.IDENT: + if self.lexer._peek(1) == '(': + self._unscan(tok, pos, lit) + return self._parse_func_call() return ast.VariableRef(pos=pos, name=lit) case lexer.Token.HEREDOC: return ast.String(pos=pos, value=lit)