Stack-basierte virtuelle Maschinen in C++

Martin Sulzmann

Übersicht

Betrachte Kompilierung von Programmen

Quellcode  ==> Syntaxbaum ==> .... ==> Assembler/Maschinencode

Assembler/Maschinencode: Intel? ARM? …

Ziel:

Hier: Stack-basierte virtuelle Maschine (VM) (auch Stack machine genannt).

Stack-basierte VM

Instruktionen

Folgende Instruktionen sind vorhanden.

Push i

Plus

Mult

Beispiel 1

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]

Beispiel 2

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]

Arithmetische Ausdrücke

Betrachte

1 + 2

Obiger Ausdruck kann wie folgt dargestellt werde

Push 1; Push 2; Plus;

Für arithmetische Ausdrücke verwenden wir “infix” Notation:

Im Fall von VM Instrukionen verwenden wir “postfix” Notation:

Diese Darstellung von arithmetischen Ausdrücken ist auch bekannt als Reverse Polish Notation (RPN).

Was ist der Vorteil von RPN?

Betrachte

1 * (2 + 3)

dargestellt als

Push 1; Push 2; Push 3; Plus; Mult;

Von arithmetischen Ausdrücken zu VM Instruktionen

Wir zeigen wie wir systematisch Ausdrücke als VM Instruktionen darstellen können.

Wr nehmen einen vereinfachten Befehlssatz an.

Z.B.

1 * (2 + 1)

entspricht dann

ONE; TWO; ONE; PLUS; MULT

Schritt 1: Darstellung als Syntaxbaum

1 * (2 + 1)

entspricht dann

Mult(Int(1), Plus(Int(2), Int(1)))

Schritt 2: Konvertiere Syntaxbaum in eine Sequenz von VM Instrukionen

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.

Einfache Stack Maschine in C++

Wir


// 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!



 */