Implement function calls
This commit is contained in:
		| @@ -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) | - [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) | - [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. | Support for these features is planned. | ||||||
| @@ -30,13 +30,21 @@ class String: | |||||||
|     pos: Position |     pos: Position | ||||||
|     value: str |     value: str | ||||||
|      |      | ||||||
|  | Literal = Integer | Float | Bool | String | ||||||
|  |      | ||||||
| @dataclasses.dataclass | @dataclasses.dataclass | ||||||
| class VariableRef: | class VariableRef: | ||||||
|     pos: Position |     pos: Position | ||||||
|     name: str |     name: str | ||||||
|      |      | ||||||
| Literal = Integer | Float | Bool | String | VariableRef | @dataclasses.dataclass | ||||||
|  | class FunctionCall: | ||||||
|  |     pos: Position | ||||||
|  |     name: str | ||||||
|  |     args: list['Value'] | ||||||
|  |  | ||||||
|  | Ref = VariableRef | FunctionCall | ||||||
|  |      | ||||||
| @dataclasses.dataclass | @dataclasses.dataclass | ||||||
| class Tuple: | class Tuple: | ||||||
|     pos: Position |     pos: Position | ||||||
| @@ -67,8 +75,13 @@ class UnaryExpression: | |||||||
|     op: Operator |     op: Operator | ||||||
|     value: 'Value' |     value: 'Value' | ||||||
|      |      | ||||||
| Expression = BinaryExpression | UnaryExpression | @dataclasses.dataclass | ||||||
| Value = Literal | Collection | Expression | class Expansion: | ||||||
|  |     pos: Position | ||||||
|  |     value: 'Value' | ||||||
|  |      | ||||||
|  | Expression = BinaryExpression | UnaryExpression | Expansion | ||||||
|  | Value = Literal | Collection | Expression | Ref | ||||||
|  |  | ||||||
| @dataclasses.dataclass | @dataclasses.dataclass | ||||||
| class Assignment: | class Assignment: | ||||||
|   | |||||||
| @@ -38,6 +38,8 @@ class Interp: | |||||||
|             if val.name not in self.vars: |             if val.name not in self.vars: | ||||||
|                 raise KeyError(f'{val.pos}: no such variable: {repr(val.name)}') |                 raise KeyError(f'{val.pos}: no such variable: {repr(val.name)}') | ||||||
|             return self.vars[val.name] |             return self.vars[val.name] | ||||||
|  |         elif isinstance(val, ast.FunctionCall): | ||||||
|  |             return self._exec_func_call(val) | ||||||
|         elif isinstance(val, ast.Literal): |         elif isinstance(val, ast.Literal): | ||||||
|             return val.value |             return val.value | ||||||
|         elif isinstance(val, ast.Tuple): |         elif isinstance(val, ast.Tuple): | ||||||
| @@ -48,6 +50,8 @@ class Interp: | |||||||
|             return self._eval_binary_expr(val) |             return self._eval_binary_expr(val) | ||||||
|         elif isinstance(val, ast.UnaryExpression): |         elif isinstance(val, ast.UnaryExpression): | ||||||
|             return self._eval_unary_expr(val) |             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: |     def _is_numerical(self, val: typing.Any) -> bool: | ||||||
|         return isinstance(val, float | int) and type(val) is not 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: |     def _is_comparable(self, val: typing.Any) -> bool: | ||||||
|         return self._is_numerical(val) or isinstance(val, str) |         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: |     def _eval_unary_expr(self, expr: ast.UnaryExpression) -> float | int | bool: | ||||||
|         val = self._convert_value(expr.value) |         val = self._convert_value(expr.value) | ||||||
|         match expr.op.value: |         match expr.op.value: | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ class Token(enum.Enum): | |||||||
|     COMMA = 11 |     COMMA = 11 | ||||||
|     COLON = 12 |     COLON = 12 | ||||||
|     OPERATOR = 13 |     OPERATOR = 13 | ||||||
|  |     ELLIPSIS = 14 | ||||||
|  |  | ||||||
| class ExpectedError(Exception): | class ExpectedError(Exception): | ||||||
|     def __init__(self, pos: ast.Position, expected: str, got: str): |     def __init__(self, pos: ast.Position, expected: str, got: str): | ||||||
| @@ -42,6 +43,8 @@ class Lexer: | |||||||
|         self.pos.name = name |         self.pos.name = name | ||||||
|  |  | ||||||
|     def _peek(self, n: int) -> str: |     def _peek(self, n: int) -> str: | ||||||
|  |         if self.unread != '': | ||||||
|  |             return self.unread | ||||||
|         pos = self.stream.tell() |         pos = self.stream.tell() | ||||||
|         text = self.stream.read(n) |         text = self.stream.read(n) | ||||||
|         self.stream.seek(pos) |         self.stream.seek(pos) | ||||||
| @@ -226,6 +229,12 @@ class Lexer: | |||||||
|                 # Ignore comments and return next token |                 # Ignore comments and return next token | ||||||
|                 self._scan_comment(char) |                 self._scan_comment(char) | ||||||
|                 return self.scan() |                 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 '': |             case '': | ||||||
|                 return Token.EOF, self.pos, char |                 return Token.EOF, self.pos, char | ||||||
|  |  | ||||||
|   | |||||||
| @@ -76,6 +76,35 @@ class Parser: | |||||||
|                 self._unscan(tok, pos, lit) |                 self._unscan(tok, pos, lit) | ||||||
|              |              | ||||||
|         return ast.Object(start_pos, items) |         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: |     def _parse_value(self) -> ast.Value: | ||||||
|         tok, pos, lit = self._scan() |         tok, pos, lit = self._scan() | ||||||
| @@ -89,6 +118,9 @@ class Parser: | |||||||
|             case lexer.Token.STRING: |             case lexer.Token.STRING: | ||||||
|                 return ast.String(pos=pos, value=pyast.literal_eval(lit)) |                 return ast.String(pos=pos, value=pyast.literal_eval(lit)) | ||||||
|             case lexer.Token.IDENT: |             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) |                 return ast.VariableRef(pos=pos, name=lit) | ||||||
|             case lexer.Token.HEREDOC: |             case lexer.Token.HEREDOC: | ||||||
|                 return ast.String(pos=pos, value=lit) |                 return ast.String(pos=pos, value=lit) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user