|
""" |
|
Please note the python 3.10 magic I was exploring here is in `def interpreter` check it out on line 156, everything else is |
|
basically boilerplate |
|
""" |
|
|
|
from dataclasses import dataclass |
|
from typing import Callable |
|
|
|
#################################################################### |
|
|
|
# your AST |
|
|
|
class ConfigAST(object): |
|
pass |
|
|
|
class ConfigError(ConfigAST): |
|
pass |
|
|
|
class DeadCode(ConfigError): |
|
def __init__(self, program: str) -> None: |
|
self.program = program |
|
|
|
def __str__(self) -> str: |
|
return f"*** dead code: '{self.program}' ***" |
|
|
|
class UnsupportedLanguageFeatures(ConfigError): |
|
def __init__(self, program: str) -> None: |
|
self.program = program |
|
|
|
def __str__(self) -> str: |
|
return f"*** unsupported language features: '{self.program}' ***" |
|
|
|
|
|
# hmm bit yuk |
|
supported_binops = { |
|
">": lambda x, y: x > y, |
|
"<": lambda x, y: x < y, |
|
"==": lambda x, y: x == y, |
|
"!=": lambda x, y: x != y, |
|
"and": lambda x, y: x and y, |
|
"or": lambda x, y: x or y, |
|
} |
|
|
|
supported_uniops = { |
|
"len": lambda x: len(x), |
|
"cast_num": lambda x: float(x) |
|
} |
|
|
|
@dataclass |
|
class UniOp(ConfigAST): |
|
op: Callable[[ConfigAST], ConfigAST] |
|
value: ConfigAST |
|
|
|
def __str__(self) -> str: |
|
return f"uniop({self.value})" |
|
|
|
@dataclass |
|
class BinOp(ConfigAST): |
|
left: ConfigAST |
|
op: Callable[[ConfigAST, ConfigAST], ConfigAST] |
|
right: ConfigAST |
|
|
|
def __str__(self) -> str: |
|
return f"binop({self.left}, {self.right})" |
|
|
|
@dataclass |
|
class NumValue(ConfigAST): |
|
value: float |
|
|
|
def __str__(self) -> str: |
|
return f"NumValue({self.value})" |
|
|
|
@dataclass |
|
class StrValue(ConfigAST): |
|
value: str |
|
|
|
def __str__(self) -> str: |
|
return f"StrValue({self.value})" |
|
|
|
@dataclass |
|
class OtherField(ConfigAST): |
|
field_name: str |
|
|
|
def __str__(self) -> str: |
|
return self.field_name |
|
|
|
# there may be a better way around this, but somewhere you need a placeholder |
|
# in your language for the thing you will later run |
|
class Placeholder(ConfigAST): |
|
def __call__(self, value) -> str: |
|
return value |
|
|
|
def __str__(self) -> str: |
|
return f"Placeholder( => T)" |
|
|
|
################################################################################# |
|
|
|
""" |
|
Build the program |
|
|
|
Here we take an input string and turn it into a program. |
|
|
|
This is where a lot of the error handling should happen. This "language" is non-strict |
|
""" |
|
def compiler(program: str) -> ConfigAST: |
|
program = str(program).strip() |
|
if "(" in program: |
|
pos_stack = [] |
|
for idx, x in enumerate(program): |
|
if x == "(": |
|
pos_stack.append(idx) |
|
elif (x == ")") and (len(pos_stack) > 1): |
|
pos_stack.pop() |
|
elif (x == ")") and (len(pos_stack) == 1): |
|
# support three types of program: |
|
# ( ... ) |
|
# ( ... ) binop ( ... ) <- language choice, left to right |
|
# uniop ( ... ) |
|
if (pos_stack[0] == 0) and (idx == len(program) - 1): |
|
return compiler(program[pos_stack[0] + 1:idx]) |
|
elif pos_stack[0] == 0: |
|
ps = program[idx + 2:].split(" ") |
|
return BinOp(compiler(program[pos_stack[0] + 1:idx]), supported_binops[ps[0]], compiler(' '.join(ps[1:]))) |
|
elif pos_stack[0] > 0: |
|
ps = program.split(" ") |
|
return UniOp(supported_uniops[ps[0]], compiler(program[pos_stack[0] + 1:idx])) |
|
else: |
|
return UnsupportedLanguageFeatures(program) |
|
elif (x == ")") and (len(pos_stack) < 1): |
|
return UnsupportedLanguageFeatures(program) |
|
|
|
|
|
|
|
match program.split(" "): |
|
case []: |
|
return DeadCode(program) |
|
case ["."]: |
|
return Placeholder() |
|
case [v] if '"' in v: |
|
return StrValue(v) |
|
case [v]: |
|
return NumValue(float(v)) |
|
case ["get_field", field_name]: |
|
return OtherField(field_name) |
|
case [op, v] if op in supported_uniops: |
|
return UniOp(supported_uniops[op], compiler(v)) |
|
case [l, op, r] if op in supported_binops: |
|
return BinOp(compiler(l), supported_binops[op], compiler(r)) |
|
case _: |
|
return UnsupportedLanguageFeatures(program) |
|
|
|
################################################################################## |
|
|
|
""" |
|
Actually run the program |
|
""" |
|
def interpreter(data: dict, field_name: str, program: ConfigAST) -> bool: |
|
match program: |
|
case Placeholder(): |
|
return data[field_name] |
|
case OtherField(other_field_name): |
|
return data[other_field_name] |
|
case NumValue(value) | StrValue(value): |
|
return value |
|
case UniOp(op, value): |
|
return op(interpreter(data, field_name, value)) |
|
case BinOp(left, op, right): |
|
return op(interpreter(data, field_name, left), interpreter(data, field_name, right)) |
|
case _: |
|
raise Exception("NO SOUP FOR YOU") |
|
|
|
|
|
if __name__ == "__main__": |
|
config = { |
|
"field1": "(cast_num .) < cast_num (get_field field2)", |
|
"field2": '((cast_num .) == 2) or (. == 3)' |
|
} |
|
|
|
data = { |
|
"field1": "12345", |
|
"field2": "3" |
|
} |
|
|
|
for field, tests in config.items(): |
|
print(f"field: {field}, test: {tests}") |
|
program = compiler(tests) |
|
print(f"AST: {program}") |
|
print(interpreter(data, field, program)) |