Implement function calls
This commit is contained in:
parent
e4d273e10f
commit
0dcacdc04a
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user