from abc import ABC, abstractmethod from dataclasses import dataclass, field from functools import partial from typing import Any, Callable, Optional # # Grammar of the language Mini-Python # # E ::= n | X | E + E | E * E | E - E | E / E # | True | False | E == E | E < E | not E | E and E | E or E # # S ::= X = E | if E: B else: B | while E: B # # B ::= { S;* } # # E, S, B sind Grammtikvariablen oder Nichtterminalsymbole # Grammatikregeln # links immer Nichtterminal # rechts: string aus Nichtterminal und Terminalsymbolen # dazwischen "::=" # (Konvention "BNF" = Backus Normalform) # Ausdrücke aus E # E -> E + E -> 42 + E -> 42 + (E * E) -> 42 + (x * 2) # Statements (Anweisungen) aus S # Blöcke aus B # S -> while E: B -> while (E < E): B -> while (0 < x): { S; } # -> while (0 < x): { X = E; } # -> while (0 < x): { x = x - 1; } # Diese Grammatik definiert "konkrete Syntrax", d.h. Programmtexte # wie sie in der Datei stehen. # Brauchen "Parser", der die konkrete Syntax in interne Darstellung umsetzt. # Interne Darstellung = eine Objektstruktur, die "abstrakte Syntax" # bzw "abstract syntax trees" oder AST # (für Neugierige: die Python AST ist im Modul ast definiert) # zu komplex für uns! Also definieren wir selbst entsprechende Datenklassen. # Die verwenden wir als abstrakte Syntax und schreiben darauf den Interpreter. # binary operators @dataclass class Oper: name: str func: Callable def __str__ (self): return f" {self.name} " def eval (self, *args): return self.func (*args) # # environments # store the values of variables # @dataclass class Env: d: dict[str,Any] = field(default_factory=dict) def __str__ (self): return str (self.d) def get(self, x: str) -> Any: return self.d.get(x) def lookup (self, x: str) -> Any: return self.d[x] def enter (self, x: str, v: Any): self.d[x] = v # # return value # @dataclass class RVal: val: Any @dataclass class Expr(ABC): @abstractmethod def eval(self, st: Env) -> Any: ... # E ::= n @dataclass class Const (Expr): value: Any def __str__ (self): return str(self.value) def eval (self, st: Env): return self.value # E ::= X @dataclass class Var (Expr): name: str def __str__ (self): return self.name def eval (self, st: Env): return st.get(self.name) # E ::= E + E | E - E | E * E | E / E | E == E | E < E @dataclass class Binop (Expr): op: Oper left: Expr right: Expr def __str__ (self): return f"({str(self.left)}{str(self.op)}{str(self.right)})" def eval (self, st: Env): vleft = self.left.eval(st) vright = self.right.eval (st) return self.op.eval (vleft, vright) AddOp = partial (Binop, Oper ("+", lambda x,y: x+y)) SubOp = partial (Binop, Oper ("-", lambda x,y: x-y)) MulOp = partial (Binop, Oper ("*", lambda x,y: x*y)) DivOp = partial (Binop, Oper ("//", lambda x,y: x//y)) EqOp = partial (Binop, Oper ("==", lambda x,y: x==y)) LtOp = partial (Binop, Oper ("<", lambda x,y: x Optional[RVal]: ... # B ::= { S;* } @dataclass class Block: stmts: list[Stmt] def __str__(self) -> str: return "\n".join(map(str, self.stmts)) def run(self, st: Env) -> Optional[RVal]: for stmt in self.stmts: r = stmt.run(st) if r is not None: return r # S ::= X = E @dataclass class Assign (Stmt): name: str rhs: Expr def __str__ (self): return self.name + " = " + str(self.rhs) def run (self, st: Env) -> Optional[RVal]: st.enter (self.name, self.rhs.eval (st)) # S ::= if E: B else: B @dataclass class IfStmt (Stmt): cond: Expr strue: Block sfalse: Block def __str__ (self): return "if " + str(self.cond) + ":\n" + str(self.strue) + " else:\n" + str(self.sfalse) def run (self, st: Env) -> Optional[RVal]: b = self.cond.eval (st) if b: return self.strue.run (st) else: return self.sfalse.run (st) # S ::= while E: B [else: B] @dataclass class WhileStmt (Stmt): cond: Expr body: Block belse: Block = Block([]) def __str__ (self): return "while " + str(self.cond) + ":\n" + str(self.body) def run (self, st : Env) -> Optional[RVal]: while self.cond.eval (st): r = self.body.run (st) if r is not None: return r else: return self.belse.run(st) ## example expressions store0 = Env() exp1 = Const (42) exp2 = Var ("x") exp3 = AddOp (exp1, exp2) store0.enter('x', -3) exp4 = MulOp (exp2, exp2) exp5 = DivOp (exp1, Const(0)) exp6 = Orop (Const (True), exp5) ## example programs stm1 = Assign('y', Const(42)) stm2 = Assign ('z', exp3) stm3 = IfStmt (LtOp (Var ('y'), Const (40)), Block([Assign ('x', Const(1))]), Block([ Assign ('x', Const (0))])) stm4 = WhileStmt (LtOp (Const (0), Var('y')), Block([ Assign ('x', AddOp (Var ('x'), Const (1))), Assign ('y', SubOp (Var ('y'), Const (1)))])) # D ::= def F( X* ): B @dataclass class Def(Stmt): fname: str params: list[str] body: Block scope: Env = field() def call(self, args: list[Any]) -> Any: stdict = self.scope.d.copy() stdict.update(zip(self.params, args)) st = Env(stdict) match self.body.run(st): case RVal(v): return v def run(self, st: Env) -> Optional[RVal]: st.enter(self.fname, self) self.scope = st # # S ::= ... | return E @dataclass class Return(Stmt): arg: Expr def __str__(self) -> str: return f"return {str(self.arg)}" def run(self, st: Env): val = self.arg.eval(st) return RVal(val) fdef = Def('f', ['x', 'y'], Block([Return(AddOp(Var('x'), Var('y')))]), Env()) globalenv = Env() fdef.run(globalenv) @dataclass class Call(Expr): fun: Expr args: list[Expr] def __str__(self) -> str: return f"{str(self.fun)}(" + ",".join(str(arg) for arg in self.args) + ")" def eval(self, st: Env) -> Any: fv = self.fun.eval(st) argv = [arg.eval(st) for arg in self.args] match fv: case Def(): return fv.call(argv) callexp = Call(Var('f'), [Const(17), Const(4)]) print(callexp.eval(globalenv))