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)
 | 
			
		||||
- [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.
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user