Skip to content

Instantly share code, notes, and snippets.

@Heimdell
Last active December 3, 2017 21:08
Show Gist options
  • Save Heimdell/f1fce236c6cb4c1f187749c799db1bb8 to your computer and use it in GitHub Desktop.
Save Heimdell/f1fce236c6cb4c1f187749c799db1bb8 to your computer and use it in GitHub Desktop.
Forth-machine
module.exports.Struct = (thunk) => {
let klass = null
return class {
constructor(args) {
Object.assign(this, args)
}
static of(args) {
if (!klass) {
klass = thunk()
}
return new klass(args)
}
with(delta) {
return Object.assign(this, delta)
}
}
}
let { Struct } = require("./adt.js")
let { defined, log } = require("./util.js")
/*
* FORTH-Machine
*/
class Machine extends Struct(() => Machine) {
constructor(args) {
let {
data, // data stack, place for all args & results
program, // string with "remaining program""
words, // linked list of named wordGroups
wordGroups, // linked list of named definition groups
compilation // true if compiling, false if evaluating
} = args
super(args)
}
/*
* Machine.addWord(Word) : Nothing
*
* Adds a word to the machine.
*/
addWord(word) {
this.words = addWord(this.words, word)
}
/*
* Machine.findWord(Name) : Word
*
* Returns a pointer to the word.
*
* If word cannot be found, its an error.
*/
findWord(name) {
for
( var i = this.words
; i != empty && i.name != name
; i = i.previous
) ;
if (i == empty) {
let num = Number(name)
if (!isNaN(num)) {
return word(num, function() {
this.data.push(num)
})
}
throw `no such word - ${name}`
}
return i
}
/*
* Machine.forget(Name) : Nothing
*
* Removes word of Name and all words declared after it.
*
* Notice: we cannot use findWord, because we must delete wordGroups too.
*
* (Name is taken from FORTH)
*/
forget(name) {
var prev = ''
for
( var word = this.words
; word != empty && prev != name
; word = word.previous
)
{
if (word.name == name && word.isProtected) {
throw `cannot remove protected word - ${name}`
}
if (word.isProtected) {
throw `cannot remove word ${name} below protected word ${word.name}`
}
if (this.wordGroups.word == word) {
this.wordGroups = this.wordGroups.previous
}
prev = word.name
}
if (word == empty && prev != name) {
throw `no such word - ${name}`
}
this.words = word
}
/*
* Machine.nameWordGroup(Name) : Nothing
*
* Sets name for group of words declared after last word group was closed.
*/
nameWordGroup(name) {
this.wordGroups = WordGroup.of({
name,
word: this.words,
previous: this.wordGroups
})
}
/*
* Machine.findWordGroup(Name) : Nothing
*
* Returns a pointer to the word group with Name.
*
* If there is no such word group, its an error.
*/
findWordGroup(name) {
for
( var def = this.wordGroups
; def != empty && def.name != name
; def = def.previous
) ;
if (def == empty) {
throw `no such wordGroups - ${name}`
}
return def
}
/*
* Machine.definitions(Name) : Nothing
*
* Forgets all words declared after given word group.
*
* If there is no such word group, its an error.
*
* (Name is taken from FORTH)
*/
definitions(name) {
this.wordGroups = this.findWordGroup(name)
this.words = this.wordGroups.word
}
/*
* Construct array of {[group]name, words} from machine state.
*/
parseWordGroups() {
let groups = [{name: "local", words: []}]
let group = this.wordGroups
for
( let i = this.words
; i != empty
; i = i.previous
)
{
if (group != empty && i == group.word) {
groups.unshift({name: group.name, words: []})
group = group.previous
}
groups[0].words.push(i.name)
}
groups.reverse()
return groups
}
/*
* Pretty-print machine state into the console.
*/
dump() {
let data = show(this.data)
let program = show(this.program)
let wordGroups = this.parseWordGroups()
log("DATA\t:", data)
log(this.compilation? "COMPILE\t:" : "EVAL\t:", program)
log("--------")
wordGroups.forEach(group => {
log(group.name.slice(0,7) + "\t:", ...group.words)
})
log()
}
loop() {
while (!this.halt) {
this.next()
}
}
next() {
this.word()
this.dispatch()
this.dump()
}
word() {
this.program = this.program.trimLeft()
let words = this.program.split(/\s+/, 1)
let word
if (words.length) {
word = words[0]
this.program = this.program.slice(word.length)
} else {
word = this.program
this.program = ''
}
this.data.push(word)
}
dispatch() {
let name = this.data.pop()
let word = this.findWord(name)
if (word.immediate || !this.compilation) {
word.body.apply(this)
} else {
let acc = this.data.pop()
acc.push(word.body)
this.data.push(acc)
}
}
compile() {
this.compilation = true
}
evaluate() {
this.compilation = false
}
}
/*
* Forth-machine state.
*/
// let Machine = struct("Machine",
// "data", // data stack, place for all args & results
// "program", // string with "remaining program""
// "words", // linked list of named wordGroups
// "wordGroups", // linked list of named definition groups
// "compilation" // true if compiling, false if evaluating
// )
/*
* Function definition. Its called "word" in FORTH.
*/
class Word extends Struct(() => Word) {
constructor(args) {
let {
name, // word name
body, // function to be invoked
immediate, // that means, function will be invoked even in compilation mode
previous, // link to the previously defined function
isProtected // if true, cannot be removed with FORGET
} = args
super(args)
}
}
/*
* Group of words.
*/
class WordGroup extends Struct(() => WordGroup) {
constructor(args) {
let {
name, // group name
word, // link to the la[te]st word in group
previous // link to previous group
} = args
super(args)
}
}
/*
* Empty list type.
*/
class Empty extends Struct(() => Empty) {}
/*
* Empty list singleton.
*/
let empty = Empty.of({})
/*
* addEntry(Words, Word) : Words
*
* Adds a word to a list of words.
*/
function addWord(previous, word) {
return word.with({previous})
}
/*
* words(...Word) : Words
*
* Pack a list of words into the vocabulary.
*/
function words(...words) {
return words.reduce(addWord, empty)
}
function word(...args) {
switch (args.length) {
case 1: {
let func = args[0]
return Word.of({name: func.name, body: func})
}
case 2: {
let name = args[0]
let func = args[1]
return Word.of({name: name, body: func})
}
case 3: {
let name = args[0]
let func = args[1]
let immediate = args[2].immediate
return Word.of({name: name, body: func, immediate})
}
default:
throw "function `word` must be called with 1, 2 or 3 params"
}
}
/*
* Custom pretty-printer.
*/
function show(data) {
switch (typeof data) {
case "string": return data
case "function": return data.name || "<func>"
case "undefined": return "undefined"
case "object":
switch (data.constructor) {
case Array:
return "[" + data.map(show).join(" ") + "]"
case Word:
return data.name
+ (data.immediate? " [I]" : "")
+ ": " + show(data.body) + " "
+ show(data.previous)
case WordGroup:
return data.name + " -> " + data.word.name + " " + show(data.previous)
case Empty:
return "."
default:
return data.toString()
}
default:
return data.toString()
}
}
function squash(funcs) {
return function() {
funcs.forEach(func => func.apply(this))
}
}
let stdlib = words(
word(function dup() {
let x = this.data.pop()
this.data.push(x, x)
}),
word(function swap(params) {
let x = this.data.pop()
let y = this.data.pop()
this.data.push(x, y)
}),
word(function() {
this.halt = true
}),
word(":", function () {
this.word()
this.compile()
this.data.push([])
}),
word(";", function() {
let body = this.data.pop()
let name = this.data.pop()
this.addWord(word(name, squash(body)))
this.evaluate()
}, {immediate: true}),
word("[", function () {
this.evaluate()
}, {immediate: true}),
word("]", function () {
this.compile()
let name = this.data.pop()
let func = this.findWord(name).body
let acc = this.data.pop()
acc.push(func)
this.data.push(acc)
}),
word("+", function () {
let x = this.data.pop()
let y = this.data.pop()
this.data.push(x + y)
})
)
/*
* FORTH-Machine instance.
*/
var forth = Machine.of({
data: [],
program: ": lol [ 1 3 + ] 2 ; lol lol",
words: stdlib,
wordGroups: WordGroup.of({name: "forth", word: stdlib, previous: empty}),
compilation: false
})
forth.dump()
forth.loop()
module.exports.defined = x => x !== undefined
module.exports.log = console.log
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment