Created
June 25, 2023 11:39
-
-
Save oussamahamdaoui/aacb7ab608cbd35313c4851a2bcca0c1 to your computer and use it in GitHub Desktop.
Short interpreter for the whitespace language in node js
This file contains 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
//@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