Skip to content

Instantly share code, notes, and snippets.

@scorbiclife
Last active April 14, 2025 08:15
Show Gist options
  • Save scorbiclife/2987519288c3c9c680c2058f6ede0199 to your computer and use it in GitHub Desktop.
Save scorbiclife/2987519288c3c9c680c2058f6ede0199 to your computer and use it in GitHub Desktop.
Conceptually simple forth implementation
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]();
}
}
});
@scorbiclife
Copy link
Author

scorbiclife commented Apr 12, 2025

Sample program: factorial

[ :n [ $n 0 <= [ 1 ] [ $n 1 - :n $this call ; $n * ] if ] :this $this call ; ; ] :factorial
5 factorial print
120

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment