Last active
April 14, 2025 08:15
-
-
Save scorbiclife/2987519288c3c9c680c2058f6ede0199 to your computer and use it in GitHub Desktop.
Conceptually simple forth implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const TERMINAL_INPUT_START = 1000; | |
const USER_CODE_START = 2000; | |
/** @type {unknown[]} */ | |
const code = []; | |
code.push( | |
_return, | |
push, | |
quit, | |
print, | |
drop, | |
stack, | |
get, | |
define, | |
undefine, | |
words, | |
begin, | |
end, | |
call, | |
if_, | |
add, | |
subtract, | |
multiply, | |
divide, | |
equals, | |
lt, | |
lte, | |
gt, | |
gte, | |
); | |
code.length = USER_CODE_START; // user code gets written with `code.push` | |
const builtinNamesByOpcode = [ | |
[], | |
[], | |
["quit", "q"], | |
["print", "p"], | |
["drop", "d"], | |
["stack", "s"], | |
[], | |
[], | |
[";"], | |
["words"], | |
["["], | |
["]"], | |
["call"], | |
["if"], | |
["+"], | |
["-"], | |
["*"], | |
["/"], | |
["="], | |
["<"], | |
["<="], | |
[">"], | |
[">="], | |
]; | |
const op = {}; | |
const dictionary = []; | |
// initialize builtin words | |
for (let i = 0; i < builtinNamesByOpcode.length; ++i) { | |
// @ts-ignore | |
const functionName = code[i].name; | |
const wordNames = builtinNamesByOpcode[i]; | |
op[functionName] = i; | |
wordNames.forEach((name) => dictionary.push(name, i)); | |
} | |
// words delimiting the start and end of brackets (compilation) | |
// should be able to run even when compiling | |
const immediateWords = [op.begin, op.end]; | |
// code[ip] contains the opcode to execute | |
let ip; | |
// code[w] contains the function to execute | |
let w; | |
const callStack = []; | |
const dataStack = []; | |
let bracketStack = []; | |
/** the inner interpreter */ | |
function _next() { | |
++ip; | |
w = code[ip]; | |
} | |
function _call() { | |
callStack.push(ip); | |
ip = w; | |
_next(); | |
} | |
function _return() { | |
ip = callStack.pop(); | |
_next(); | |
} | |
/** builtin functions */ | |
function quit() { | |
globalThis.process.exit(); | |
} | |
function push() { | |
++ip; | |
dataStack.push(code[ip]); | |
_next(); | |
} | |
function print() { | |
console.log(dataStack.pop()); | |
_next(); | |
} | |
function drop() { | |
dataStack.pop(); | |
_next(); | |
} | |
function stack() { | |
console.debug(dataStack); | |
_next(); | |
} | |
function findWordFromDictionary(nameToFind) { | |
for (let i = dictionary.length - 1; i - 1 >= 0; i -= 2) { | |
const [name, value] = [dictionary[i - 1], dictionary[i]]; | |
if (name === nameToFind) { | |
return value; | |
} | |
} | |
throw new Error(`Word Not Found: ${nameToFind}`); | |
} | |
function get() { | |
++ip; | |
let nameToFind = code[ip]; | |
try { | |
const value = findWordFromDictionary(nameToFind); | |
dataStack.push(value); | |
} catch (error) { | |
console.error(error.message); | |
} | |
_next(); | |
} | |
function define() { | |
++ip; | |
const name = code[ip]; | |
let value = dataStack.pop(); | |
dictionary.push(name); | |
dictionary.push(value); | |
_next(); | |
} | |
function undefine() { | |
dictionary.pop(); | |
dictionary.pop(); | |
_next(); | |
} | |
function words() { | |
console.debug(dictionary); | |
_next(); | |
} | |
/** compilation */ | |
function begin() { | |
bracketStack.push(dataStack.length); | |
dataStack.push(_call); | |
_next(); | |
} | |
function end() { | |
const codeAddress = code.length; | |
const bracketStart = bracketStack.pop(); | |
dataStack.push(op._return); | |
for (let i = bracketStart; i < dataStack.length; ++i) { | |
code.push(dataStack[i]); | |
} | |
dataStack.length = bracketStart; | |
if (bracketStack.length === 0) { | |
// this is going to be interpreted | |
dataStack.push(codeAddress); | |
} else { | |
// this is going to be compiled | |
dataStack.push(op.push); | |
dataStack.push(codeAddress); | |
} | |
_next(); | |
} | |
function call() { | |
w = dataStack.pop(); | |
_call(); | |
} | |
/** conditionals */ | |
function if_() { | |
const falseBranch = dataStack.pop(); | |
const trueBranch = dataStack.pop(); | |
const condition = dataStack.pop(); | |
w = condition ? trueBranch : falseBranch; | |
_call(); | |
} | |
/** standard library */ | |
function add() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x + y); | |
_next(); | |
} | |
function subtract() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x - y); | |
_next(); | |
} | |
function multiply() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x * y); | |
_next(); | |
} | |
function divide() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x / y); | |
_next(); | |
} | |
function equals() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x === y); | |
_next(); | |
} | |
function lt() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x < y); | |
_next(); | |
} | |
function lte() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x <= y); | |
_next(); | |
} | |
function gt() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x > y); | |
_next(); | |
} | |
function gte() { | |
const y = dataStack.pop(); | |
const x = dataStack.pop(); | |
dataStack.push(x >= y); | |
_next(); | |
} | |
/** The outer interpreter */ | |
let terminalInputEnd; | |
function readWord(word) { | |
// words that match a number regex are numbers | |
if (/[+-]?\d+/.test(word)) { | |
code[terminalInputEnd++] = op.push; | |
code[terminalInputEnd++] = Number(word); | |
return; | |
} | |
// words that start with $ are syntax sugar for variables | |
if ("$".includes(word[0])) { | |
code[terminalInputEnd++] = op.get; | |
code[terminalInputEnd++] = word.slice(1); | |
return; | |
} | |
// words that start with : are definitions | |
if (":".includes(word[0])) { | |
code[terminalInputEnd++] = op.define; | |
code[terminalInputEnd++] = word.slice(1); | |
return; | |
} | |
// other words are commands | |
let value; | |
try { | |
value = findWordFromDictionary(word); | |
code[terminalInputEnd++] = value; | |
} catch (error) { | |
console.error(error.message); | |
} | |
} | |
const process = require("process"); | |
const rl = require("readline"); | |
const rli = rl.createInterface(process.stdin); | |
rli.on("line", (line) => { | |
terminalInputEnd = TERMINAL_INPUT_START; | |
const words = line.split(/\s+/).filter((w) => w !== ""); | |
for (const word of words) { | |
readWord(word); | |
} | |
ip = TERMINAL_INPUT_START; | |
w = code[ip]; | |
while (ip !== terminalInputEnd) { | |
if (bracketStack.length > 0 && !immediateWords.includes(w)) { | |
dataStack.push(w); | |
_next(); | |
} else { | |
if (w === undefined) { | |
console.error(`error: invalid address`); | |
break; | |
} | |
// @ts-ignore | |
code[w](); | |
} | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample program: factorial