Skip to content

Instantly share code, notes, and snippets.

@hamishdickson
Last active January 22, 2023 17:54
Show Gist options
  • Save hamishdickson/679321f842207198e91b93c5b0ec807e to your computer and use it in GitHub Desktop.
Save hamishdickson/679321f842207198e91b93c5b0ec807e to your computer and use it in GitHub Desktop.
Mini programming language in python 3.10
"""
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))

Output

field: field1, test: (cast_num .) < cast_num (get_field field2)
AST: binop(uniop(Thunk( => T)), uniop(field2))
False
field: field2, test: ((cast_num .) == 2) or (. == 3)
AST: binop(binop(uniop(Thunk( => T)), NumValue(2.0)), binop(Thunk( => T), NumValue(3.0)))
False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment