3 Commits

Author SHA1 Message Date
0dcacdc04a Implement function calls 2024-11-10 12:12:37 -08:00
e4d273e10f Fix parameter name in README 2024-11-10 07:32:15 +00:00
a82533d34e Update pyproject.toml 2024-11-09 23:26:00 -08:00
6 changed files with 81 additions and 7 deletions

View File

@@ -27,7 +27,7 @@ with open('test.hcl', 'r') as fl:
cfg = hisscl.load(fl, name=fl.name)
```
Each `load*` function has an optional `vars: dict[str, Any]` parameter, whose elements are used as variables in your config file. For example, if you have `x = y + 1`, `y` must be defined in `globals`.
Each `load*` function has an optional `vars: dict[str, Any]` parameter, whose elements are used as variables in your config file. For example, if you have `x = y + 1`, `y` must be defined in `vars`.
For more advanced use-cases, `lexer`, `parser`, `ast`, and `interp` submodules are provided.
@@ -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.

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -11,7 +11,7 @@
[project]
name = "hisscl"
description = "Python HCL parser"
description = "A Python HCL parser"
dynamic = ["version"]
authors = [{ name = "Elara6331", email = "elara@elara.ws" }]
readme = "README.md"
@@ -29,4 +29,5 @@
]
[project.urls]
Repository = "https://gitea.elara.ws/Elara6331/hisscl"
Repository = "https://gitea.elara.ws/Elara6331/hisscl"
GitHub = "https://github.com/Elara6331/hisscl"