Skip to content

Instantly share code, notes, and snippets.

@oussamahamdaoui
Created June 25, 2023 11:39
Show Gist options
  • Save oussamahamdaoui/aacb7ab608cbd35313c4851a2bcca0c1 to your computer and use it in GitHub Desktop.
Save oussamahamdaoui/aacb7ab608cbd35313c4851a2bcca0c1 to your computer and use it in GitHub Desktop.
Short interpreter for the whitespace language in node js
//@ts-check
const { readFileSync } = require('fs');
const { join } = require('path');
const readline = require('readline');
/** @typedef {Exclude<typeof instructions[number]["name"], "NEW_LABEL">} INSTRUCTION_NAME */
/** @typedef {{name:INSTRUCTION_NAME}} INSTRUCTION */
/** @typedef {{value:bigint, type:"NUMBER"}|{type:"LABEL", value:string}} VALUE */
/** @typedef {VALUE|INSTRUCTION} VALUE_OR_INSTRUCTION */
/** @typedef {{[key:string]:number}} LABELS */
/**
* @template T
* @template {number} N
* @template {readonly T[]} [R=[]]
* @typedef {R['length'] extends N ? R : Tuple<T, N, readonly [T, ...R]>} Tuple
* */
const S = " ";
const T = "\t";
const L = "\n";
const NOT_WHITE_SPACE = new RegExp(`[^${S}${T}${L}]+`, "g");
let stdBuff = "";
/**
* @param {"string"} [type]
* @returns {bigint|Promise<bigint>}
*/
const scan = (type) => {
if (type === "string" && stdBuff) {
const char = stdBuff.charCodeAt(0);
stdBuff = stdBuff.slice(1);
return BigInt(char);
}
return new Promise(r => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
rl.on('line', (line) => {
if (type === "string") {
const str = line.toString() + "\n";
stdBuff = str.slice(1);
r(BigInt(str.charCodeAt(0)))
} else {
r(BigInt(line.toString()))
}
rl.close();
});
})
}
const instructions = /**@type {const} */ ([
{
reg: new RegExp(`^${S}${S}`),
name: "PUSH",
params: ["NUMBER"]
},
{
reg: new RegExp(`^${S}${L}${S}`),
name: "COPY_TOP",
params: [],
},
{
reg: new RegExp(`^${S}${T}${S}`),
name: "COPY",
params: ["NUMBER"]
},
{
reg: new RegExp(`^${S}${L}${T}`),
name: "SWAP",
params: [],
},
{
reg: new RegExp(`^${S}${T}${L}`),
name: "SLIDE",
params: [],
},
{
reg: new RegExp(`^${S}${L}${L}`),
name: "POP",
params: [],
},
{
reg: new RegExp(`^${T}${S}${S}${S}`),
name: "ADD",
params: [],
},
{
reg: new RegExp(`^${T}${S}${S}${T}`),
name: "SUB",
params: [],
},
{
reg: new RegExp(`^${T}${S}${S}${L}`),
name: "MUL",
params: [],
},
{
reg: new RegExp(`^${T}${S}${T}${S}`),
name: "DIV",
params: [],
},
{
reg: new RegExp(`^${T}${S}${T}${T}`),
name: "MOD",
params: [],
},
{
reg: new RegExp(`^${T}${T}${S}`),
name: "PUT_HEAP",
params: [],
},
{
reg: new RegExp(`^${T}${T}${T}`),
name: "GET_HEAP",
params: [],
},
{
reg: new RegExp(`^${L}${S}${S}`),
name: "NEW_LABEL",
params: ["LABEL"],
},
{
reg: new RegExp(`^${L}${S}${T}`),
name: "CALL_LABEL",
params: ["LABEL"],
},
{
reg: new RegExp(`^${L}${S}${L}`),
name: "JUMP_LABEL",
params: ["LABEL"],
},
{
reg: new RegExp(`^${L}${T}${S}`),
name: "JUMP_LABEL_IF_0",
params: ["LABEL"],
},
{
reg: new RegExp(`^${L}${T}${T}`),
name: "JUMP_LABEL_IF_LT_0",
params: ["LABEL"],
},
{
reg: new RegExp(`^${L}${T}${L}`),
name: "RETURN",
params: [],
},
{
reg: new RegExp(`^${L}${L}${L}`),
name: "END_PROG",
params: [],
},
{
reg: new RegExp(`^${T}${L}${S}${S}`),
name: "PRINT_CHAR",
params: [],
},
{
reg: new RegExp(`^${T}${L}${S}${T}`),
name: "PRINT_INT",
params: [],
},
{
reg: new RegExp(`^${T}${L}${T}${S}`),
name: "SCAN_CHAR",
params: [],
},
{
reg: new RegExp(`^${T}${L}${T}${T}`),
name: "SCAN_INT",
params: [],
},
]);
const PARAM_TYPES = {
LABEL: {
reg: new RegExp(`^[${S}${T}]*${L}`),
/** @param {string} str */
getValue: (str) => {
return (str || "")
.replace(new RegExp(S, "g"), "0")
.replace(new RegExp(T, "g"), "1")
.replace(new RegExp(L, "g"), "")
}
},
NUMBER: {
reg: new RegExp(`^[${S}${T}]+${L}`),
/** @param {string} str */
getValue: (str) => {
const sn = str[0] === S ? 1n : -1n;
const bin = str.slice(1)
.replace(new RegExp(S, "g"), "0")
.replace(new RegExp(T, "g"), "1")
.replace(new RegExp(L, "g"), "");
const n = sn * BigInt("0b" + (bin.length === 0 ? "0" : bin))
return n;
}
}
}
/** @param {string} src */
const tokenize = (src) => {
src = src.replace(NOT_WHITE_SPACE, "");
const tokens = /** @type {VALUE_OR_INSTRUCTION[]} */ ([]);
const labels = /** @type {LABELS}*/ ({});
while (true) {
const match = instructions
.map((e, i) => ({ length: src.match(e.reg)?.[0].length || 0, index: i }))
.sort((a, b) => b.length - a.length)[0];
if (match.length === 0) throw Error(`Instruction not valid`);
const { name, params } = instructions[match.index];
src = src.slice(match.length);
if (name === "NEW_LABEL") {
params.forEach((argType) => {
const argument = src.match(PARAM_TYPES[argType].reg)?.[0];
if (!argument) throw Error(`Expected ${argType} argument`);
labels[PARAM_TYPES[argType].getValue(argument)] = tokens.length - 1;
src = src.slice(argument.length);
});
continue;
}
tokens.push({ name });
if (params) {
params.forEach((argType) => {
const argument = src.match(PARAM_TYPES[argType].reg)?.[0];
if (!argument) throw Error(`Expected ${argType} argument`);
tokens.push({
value: PARAM_TYPES[argType].getValue(argument),
type: argType,
});
src = src.slice(argument.length);
});
}
if (src.length === 0) break;
}
return { pgm: tokens, labels };
}
/**
* @template {number}U
* @template T
* @param {T[]} stack
* @param {U} nb
* @returns {Tuple<T,U>}
*/
const popValidate = (stack, nb) => {
return /**@type {Tuple<T,U>}*/ (Array.from({ length: nb }, () => {
const v = stack.pop();
if (v === undefined) {
throw Error("Not enaugh values on the stack");
}
return v;
}))
}
/**
* @template {VALUE["type"]} T
* @param {VALUE_OR_INSTRUCTION} arg
* @param {T} expectedType
* @returns {Extract<VALUE, {type:T}>}
*/
const validateArgType = (arg, expectedType) => {
if (!arg || !("type" in arg) || expectedType !== arg.type) {
throw Error(`Expected parameter of type ${expectedType}`);
}
// @ts-ignore
return arg;
}
/**
* @param {LABELS} labels
* @param {string} label
*/
const labelExists = (labels, label) => {
if (labels[label] === undefined) throw Error(`Label not found`);
return label;
}
/** @param {ReturnType<typeof tokenize>} param0 */
const run = async ({ pgm, labels }) => {
const stack = /** @type {bigint[]}*/ ([]);
const heap = /** @type {bigint[]}*/ ([]);
const callStack = /** @type {number[]}*/ ([]);
let pc = 0;
let steps = 0;
const execute = /** @type {Record<INSTRUCTION_NAME,()=>void>} */ ({
"PUSH": () => {
const nextToken = validateArgType(pgm[pc + 1], "NUMBER");
stack.push(nextToken.value);
pc++;
},
"COPY_TOP": () => {
const [v] = popValidate(stack, 1);
stack.push(v, v);
},
"COPY": () => {
const nextToken = validateArgType(pgm[pc + 1], "NUMBER");
const n = Number(nextToken.value);
stack.push(stack[stack.length - n]);
pc++;
},
"SWAP": () => {
const temp = stack[stack.length - 1];
stack[stack.length - 1] = stack[stack.length - 2];
stack[stack.length - 2] = temp;
},
"SLIDE": () => {
const nextToken = validateArgType(pgm[pc + 1], "NUMBER");
const n = Number(nextToken.value);
stack.splice(stack.length - 1 - n, n);
pc++;
},
"POP": () => popValidate(stack, 1),
"ADD": () => {
const [b, a] = popValidate(stack, 2);
stack.push(a + b)
},
"SUB": () => {
const [b, a] = popValidate(stack, 2);
stack.push(a - b)
},
"MUL": () => {
const [b, a] = popValidate(stack, 2);
stack.push(a * b)
},
"DIV": () => {
const [b, a] = popValidate(stack, 2);
stack.push(a / b)
},
"MOD": () => {
const [b, a] = popValidate(stack, 2);
stack.push(a % b)
},
"PUT_HEAP": () => {
const [val, addr] = popValidate(stack, 2);
heap[Number(addr)] = val;
},
"GET_HEAP": () => {
const [addr] = popValidate(stack, 1);
const val = heap[Number(addr)];
stack.push(val);
},
"CALL_LABEL": () => {
const nextToken = validateArgType(pgm[pc + 1], "LABEL");
const label = labelExists(labels, nextToken.value);
callStack.push(pc + 1);
pc = labels[label];
},
"RETURN": () => {
[pc] = popValidate(callStack, 1);
},
"JUMP_LABEL": () => {
const nextToken = validateArgType(pgm[pc + 1], "LABEL");
const label = labelExists(labels, nextToken.value);
pc = labels[label];
},
"JUMP_LABEL_IF_0": () => {
const nextToken = validateArgType(pgm[pc + 1], "LABEL");
const label = labelExists(labels, nextToken.value);
const [v] = popValidate(stack, 1);
if (v === 0n) {
pc = labels[label];
} else {
pc++
}
},
"JUMP_LABEL_IF_LT_0": () => {
const nextToken = validateArgType(pgm[pc + 1], "LABEL");
const label = labelExists(labels, nextToken.value);
const [v] = popValidate(stack, 1);
if (v < 0n) {
pc = labels[label];
} else {
pc++
}
},
"PRINT_CHAR": () => {
const [charCde] = popValidate(stack, 1);
const char = String.fromCharCode(Number(charCde));
process.stdout.write(char);
},
"PRINT_INT": () => {
const [charCde] = popValidate(stack, 1);
process.stdout.write(charCde.toString());
},
"SCAN_CHAR": async () => {
const [addr] = popValidate(stack, 1);
const char = await scan("string");
heap[Number(addr)] = char;
},
"SCAN_INT": async () => {
const [addr] = popValidate(stack, 1);
const num = await scan();
heap[Number(addr)] = num;
},
"END_PROG": () => {
process.stdout.write('\n');
pc = pgm.length;
}
});
while (pgm[pc]) {
const fetch = pgm[pc];
if (!fetch || !("name" in fetch)) throw new Error(`Unexpected instruction`);
const cmd = execute[fetch.name];
await cmd();
pc++;
steps++;
}
}
(async () => {
const src = readFileSync(join(process.cwd(), process.argv[2]), 'utf-8');
await run(tokenize(src));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment