commit 8a7517706099cca81568458927fe0967c630aa98 Author: filifa Date: Thu Dec 11 23:49:34 2025 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/index.html b/index.html new file mode 100644 index 0000000..5fc483c --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + mcalc + + + + + + + + + + + + + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..46f23be --- /dev/null +++ b/main.js @@ -0,0 +1,143 @@ +import { tokenize } from "./modules/lexer.js"; +import { shunt } from "./modules/parser.js"; + +function xgcd(a, b) { + let [old_r, r] = [a, b]; + let [old_s, s] = [1n, 0n]; + let [old_t, t] = [0n, 1n]; + + while (r !== 0n) { + const quotient = old_r / r; + [old_r, r] = [r, old_r - quotient * r]; + [old_s, s] = [s, old_s - quotient * s]; + [old_t, t] = [t, old_t - quotient * t]; + } + + return [old_r, old_s, old_t]; +} + +function modinv(x, modulus) { + let [r, s, t] = xgcd(x, modulus); + if (r !== 1n) { + throw new Error(`no inverse exists - ${x} and ${modulus} are not coprime`); + } + + if (s < 0n) { + s += modulus; + } + + return s; +} + +function modpow(base, exponent, modulus) { + if (exponent < 0n) { + const p = modpow(base, -exponent, modulus); + return modinv(p, modulus); + } + + if (modulus === 1n) { + return 0n; + } + + let result = 1n; + base %= modulus; + + while (exponent > 0) { + if (exponent % 2n === 1n) { + result *= base; + result %= modulus; + } + + exponent >>= 1n; + base *= base; + base %= modulus; + } + + return result; +} + +function binaryOpPop(stack) { + const b = stack.pop(); + const a = stack.pop(); + if (a === undefined || b === undefined) { + throw new Error("invalid expression"); + } + + return [a, b]; +} + +function compute(queue, modulus) { + const stack = []; + for (const token of queue) { + if (typeof token === "bigint") { + stack.push(token % modulus); + } else if (token === "+") { + const [a, b] = binaryOpPop(stack); + const c = (a + b) % modulus; + stack.push(c); + } else if (token === "-") { + const [a, b] = binaryOpPop(stack); + const c = (a - b) % modulus; + stack.push(c); + } else if (token === "*") { + const [a, b] = binaryOpPop(stack); + const c = (a * b) % modulus; + stack.push(c); + } else if (token === "/") { + const [a, b] = binaryOpPop(stack); + const binv = modinv(b, modulus); + const c = (a * binv) % modulus; + stack.push(c); + } else if (token === "^") { + const [a, b] = binaryOpPop(stack); + const c = modpow(a, b, modulus); + stack.push(c); + } + } + + if (stack.length !== 1) { + throw new Error("error evaluating expression"); + } + + let result = stack[0]; + if (result < 0n) { + result += modulus; + } + + return result; +} + +function calculate() { + const expr = document.querySelector("#expr"); + const modulus = document.querySelector("#modulus"); + const m = BigInt(modulus.value); + + const tokens = tokenize(expr.value); + console.log(tokens); + + const result = document.querySelector("#result"); + let queue = []; + try { + queue = shunt(tokens); + } catch(e) { + result.value = e; + console.log(e); + return; + } + + console.log(queue); + + let r = 0; + try { + r = compute(queue, m); + } catch(e) { + result.value = e; + console.log(e); + return; + } + + result.value = r; + +} + +document.querySelector("#enter").addEventListener("click", calculate); diff --git a/modules/lexer.js b/modules/lexer.js new file mode 100644 index 0000000..0f2c370 --- /dev/null +++ b/modules/lexer.js @@ -0,0 +1,24 @@ +function tokenize(expr) { + // NOTE: not handling whitespace + // NOTE: currently ends early if string doesn't match token + // FIXME: need to handle unary minus (e.g. on parentheses) + const regexp = /-?[0-9]+|[-+*/^]|\(|\)/gy; + const matches = expr.matchAll(regexp); + + const tokens = []; + for (const match of matches) { + if (/[0-9]+/.test(match[0])) { + tokens.push(BigInt(match[0])); + } else if (/[-+*^/]/.test(match[0])) { + tokens.push(match[0]) + } else if (match[0] == "(") { + tokens.push("("); + } else if (match[0] == ")") { + tokens.push(")"); + } + } + + return tokens; +} + +export { tokenize }; diff --git a/modules/parser.js b/modules/parser.js new file mode 100644 index 0000000..85f2caa --- /dev/null +++ b/modules/parser.js @@ -0,0 +1,69 @@ +function isLeftAssociative(op) { + return op === "+" || op === "-" || op === "*" || op === "/"; +} + +function popOps(opstack, queue, op) { + const prec = {"+": 1, "-": 1, "*": 2, "/": 2, "^": 3} + while (true) { + const op2 = opstack.at(-1); + if (op2 === undefined) { + break; + } else if (op2 === "(") { + break; + } + + if ((prec[op2] > prec[op]) || (prec[op2] === prec[op] && isLeftAssociative(op))) { + opstack.pop(); + queue.push(op2); + } else { + break; + } + } + + opstack.push(op); +} + +function popBetweenParens(opstack, queue) { + while (opstack.at(-1) !== "(") { + if (opstack.length === 0) { + throw new Error("mismatched parentheses"); + } + + const op = opstack.pop(); + queue.push(op); + } + + opstack.pop(); +} + +function empty(opstack, queue) { + while (opstack.length !== 0) { + const op = opstack.pop(); + if (op === "(") { + throw new Error("mismatched parentheses"); + } + + queue.push(op); + } +} + +function shunt(tokens) { + const queue = []; + const opstack = []; + for (const token of tokens) { + if (typeof token === "bigint") { + queue.push(token); + } else if (/[-+*/^]/.test(token)) { + popOps(opstack, queue, token); + } else if (token === "(") { + opstack.push(token); + } else if (token === ")") { + popBetweenParens(opstack, queue); + } + } + + empty(opstack, queue); + return queue; +} + +export { shunt };