Skip to content

Instantly share code, notes, and snippets.

@uho
Forked from sma/README.md
Created December 2, 2012 20:52
Show Gist options
  • Save uho/4191003 to your computer and use it in GitHub Desktop.
Save uho/4191003 to your computer and use it in GitHub Desktop.
A tiny Forth interpreter written in JavaScript plus some descriptions how to use Forth modules with Node.js

Forth für Node.js

Dies ist ein Experiment, das zeigt, wie man ein Modul in einer fremden Sprache in Node laden kann.

Dies ist ein Stück Forth:

: sq ( x -- x² ) dup * ; 3 4 * dup . sq .

Der Programmcode definiert ein neues Wort sq, welches (wie der in runde Klammern eingeschlossene Kommentar erläutert) die oberste auf dem Stack befindliche Zahl quadriert. Danach berechnet es das Produkt von 3 und 4, gibt es aus und quadriert das Ergebnis und gibt auch das aus.

Ich speichere den Forth-Quelltext unter dem Namen test.f ab.

Ich kann den Programmcode nicht direkt ausführen, doch

$ node -e "require('./forth'); require('./test')"
12
144

macht, was ich möchte -- wenn ich das passende Modul forth.js schreibe. Darum soll es im folgenden gehen.

Beginnen wir am Schluss:

require.extensions['.f'] = function(module, filename) {
  Forth.run(require("fs").readFileSync(filename, 'utf8'));
};

Ich kann bei Node eine Funktion angeben, die aufgerufen wird, wenn require eine Datei laden soll, die mit der Endung .f endet. Übergeben wird das Modul-Objekt, wo ich insbesondere in dessen exports-Eigenschaft die von diesem Modul exportierten Objekte eintragen könnte und als zweiter Parameter der Dateiname des Moduls (den ich aber auch unter module.filename finden würde). Ich nutze den Namen, um den Inhalt dieser Datei von meinem Forth genannten Forth-Interpreter ausführen zu lassen.

Der Interpreter ist ein Singleton, das in der globalen Variable Forth lebt:

var Forth = {
  words: [],
  stack: [],
  vocab: { ... },
  push: function (v) { this.stack.push(v); },
  pop: function () { return this.stack.pop(); },
  top: function (o) { return this.stack[this.stack.length - (o || 1)]; },
  scan: ...,
  parse: ...,
  quot: ...,
  exec: ...,
  run: ...
};

In words werde ich den in eine Liste (kurz seq für sequence) von Zeichenketten verwandelten Quelltext speichern. Alle anderen Methoden bedienen sich dann über scan aus dieser Liste.

In stack verwalte ich den Stapelspeicher des Interpreters. Die Methoden push, pop und top manipulieren ihn gemäß der üblichen Semantik. push schiebt ein Element auf den Stapel, pop entfernt es wieder und mit top, das einen optional offset-Parameter hat, kann ich auf ein Element zugreifen, ohne es zu entfernen.

In vocab speichere ich die vordefinierten Forth-Wörter als JavaScript-Funktionen, denen jeweils der Forth-Interpreter als Argument übergeben wird, damit die Funktionen Zugriff auf dessen Kontext haben.

So sehen die Funktionen aus:

vocab: {
  'dup': function (f) { f.push(f.top()); },
  '*':   function (f) { f.push(f.pop() * f.pop()); },
  '.':   function (f) { console.log(f.pop()); },
  '(':   parsing(function (f) { while (f.scan() !== ')'); }),
  ':':   parsing(function (f) {
           var name = f.scan(), quot = f.quot(f.parse(";"));
           f.vocab[name] = function (f) { f.exec(quot); };
         })
}

Das Wort dup verdoppelt das oberste Element des Stapels. Das Wort * nimmt die beiden obersten Zahlen vom Stapel und schiebt ihr Produkt wieder auf den Stapel. Das Wort . entfernt das oberste Element und gibt es auf der Konsole aus. Das Wort ( ignoriert alle weiteren Wörter bis zu einer schließenden Klammer. Die Methode scan beschreibe ich gleich. Das Wort : definiert ein neues Wort und erzeugt eine JavaScript-Funktion, die unter dem direkt auf : folgenden Wort in vocab abgelegt wird. Die Definition wird mit ; beendet.

Die Hilfsfunktion parsing setzt bei einem Funktionsobjekt die Eigenschaft parsing auf true, um dem Forth-Interpreter zu signalisieren, dass er diese Funktion schon beim Parsen der Wörter (in der Methode parse) ausführen soll. Alle anderen Wörter werden erst zur Laufzeit (in der Methode exec) ausgeführt.

function parsing(fn) { fn.parsing = true; return fn; }

Mit run initialisiere ich wie schon erwähnt words, indem ich den Quelltext entweder in einzelne in doppelten Anführungszeichen stehende oder aber durch Leerzeichen oder Leerzeilen getrennte Zeichenketten aufteile. Dann starte ich den Interpreter, indem ich die Wörter aus dieser Liste parse, quotiere und ausführe:

run: function (source) {
  this.words = source.match(/"[^"]*"|\S+/g);
  this.exec(this.quot(this.parse()));
}

Mit scan lese ich das nächste Wort aus der Liste aller Wörter. Die Methode gibt automatisch undefined zurück, wenn die Liste der Wörter leer ist. Wie man vielleicht sieht, beachte ich diesen Fall allerdings nicht in der Funktion des (-Worts. Hier könnte man noch besser werden.

scan: function () { return this.words.shift(); },

Mit parse verarbeite ich eine Anzahl von Wörtern bis (aber nicht einschließlich) zum den angegebenen Wort. Sollte es fehlen, wäre es undefined, was zufällig genau die Antwort von scan() ist, die kommt, wenn die Liste alle Wörter aufgebraucht ist.

parse: function (end) {
  var code = [], t = this.scan();
  while (t !== end) {
    var f = this.vocab[t];
    if (f === undefined) {
      if (/^\d+$/.test(t)) f = Number(t);
      else if (/^"[^"]*"$/.test(t)) f = t.substr(1, t.length - 2);
      else throw "unknown " + t;
    }
    if (f.parsing) {
      f(this);
    } else {
      code.push(f);
    }
    t = this.scan();
  }
  return code;
},

Betrachten wir die Methode genauer. In code sammle ich das Ergebnis. In einer Schleife lese ich solange mit scan ein Wort ein, bis ich auf die Ende-Marke treffe. Dies könnte zum Beispiel wie beim :-Wort ein ; sein. Ich schlage jedes Wort in vocab nach. Wenn ich es dort nicht finde, ist es kein definiertes Wort, aber vielleicht ist es eine Zahl. Fall ja, mache ich aus dem Wort eine Zahl. Andernfalls prüfe ich, ob das Wort in doppelten Anführungszeichen steht. In diesem Fall betrachte ich es als Zeichenketten und entferne die ". Ist es weder das eine noch das andere, ist es ein Fehler. Ein definiertes Wort wird durch ein Funktionsobjekt repräsentiert. Nur diese können ein Attribut parsing haben (siehe parsing-Funktion) aber es schadet auch nicht, bei Zahlen oder Strings darauf zuzugreifen zu versuchen. Gibt es diese Eigenschaft und ist sie true, führe ich die Funktion sofort aus. Andernfalls merke ich mir das Wort oder die Zahl oder die Zeichenkette in code. Diese ist das Ergebnis von parse.

Ein Forth-Programm besteht allerdings ausschließlich aus ausführbaren Wörtern, sprich Funktionsobjekten. Um die von parse in die Liste eingetragenen Zahlen oder Zeichenketten in passende Funktionen umzuwandeln, benutze ich die Methode quot:

quot: function (seq) {
  return seq.map(function (w) { return isf(w) ? w : lit(w); });
  function isf(w) { return typeof w === 'function'; }
  function lit(w) { return function (f) { f.push(w); }; }
},

Wenn ein Wort in der Liste seq eine Funktion ist, behalte ich es, andernfalls erzeuge ich eine anonyme Funktion, die, wenn sie aufgerufen wird, das Objekt auf den Stack schreibt. Auf diese Weise implementiere ich die korrekte Semantik von Forth, wo Zahlen (oder Zeichenketten) automatisch auf den Stapel wandern.

Die Methoden parse und quot habe ich aufgeteilt, weil es praktisch ist, Listen bestehend aus Konstanten einlesen zu können. Das Wort { könnte wie folgt eine Liste als neue Form von Literal neben Zahlen und Zeichenketten einlesen. Das ganze funktioniert übrigens automatisch rekursiv.

Forth.vocab['{'] = parsing(function (f) { f.push(f.parse('}')); });

Das Ausführen einer "quotation" übernimmt die Methode exec:

exec: function (seq) {
  for (var i = 0; i < seq.length; i++) seq[i](this);
};

Theoretisch könnte auch diese Methode erst schauen, ob wohl ein Objekt auf den Stack geschoben werden muss, aber das ist ein Test, den man besser nur einmal beim Einlesen des Programms macht und nicht jedes Mal beim Ausführen.

Wenn ich möchte, kann ich meinen kleinen Forth-Interpreter noch aus dem forth.js-Modul heraus exportieren, doch das bräuchte ich eigentlich nicht, wenn ich nur ein Forth-Programm über Node direkt ausführen können möchte.

exports.Forth = Forth;

Damit ich nicht zwei require-Anweisungen angeben muss, kann ich auch einen kleinen Wrapper schreiben und als run.js speichern:

require("./forth"); // oder ohne "./" wenn das Modul über npm installiert wurde
for (var i = 2; i < process.argv.length; i++) require("./" + process.argv[i]);

Dann kann ich mein Forth-Programm wie folgt ausführen:

node run test

Unter Unix bzw. OS X kann ich run.js natürlich auch ausführbar machen:

#!/usr/bin/env node
require("./forth"); // oder ohne "./" wenn das Modul über npm installiert wurde
for (var i = 2; i < process.argv.length; i++) require("./" + process.argv[i]);

nenne ich die Datei node-forth und führe ein chmod +x node-forth aus, kann ich mit ./node-forth test jetzt das Beispiel ausführen. Das ganze ließe sich jetzt als Package über npm verteilen und installieren, wenn man wollte. Dazu braucht es eine nur eine passende package.json-Datei.

Stefan

Anhang

Kann ich mehr von meinen vordefinierten Wörtern in Forth schreiben? Wie wäre dies:

: ( ")" scan-until ; parsing

Damit parsing einer Funktion nachträglich die Eigenschaft parsing auf true setzen kann, muss ich dafür die : umschreiben. Die soll das definierte Wort in eine globale Variable word des Interpreters schreiben:

':':   parsing(function (f) {
         var name = f.scan(), quot = f.quot(f.parse(";"));
         f.word = f.vocab[name] = function (f) { f.exec(quot); };
       })

Dann kann ich parsing wie folgt definieren:

'parsing': function (f) { f.word.parsing = true; },

Als nächstes muss ich das Wort scan-until definieren:

'scan-until': function (f) { var end = f.pop(); while (f.scan() !== end); },

Habe ich dadurch viel gewonnen? Vielleicht, wenn ich auch versuche, : mit sich selbst zu definieren:

: : scan ";" parse-until quot define ; parsing

Hier sind die Definitionen der benutzen Wörter:

'scan':        function (f) { f.push(f.scan()); },
'parse-until': function (f) { f.push(f.parse(f.pop())); },
'quot':        function (f) { f.push(f.quot(f.pop())); },
'define':      function (f) { 
                 var quot = f.pop(); 
                 f.word = f.vocab[f.pop()] = function (f) { f.exec(quot); }; 
               }

Das funktioniert natürlich nicht, ohne nicht wenigstens einmal eine Definition für : zu haben, aber diese kann nun so aussehen:

colonWords = [
  Forth.vocab['scan'],
  function (f) { f.push(";"); },
  Forth.vocab['parse-until'],
  Forth.vocab['quot'],
  Forth.vocab['define']
];
colonFunc = function (f) { f.exec(colonWords); };
colonFunc.parsing = true;
Forth.vocab[':'] = colonFunc;

Schaut man sich die Definition der Wörter an, sieht man, sie eigentlich identisch mit den Methoden sind, nur das Argumente erst vom Stapel geholt und danach wieder die Ergebnisse auf den Stapel geschrieben werden. Das könnte einfacher sein, wenn ich bereits die Methoden den Stapel benutzen lasse:

var Forth = {
    ...,
    scan: function () { this.push(this.words.shift()); },
    parse: function () {
      var end = this.pop();
      this.push([]);
      this.scan();
      while (this.top() !== end) {
        var t = this.pop(), f = this.vocab[t];
        if (f === undefined) {
          if (/^\d+$/.test(t)) f = Number(t);
          else if (/^"[^"]*"$/.test(t)) f = t.substr(1, t.length - 2);
          else throw "unknown " + t;
        }
        if (f.parsing) f.call(this);
        else this.top().push(f);
        this.scan();
      }
      this.pop();
    },
    quot: function () {
      this.push(this.pop().map(function (w) { return isf(w) ? w : lit(w); }));
      function isf(w) { return typeof w === 'function'; }
      function lit(w) { return function () { this.push(w); }; }
    },
    exec: function () {
      var seq = this.pop(); for (var i = 0; i < seq.length; i++) seq[i].call(this);
    },
    run: function (source) {
      this.words = source.match(/"[^"]*"|\S+/g);
      this.parse(); this.quot(); this.exec();
    },
    vocab: {
      'dup': function () { this.push(this.top()); },
      '*'  : function () { this.push(this.pop() * this.pop()); },
      '.'  : function () { console.log(this.pop()); },
      ':'  : parsing(function () {
               this.scan();
               this.push(";");
               this.parse();
               this.quot();
               var quot = this.pop();
               this.word = this.vocab[this.pop()] = function () { this.exec(quot); }
             })
    }
}

Der nächste Schritt wäre nun, die Methode parse zu vereinfachen:

: parse ( end -- seq ) 
  { } swap
  [ scan 2dup = ] [
    findword parsing? [ call ] [ swap [ append ] nip ] if
  ] while drop ;

Das Wort { erzeugt wie schon beschrieben eine Liste auf dem Stapel. Mit swap vertausche ich die obersten beiden Stapelelemente. Das Wort [ erzeugt eine Quotation, eine Folge von ausführbaren Wörtern, die dann von while oder if nach Bedarf ausgeführt werden kann. Mit 2dup kann ich die obersten beiden Stapelelemente verdoppeln. Mit = kann ich die obersten beiden Elemente vergleichen. Sie werden dabei entfernt und durch einen Wahrheitswert ersetzt. while führt die erste Quotation aus um zu ermitteln, wie häufig die zweite Quotation ausgeführt werden soll. In findword verstecke ich die Logik, aus einer Zeichenkette ein Wort oder Literal zu machen. Mit parsing? kann ich prüfen, ob ein Wort auf dem Stapel die Eigenschaft parsing gesetzt hat und if erwartet einen Wahrheitswert und zwei Quotations auf dem Stapel um dann zu entscheiden ob die erste oder zweite ausgeführt werden soll. Das Wort call führt ein Wort auf dem Stapel aus. Mit append füge ich einer Liste eine Element hinzu und mit nip kann ich eine Quotation ausführen nachdem ich das oberste Element vom Stapel entfernt habe. Danach wird es wieder auf den Stapel geschrieben.

So kann ich nip definieren:

: nip swap 1seq quotation combine call ;

'1seq':      function () { this.push([this.pop()]); },
'quotation': function () { this.quot(); this.top().call = this.exec.call; }
'combine':   function () {
               var seq = this.pop(); seq = this.pop().concat(seq);
               seq.call = this.exec.call; this.push(seq);
             },
'call':      function () { this.pop().call(this); }
function parsing(f){ f.parsing = true; return f; }
function makeword(quot){ return function(f){ f.exec(quot); }; }
var Forth = {
words: [],
stack: [],
push: function(v){ this.stack.push(v); },
pop: function(){ return this.stack.pop(); },
top: function(o){ return this.stack[this.stack.length - (o || 1)]; },
vocab: {
'dup': function(f){ f.push(f.top()); },
'*': function(f){ f.push(f.pop() * f.pop()); },
'.': function(f){ console.log(f.pop()); },
'(': parsing(function(f){ while (f.scan() !== ')'); }),
':': parsing(function(f){
var name = f.scan(), quot = f.quot(f.parse(";"));
f.vocab[name] = makeword(quot);
})
},
scan: function(){ return this.words.shift(); },
parse: function(end){
var code = [], t = this.scan();
while (t !== end){
var f = this.vocab[t];
if (f === undefined){
if (/^\d+$/.test(t)) f = Number(t);
else if (/^"[^"]*"$/.test(t)) f = t.substr(1, t.length - 2);
else throw "unknown " + t;
}
if (f.parsing){
f(this);
}else{
code.push(f);
}
t = this.scan();
}
return code;
},
quot: function(ary){
return ary.map(function(w){ return isf(w) ? w : lit(w); });
function isf(w){ return typeof w === 'function'; }
function lit(w){ return function(f){ f.push(w); }; }
},
exec: function(ary){
for (var i = 0; i < ary.length; i++) ary[i](this);
},
run: function(source){
this.words = source.match(/"[^"]*"|\S+/g);
this.exec(this.quot(this.parse()));
}
};
exports.Forth = Forth;
// Forth.run(": sq ( x -- 2x ) dup * ; 3 4 * . 5 sq .");
require.extensions['.f'] = function(module, filename){
Forth.run(require("fs").readFileSync(filename, 'utf8'));
};
#!/usr/bin/env node
require("./forth");
for (var i = 2; i < process.argv.length; i++) require("./" + process.argv[i]);
: sq ( x -- x² ) dup * ; 3 4 * dup . sq .
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment