Martin Sulzmann
Betrachte Kompilierung von Programmen
Quellcode ==> Syntaxbaum ==> .... ==> Assembler/Maschinencode
Assembler/Maschinencode: Intel? ARM? …
Ziel:
Plattform-unabhängig
Z.B. JVM, .Net, …
Hier: Stack-basierte virtuelle Maschine (VM) (auch Stack machine genannt).
Operationen werden mit Hilfe eines Stacks ausgeführt.
Befehlssatz ist sehr (sehr) einfach und kann sehr effizient umgesetzt werden.
Populäres Beispiel, betrachte man WebAssembly.
Folgende Instruktionen sind vorhanden.
Hole Operanden von Stack
Führe Addition aus
Schiebe Ergebnis auf Stack
Hole Operanden von Stack
Führe Multiplikation aus
Schiebe Ergebnis aud Stack
Push 1; Push 2; Plus;
Ausführung ist wie folgt.
Den Stack schreiben wir von links nach rechts (d.h. links steht das oberste Element).
Initial:
Push 1; Push 2, Plus
Stack = []
1. Schritt
Push 2, Plus
Stack = [1]
2. Schritt
Plus
Stack = [2, 1]
3. Schritt
Stack = [3]
Push 1; Push 2; Push 3; Plus; Mult;
Ausführung ist wie folgt.
Initial:
Push 1; Push 2; Push 3; Plus; Mult;
Stack = []
1. Schritt
Push 2; Push 3; Plus; Mult;
Stack = [1]
2. Schritt
Push 3; Plus; Mult;
Stack = [2, 1]
3. Schritt
Plus; Mult;
Stack = [3, 2, 1]
4. Schritt
Mult;
Stack = [5, 1]
5. Schritt
Stack = [5]
Betrachte
1 + 2
Obiger Ausdruck kann wie folgt dargestellt werde
Push 1; Push 2; Plus;
Für arithmetische Ausdrücke verwenden wir “infix” Notation:
Operanden stehen links und rechts
Dazwischen steht der Operator
Im Fall von VM Instrukionen verwenden wir “postfix” Notation:
Operanden kommen zuerst
Erst danach kommt der Operator
Diese Darstellung von arithmetischen Ausdrücken ist auch bekannt als Reverse Polish Notation (RPN).
Was ist der Vorteil von RPN?
Klammern sind nicht notwendig
Einfache Ausführung
Betrachte
1 * (2 + 3)
dargestellt als
Push 1; Push 2; Push 3; Plus; Mult;
Wir zeigen wie wir systematisch Ausdrücke als VM Instruktionen darstellen können.
Wr nehmen einen vereinfachten Befehlssatz an.
ONE: Push 1
TWO: Push 2
PLUS: Addiere
MULT: Multipliziere
Z.B.
1 * (2 + 1)
entspricht dann
ONE; TWO; ONE; PLUS; MULT
1 * (2 + 1)
entspricht dann
Mult(Int(1), Plus(Int(2), Int(1)))
Idee: Rekursiver Ansatz (von “innen” nach “aussen”).
Mult(Int(1), Plus(Int(2), Int(1)))
==convert=>
Zwischenschritte:
Int(1) ==convert=> ONE
Plus(Int(2), Int(1)) ==convert==> TWO; ONE; PLUS
Fasse Zwischenergebnisse zusammen:
ONE; TWO; ONE; PLUS; MULT
Genauer betrachtet, wir verwenden eine virtuelle Methode
convert
. In Pseudo Code, die Konvertierung ist wie
folgt.
Mult(Int(1), Plus(Int(2), Int(1))).convert()
= Int(1).convert() ++ Plus(Int(2), Int(1)).convert() ++ [MULT]
= [ONE] ++ Int(2).convert ++ Int(1).convert() ++ [PLUS] ++ [MULT]
= [ONE] ++ [TWO] ++ [ONE] ++ [PLUS] ++ [MULT]
= [ONE,TWO,ONE,PLUS,MULT]
Wir schreiben [...]
für Listen und ++
für
Konkatenation.
Wir
verwenden Klassenhierarchien zur Darstellung der Objekte des Syntaxbaums, und
virtuelle Methoden zwecks Konvertierung
// Simple stack-based VM.
#include <vector>
#include <stack>
#include <iostream>
#include <string>
#include <memory>
using namespace std;
///////////////////////////////////////
// VM code
typedef enum {
PLUS,
MULT,
ONE,
TWO
} Code_t;
string show(Code_t c) {
switch(c) {
case ONE: return "1";
case TWO: return "2";
case PLUS: return "+";
case MULT: return "*";
}
}
string show(vector<Code_t> cs) {
string s;
for(int i=0; i < cs.size(); i++) {
s = s + show(cs[i]);
if(i < cs.size() - 1) {
s = s + " ";
}
}
return s;
}
///////////////////////////////////////
// Expressions
// - eval (interpreter)
// - convert to "reverse polish notation"
// Operands come first
// Computations require a single stack
class Exp {
public:
virtual int eval() = 0;
virtual vector<Code_t>convert() = 0;
};
class IntExp : public Exp {
public:
int x;
IntExp(int x) {
if(x == 1 || x == 2) {
this->x = x;
}
else {
this->x = 1;
cout << "\nmust be 1 or 2";
}
}
int eval() { return this->x; }
vector<Code_t>convert() {
vector<Code_t> v;
auto n = x == 1 ? ONE : TWO;
v.push_back(n);
return v;
}
};
class PlusExp : public Exp {
public:
std::shared_ptr<Exp> left, right;
PlusExp(std::shared_ptr<Exp> left, std::shared_ptr<Exp> right) {
this->left = left; this->right = right;
}
int eval() { return left->eval() + right->eval(); }
vector<Code_t>convert() {
auto v1 = left->convert();
auto v2 = right->convert();
v1.insert(v1.end(),v2.begin(),v2.end()); // append v2 to v1
v1.push_back(PLUS);
return v1;
}
};
class MultExp : public Exp {
public:
std::shared_ptr<Exp> left, right;
MultExp(std::shared_ptr<Exp> left, std::shared_ptr<Exp>right) {
this->left = left; this->right = right;
}
int eval() { return left->eval() * right->eval(); }
vector<Code_t>convert() {
auto v1 = left->convert();
auto v2 = right->convert();
v1.insert(v1.end(),v2.begin(),v2.end()); // append v2 to v1
v1.push_back(MULT);
return v1;
}
};
// Helper functions
std::shared_ptr<Exp> newInt(int i) {
return std::make_shared<IntExp>(i);
}
std::shared_ptr<Exp> newPlus(std::shared_ptr<Exp> left, std::shared_ptr<Exp> right) {
return std::make_shared<PlusExp>(PlusExp(left,right));
}
std::shared_ptr<Exp> newMult(std::shared_ptr<Exp> left, std::shared_ptr<Exp> right) {
return std::make_shared<MultExp>(MultExp(left,right));
}
///////////////////////////////////////
// VM run-time
class VM {
vector<Code_t> code;
public:
VM(vector<Code_t> c) : code(c) {}
void showRunConvert() {
cout << "\n VM code: " << show(code);
cout << "\n => " << run();
cout << "\n Exp: " << convert()->eval();
}
int run() {
stack<int> s;
for(int i = 0; i < code.size(); i++) {
switch(code[i]) {
case ONE:
s.push(1);
break;
case TWO:
s.push(2);
break;
case MULT: {
auto right = s.top();
s.pop();
auto left = s.top();
s.pop();
s.push(left * right);
break;
}
case PLUS: {
auto right = s.top();
s.pop();
auto left = s.top();
s.pop();
s.push(left + right);
break;
}
} // switch
} // for
return s.top();
} // run
std::shared_ptr<Exp> convert() {
stack<std::shared_ptr<Exp>> s;
for(int i = 0; i < code.size(); i++) {
switch(code[i]) {
case ONE:
s.push(newInt(1));
break;
case TWO:
s.push(newInt(2));
break;
case MULT: {
auto right = s.top();
s.pop();
auto left = s.top();
s.pop();
s.push(newMult(left,right));
break;
}
case PLUS: {
auto right = s.top();
s.pop();
auto left = s.top();
s.pop();
s.push(newPlus(left,right));
break;
}
} // switch
} // for
return s.top();
} // convert
};
///////////////////////////////////////
// Examples
void testVM() {
{
vector<Code_t> cs{
ONE, TWO, TWO, PLUS, MULT
};
VM(cs).showRunConvert();
}
{
vector<Code_t> cs{
ONE, TWO, PLUS, TWO, MULT
};
VM(cs).showRunConvert();
}
}
void testExp() {
auto e = newPlus(newMult(newInt(1),newInt(2)), newInt(1));
auto run = [](std::shared_ptr<Exp> e) {
cout << "\n Exp yields " << e->eval();
auto vm = VM(e->convert());
vm.showRunConvert();
};
run(e);
}
int main() {
testVM();
// testExp();
}
/*
Wie koennten wir das ganze testen?
z.B.
Property 1:
Exp => convert => VMCode => convert Exp2
teste ob Exp.eval == Exp2.eval ist
Property 2:
VMCode => convert => Exp => convert => VMCode2
teste ob VMCode.run == VMCod2.run
Generierung von Testeingaben!
Z.B. generiere beliebige Exp Objekte!
*/