Stefan
Ein Python-Interpreter
Dieser Text beschreibt theoretische Überlegungen, Python-Programme in Java zu präsentieren, um sie dann auszuführen. Auf welche Weise der Python-Quelltext übersetzt wird, ist nicht wichtig, nur das man zu jedem Python-Teilprogramm eindeutig ein Exemplar einer Java-Klasse zuordnen kann. Um den syntaktischen Ballast von Java zu reduzieren, lasse ich, wo nicht weiter wichtig, alle Sichtbarkeitsmodifikatoren und die offensichtlichen Konstruktoren zur Initialisierung der aufgeführten Exemplarvariablen weg.
Ich übersetze ein Python-Programm in zwei Arten von AST-Knoten:
Code: Alles auswählen
abstract class Stmt {
abstract void exec(Frame f);
}
abstract class Expr {
abstract Obj eval(Frame f);
}
Gegeben sei dieses Python-Programm:
Code: Alles auswählen
for i in range(10):
j = -i
if i % 2:
print j
Zur seiner Repräsentation brauche ich die folgenden AST-Knoten:
Code: Alles auswählen
class ForStmt extends Stmt {
Expr target;
Expr items;
Suite body;
}
class AssignStmt extends Stmt {
Expr target;
Expr value;
}
class IfStmt extends Stmt {
Expr condition;
Suite then;
}
class PrintStmt extends Stmt {
Expr value;
}
class Suite extends Stmt {
Stmt[] statements;
}
class Call extends Expr {
Expr primary;
Expr[] arguments;
}
class Lit extends Expr {
Obj value;
}
class Var extends Expr {
Str name;
}
class Neg extends Expr {
Expr expr;
}
class Mod extends Expr {
Expr left, right;
}
Code: Alles auswählen
new ForStmt(new Var("i"), new Call(new Var("range"), new Lit(10)), new Suite(
new AssignStmt(new Var("j"), new Neg(new Var("i"))),
new IfStmt(new Mod(new Var("i"), new Lit(2)), new Suite(
new PrintStmt(new Var("j"))))))
Ich werte ein Programm durch einen rekursiven Interpreter aus. Dazu muss ich für alle Unterklassen von `Stmt` die Methode `exec` und für alle Unterklassen von `Expr` die Methode `eval` implementieren.
Eine `for`-Schleife iteriert über ihre Elemente `items`, wobei die Schleifenvariable `target` jeweils das aktuelle Element aufnimmt. Python erlaubt verschiedene Objekte als Elemente: Eine Liste, ein Tupel, ein Dict, ein Iterator, ein Generator oder ein Exemplar einer Klasse, das entweder eine `__getitem__`-Methode oder eine `__iter__`-Methode hat. Ich nehme an, dass die Methode `iter()` des Laufzeitsystems das alles jeweils in einen `java.util.Iterator<Obj>` verwandelt.
Code: Alles auswählen
class ForStmt...
void exec(Frame f) {
for (Iterator<Obj> it = items.eval(f).iter(); it.hasNext();) {
target.set(f, it.next());
body.exec(f);
}
}
Code: Alles auswählen
abstract class Expr...
void set(Frame f, Obj value) {
throw new UnsupportedOperationException();
}
}
Code: Alles auswählen
class Var...
void set(Frame f, Obj value) {
f.set(name, value);
}
Code: Alles auswählen
class AssignStmt...
void exec(Frame f) {
target.set(f, value.eval(f));
}
Code: Alles auswählen
class Index extends Expr {
Expr primary;
Expr index;
Obj eval(Frame f) {
return primary.eval(f).getItem(index.eval(f));
}
void set(Frame f, Obj value) {
primary.eval(f).setItem(index.eval(f), value);
}
}
Code: Alles auswählen
class TupleConstr extends Expr {
Expr[] exprs;
Obj eval(Frame f) {
Obj[] values = new Obj[exprs.length];
for (int i = 0; i < exprs.length; i++) {
values[i] = exprs[i].eval(f);
}
return Tuple(values);
}
void set(Frame f, Obj value) {
for (int i = 0; i < exprs.length; i++) {
exprs[i].set(value.getItem(i));
}
}
}
Code: Alles auswählen
class Frame {
Dict locals;
void set(Str name, Obj value) {
locals.setItem(name, value);
}
}
Code: Alles auswählen
class IfStmt...
void exec(Frame f) {
if (condition.eval(f).truish()) {
then.exec(f);
}
}
class PrintStmt...
void exec(Frame f) {
System.out.println(value.eval(f).str());
}
Bei den `Expr`-Implementierungen beginne ich mit `Lit`, weil dieses trivial ist:
Code: Alles auswählen
class Lit...
Obj eval(Frame f) {
return value;
}
Code: Alles auswählen
class Var...
Obj eval(Frame f) {
return f.get(name);
}
class Frame...
Dict globals;
...
Obj get(Str name) {
Obj value = locals.getItem(name);
if (value != null) {
return value;
}
return getGlobal(name);
}
Obj getGlobal(Str name) {
value = globals.getItem(name);
if (value != null) {
return value;
value = globals.getItem(Str.__builtins__);
if (value != null) {
if (value instanceof Module) {
value = ((Module) value).dict;
}
value = value.getItem(name);
if (value != null) {
return value;
}
}
throw nameError(name);
}
class Str...
static Str __builtins__ = new Str("__builtins__");
Der Vollständigkeit halber hier noch die Methode um eine globale Variable zu setzen:
Code: Alles auswählen
class Frame...
void setGlobal(Str name, Obj value) {
globals.setItem(name, value);
}
Code: Alles auswählen
class GlobalVar extends Expr {
Str name;
Obj eval(Frame f) {
return f.getGlobal(name);
}
void set(Frame f, Obj value) {
f.setGlobal(f, value);
}
}
Das war jetzt aber gleich ein Sprung in Semantik der Variablen. Zurück zu den Operationen, die wieder einfach sind. Die `eval`-Methoden delegieren an entsprechende Methoden von `Obj`, die für Datentypen für Zahlen passend überschrieben werden.
Code: Alles auswählen
class Neg...
Obj eval(Frame f) {
return expr.eval(f).neg();
}
class Mod...
Obj eval(Frame f) {
return left.eval(f).mod(right.eval(f));
}
Alles erbt von `Obj`, meiner abstrakten Oberklasse für alle Python-Objekte, die alle aufrufbaren Java-Methoden (und davon gibt es viele) definiert. Damit spare ich mir die meisten Cast-Operationen im Code:
Code: Alles auswählen
abstract class Obj {
java.util.Iterator<Obj> iter() { ... }
boolean truish() { ... }
Str str() { ... }
Obj getItem(Obj key) { ... }
void setItem(Obj key, Obj value) { ... }
Obj getAttr(Str name) { ... }
Obj neg() { ... }
Obj mod(Obj other) { ... }
Obj call(Obj... args) { ... }
...
}
Code: Alles auswählen
class Int extends Obj {
int value;
Str str() { return new Str(String.valueOf(value)); }
}
Code: Alles auswählen
class Str extends Obj {
String value;
Str str() { return this; }
String toString() { return value; }
}
Code: Alles auswählen
class Dict extends Obj {
HashMap<Obj, Obj> values;
Obj getItem(Obj key) { return values.get(key); }
void setItem(Obj key, Obj value) { values.put(key, value); }
}
Code: Alles auswählen
class List extends Obj {
ArrayList<Obj> values;
Obj getItem(Obj key) { return values.get(((Int) key).value); }
void setItem(Obj key, Obj value) { values.set(((Int) key).value, value); }
}
Code: Alles auswählen
class Class extends Obj {
Str name;
Class base;
Dict dict;
}
Code: Alles auswählen
class Instance extends Obj {
Class cls;
Dict dict;
}
Code: Alles auswählen
class Func extends Obj {
Str[] params;
Suite body;
Dict globals;
}
Code: Alles auswählen
class Method extends Obj {
Func func;
Instance self;
}
Code: Alles auswählen
abstract class Builtin extends Obj {
}
Code: Alles auswählen
class Module extends Obj {
Str name;
Dict dict;
}
Code: Alles auswählen
Module __builtins__ = new Module(Str.__builtins__, new Dict());
__builtins__.dict.setItem(Str.range, new Builtin() { ... });
Module __main__ = new Module(Str.__main__, new Dict());
__main__.dict.setItem(__builtins__.name, __builtins__);
__main__.dict.setItem(Str.__name__, __main__.name);
program.exec(new Frame(new Dict(), __main__.dict));
Prinzipiell ist es erst mal ein Fehler:
Code: Alles auswählen
class Obj...
Obj neg() {
throw typeError("bad operand type for unary -");
}
Code: Alles auswählen
class Int...
Obj neg() {
return new Int(-value);
}
Code: Alles auswählen
class Instance...
Obj neg() {
return findMethod(new Str("__neg__")).call();
}
Code: Alles auswählen
class Instance...
private Obj findMethod(Str name) {
Obj func = cls.getAttr(name);
if (func != null) {
return func.bind(this);
}
throw attributeError(name);
}
Code: Alles auswählen
class Instance...
Obj neg() {
Obj meth;
try {
meth = findMethod(new Str("__neg__"));
} catch (AttributeError e) {
return super.neg();
}
return meth.call();
}
Code: Alles auswählen
class Class...
Obj getAttr(Str name) {
Obj value = dict.getItem(name);
if (value != null) {
return value;
}
if (base != null) {
return base.getAttr(name);
}
return null;
}
Code: Alles auswählen
class Obj...
Obj bind(Instance self) {
return this;
}
class Func...
Obj bind(Instance self) {
return new Method(this, self);
}
Prinzipiell geht es nicht:
Code: Alles auswählen
class Obj...
Obj call(Obj... args) {
throw typeError("call of non-function");
}
Code: Alles auswählen
class Func...
Obj call(Obj... args) {
Dict locals = new Dict();
for (int i = 0; i < params.length; i++) {
locals.setItem(params[i], args[i]);
}
body.exec(new Frame(locals, globals));
return None;
}
Code: Alles auswählen
class Method...
Obj call(Obj... args) {
return func.call(prepend(self, args));
}
static Obj[] prepend(Obj first, Obj[] rest) {
Obj[] objs = new Obj[rest.length + 1];
objs[0] = first;
System.arraycopy(rest, 0, objs, 1, rest.length);
return objs;
}
Ungefähr so:
Code: Alles auswählen
class Class...
Obj call(Obj... args) {
Instance i = new Instance(this, Dict());
i.findMethod(new Str("__init__")).call(args);
return i;
}
Code: Alles auswählen
class Instance...
Obj call(Obj... args) {
Obj meth;
try {
meth = findMethod(new Str("__call__"));
} catch (AttributeError e) {
return super.call(args);
}
return meth.call(args);
}
Code: Alles auswählen
class BuiltinRange extends Builtin {
Obj call(Obj... args) {
if (args.length != 1) {
throw typeError("range takes exactly 1 parameter");
}
if (!(args[0] instanceof Int)) {
throw typeError("illegal argument type");
}
int n = ((Int) args[0]).value;
List list = new List(n);
for (int i = 0; i < n; i++) {
list.append(new Int(i));
}
return list;
}
Hinweis: Leider sind Parameterlisten in Python nicht so einfach aufgebaut, wie ich bislang behauptet habe, aber das ändern nichts am Prinzip der Aufrufe, sondern macht sie nur im Detail komplizierter, indem für fehlende Argumente Ersatzwerte gesetzt werden müssen, Schlüsselwortargumente ausgewertet und zugeordnet werden müssen, Restlisten für normale Argumente in Form eines Tupel und für Schlüsselwortargumente in Form eines Dict berechnet werden müssen und Tupel oder Dicts eventuell zu einer Parameterliste ausgepackt werden müssen. Dazu kommen vor Python 3.0 noch Tupel-Parameter und ab Python 3.0 keyword-only-Parameter für primitive Funktionen.
Nachdem wir jetzt wissen, wie ein Funktionsaufruf funktioniert, können wir den letzten Baustein einfügen und den `Call`-Knoten implementieren. Zunächst wird das Objekt vor den Klammern ausgewertet. Es sollte ein "callable" sein, also etwas, dass eine `call`-Methode sinnvoll implementiert. Dann werden alle Argumente rekursiv ausgewertet und die Funktion (bzw. das "callable") aufgerufen.
Code: Alles auswählen
class Call...
Obj eval(Frame f) {
Obj func = primary.eval(f);
Obj[] args = new Obj[arguments.length];
for (int i = 0; i < arguments.length; i++) {
args[i] = arguments[i].eval(f);
}
return func.call(args);
}
Anweisungen werden zu einer `Suite` zusammengefasst und diese wird dann der Reihe nach ausgewertet. Damit kennen wir jetzt alle Methoden des rekursiven Interpreters:
Code: Alles auswählen
class Suite...
void exec(Frame f) {
for (Stmt stmt : statements) {
stmt.exec(f);
}
}
Code: Alles auswählen
class BuiltinLocals extends Builtin {
Obj call(Frame f, Obj... args) {
return f.locals;
}
}