1
0
mirror of https://github.com/osmarks/website synced 2025-09-02 10:48:03 +00:00

RPNCalcv4 and stuff

This commit is contained in:
2020-11-17 20:41:31 +00:00
parent c179ee8d45
commit f3bde26728
11 changed files with 1443 additions and 9 deletions

View File

@@ -0,0 +1,128 @@
import {defnOp, makeOp, defn} from './shiny.mjs';
import {parseExprs} from './parse.mjs';
import {tokenize} from './token.mjs';
const objEq = (a, b) => {
if (typeof a !== 'object' && typeof b !== 'object') {
return a === b;
} else if (typeof a !== 'object' || typeof b !== 'object') {
return false;
} else {
const aprop = Object.getOwnPropertyNames(a);
const bprop = Object.getOwnPropertyNames(b);
if (bprop.length !== aprop.length) {
return false;
}
for (let i = 0; i < aprop.length; i++) {
if (!objEq(a[aprop[i]], b[aprop[i]])) {
return false;
}
}
return true;
}
}
export let scope = {};
export const customHandler = (ins) => {
return [ins];
}
const addRPNDefn = (name, def) => {
let toks = tokenize(def);
if (!toks) {
throw 'could not load builtin'
}
toks = toks.map(elem => {
elem.startPos = 0;
elem.endPos = 0;
return elem;
});
let ast = parseExprs(toks);
if (!ast.parsed) {
throw 'could not load builtin'
}
scope = defn(name, ast.parsed.arr, scope);
}
const assertType = (type) => (elem) => {
if (elem.type !== type) {
throw 'typeerror'
}
}
const addDefn = (name, args, fn) => {
if (Array.isArray(args)) {
const nargs = [...Array(args.length).keys()];
const liftFn = (scope) => {
let newscope = Object.create(scope);
for (let i = 0; i < args.length; i++) {
assertType(args[i])(scope[i][0]);
newscope[i] = scope[i][0].val;
}
return fn(newscope);
}
const op = makeOp(nargs, liftFn);
defnOp(name, op);
} else {
const nargs = [...Array(args).keys()];
const liftFn = (scope) => {
let newscope = Object.create(scope);
for (let i = 0; i < args; i++) {
newscope[i] = scope[i][0];
}
return fn(newscope);
}
const op = makeOp(nargs, liftFn);
defnOp(name, op);
}
}
const add = (args) => [{type:"num", val:args[0] + args[1]}];
const sub = (args) => [{type:"num", val:args[0] - args[1]}];
const div = (args) => [{type:"num", val:args[0] / args[1]}];
const mult = (args) => [{type:"num", val:args[0] * args[1]}];
const pow = (args) => [{type:"num", val:Math.pow(args[0], args[1])}];
const root = (args) => [{type:"num", val:Math.sqrt(args[0])}];
const type = (args) => [{type:"type", val:args[0].type}];
const pair = (args) => [{type:"pair", val:{fst:args[0], snd:args[1]}}];
const fst = (args) => [args[0].fst];
const snd = (args) => [args[0].snd];
const tuple = (args) => makeOp([...Array(args[0]).keys()], (args) => {return [{type:"tuple", val:args}]});
const index = (args) => args[0][args[1]];
const len = (args) => [{type:"num", val:args[0].length}];
const eq = (args) => {
if (args[2].type === args[3].type && objEq(args[2].val, args[3].val)) {
return [args[0]];
} else {
return [args[1]];
}
}
addDefn("+", ["num", "num"], add);
addDefn("-", ["num", "num"], sub);
addDefn("/", ["num", "num"], div);
addDefn("*", ["num", "num"], mult);
addDefn("^", ["num", "num"], pow);
addDefn("sqrt", ["num"], root);
addDefn("==", 4, eq);
addDefn("pair", 2, pair);
addDefn("fst", ["pair"], fst);
addDefn("snd", ["pair"], snd);
addDefn("tuple", ["num"], tuple);
addDefn("!!", ["tuple", "num"], index);
addDefn("len", ["tuple"], len);
addDefn("typeof", 1, type);
addRPNDefn("stop", "\"stop");
addRPNDefn("inv", "(x -> 1 x /)");
addRPNDefn("fold", "(x acc fn -> acc '(-> x acc fn 'fn fold) 'x stop ==)");
addRPNDefn("range", "(x y -> x '(->x x 1 + y range) 'x y ==)");
addRPNDefn("listthen", "(fn -> (internal; x acc -> '(->acc fn) '(->x acc pair internal) x stop ==) 0 tuple internal)");
addRPNDefn("list", "'(a -> a) listthen");
addRPNDefn("lmap", "(list fn -> list '(->list fst fn list snd 'fn lmap pair) list 0 tuple ==)");
addRPNDefn("unlist", "(l -> (internal; list -> '(->) '(->list fst list snd internal) list 0 tuple ==) stop l internal)");
addRPNDefn("map", "fn -> '(l->l 'fn lmap unlist) listthen");

View File

@@ -0,0 +1,48 @@
---
title: RPNCalc v4
slug: rpncalc4
description: Reverse Polish Notation calculator, version 4 - increasingly esoteric and incomprehensible. Contributed by Aidan.
---
<style>
textarea {
width: 100%;
height: 30%;
}
code {
background-color:rgb(230, 230, 230);
padding: 0 0.125rem;
}
</style>
<textarea id="inbox"></textarea>
<button id="step">step</button>
<button id="play">play</button>
<button id="load">load</button>
<input type="checkbox" id="use-std" name="use-std" checked>
<label for="use-std">use stdlib?</label>
<pre id="insbox"></pre>
<pre id="outbox"></pre>
<script src="./main.js" type="module"></script>
<h3>documentation</h3>
<p>use <code>(name; value)</code> to define something. the definition can be recursive. <code>value</code> is executed and <code>name</code> is set to the final state of the stack, i.e. <code>(name; 1 3)</code> is possible</p>
<p>use <code>'</code> to push instead of apply to the stack, e.g. <code>'(a -> a)</code>. This is useful for lazyness, i.e. <code>'(->lazy evaluated thing)</code></p>
<ul>
<li><code>+, -, *, /, ^, sqrt</code>: mathematical operations</li>
<li><code>==</code>: equality (automatically derived for all types); returns <code>a b -> a</code> if true, <code>a b -> b</code> if false</li>
<li><code>typeof</code>: returns the type of the object</li>
<li><code>pair, fst, snd</code>: pairs two objects, gets first or second item of pair</li>
<li><code>tuple</code>: used like <code>... 3 tuple</code>; creates an n tuple of n items on the stack</li>
<li><code>!!</code>: index into a tuple</li>
<li><code>len</code>: length of a tuple</li>
</ul>
<h3>stdlib</h3>
<ul>
<li><code>stop; "stop</code></li>
<li><code>inv; x -> 1 x /</code></li>
<li><code>fold; x acc fn -> acc '(-> x acc fn 'fn fold) 'x \"stop ==</code></li>
<li><code>range; x y -> x '(->x x 1 + y range) 'x y ==</code></li>
<li><code>listthen; fn -> (internal; x acc -> '(->acc fn) '(->x acc pair internal) x stop ==) 0 tuple internal</code></li>
<li><code>list; (a -> a) listthen</code></li>
<li><code>lmap; list fn -> list '(->list fst fn list snd 'fn lmap pair) list 0 tuple ==</code></li>
<li><code>unlist; l -> (internal; list -> '(->) '(->list fst list snd internal) list 0 tuple ==) stop l internal</code></li>
<li><code>map; fn -> '(l->l 'fn lmap unlist) listthen</code></li>
</ul>

View File

@@ -0,0 +1,130 @@
import {execRPN, step} from './shiny.mjs';
import {parseExprs} from './parse.mjs';
import {tokenize} from './token.mjs';
import {scope, customHandler} from './builtin.mjs';
//const scope = {};
//const customHandler = a => [a];
const inbox = document.getElementById("inbox")
const insbox = document.getElementById("insbox")
const outbox = document.getElementById("outbox")
const stepb = document.getElementById("step")
const play = document.getElementById("play")
const load = document.getElementById("load")
const usestd = document.getElementById("use-std")
let state = null;
let input = null;
const highlight = (str, start, end, color) => {
return str.slice(0, start) + `<span style='background-color:${color}'>` + str.slice(start, end) + "</span>" + str.slice(end);
}
const loadState = () => {
input = inbox.value;
let toks = tokenize(input);
let ast = parseExprs(toks);
console.log(ast);
if (ast.parsed === null) {
insbox.innerHTML = "could not parse AST!"
} else if (ast.stream.length !== 0) {
insbox.innerHTML = highlight(input, ast.stream[0].startPos, ast.stream[0].endPos, "red");
insbox.innerHTML += "<br>unexpected token"
input = null;
state = null;
return;
}
insbox.innerHTML = input;
outbox.innerHTML = "";
if (usestd.checked) {
state = {scopes:[scope], stacks:[[]], calls:[ast.parsed.arr]};
} else {
state = {scopes:[{}], stacks:[[]], calls:[ast.parsed.arr]};
}
}
const showIns = (ins) => {
if (ins.val) {
return ins.val;
} else if (ins.ident) {
return ins.ident;
} else if (ins.name) {
return ins.name;
} else {
return 'anon';
}
}
load.onclick = _ => {
loadState();
}
play.onclick = _ => {
if (state === null) {
loadState();
}
insbox.innerHTML = "";
while (state.calls[0].length > 0 || state.calls.length > 1) {
try {
step(state, customHandler, showIns);
} catch (err) {
insbox.innerHTML = err;
state = null;
input = null;
return;
}
}
outbox.innerHTML = prettyprint(state.stacks[0]);
state = null;
input = null;
}
stepb.onclick = _ => {
if (state === null) {
return;
}
if (state.calls[0].length > 0 || state.calls.length > 1) {
let pos;
try {
pos = step(state, customHandler, showIns);
} catch (err) {
insbox.innerHTML = err;
state = null;
input = null;
return;
}
if (!(pos.start === 0 && pos.end === 0)) {
insbox.innerHTML = highlight(input, pos.start, pos.end, "green");
}
if (state.stacks.length > 1) {
outbox.innerHTML = "... " + prettyprint(state.stacks[state.stacks.length-1]);
} else {
outbox.innerHTML = prettyprint(state.stacks[0]);
}
}
}
const show = (elem) => {
if (elem.type === "pair") {
return "{" + show(elem.val.fst) + ", " + show(elem.val.snd) + "}"
} else if (elem.type === "closure") {
return "(needs " + elem.val.args.length + ")"
} else if (elem.type === "array") {
return "[" + prettyprint(elem.val) + "]"
} else {
return "(" + elem.val + ": " + elem.type + ")"
}
}
const prettyprint = (out) => {
let str = "";
for (let i = 0; i < out.length; i++) {
str += show(out[i]);
if (i < out.length - 1) {
str += " ";
}
}
return str;
}

View File

@@ -0,0 +1,231 @@
/* make parser safe */
const debug = false;
const attempt = (parser) => (stream) => {
if (debug) {
console.log("attempt");
console.log(stream);
}
let streamclone = [...stream];
try {
let out = parser(stream);
return out;
} catch(err) {
return {parsed:null,stream:streamclone};
}
}
/* chain */
const or = (a, b) => (stream) => {
let streamclone = [...stream];
if (debug) {
console.log("or");
console.log(stream);
}
let aout = a(stream);
if (aout.parsed === null) {
return b(streamclone);
} else {
return aout;
}
}
/* (parser) */
const parens = (parser) => (stream) => {
if (debug) {
console.log("parens");
console.log(stream);
}
let a = parseSyntax("(")(stream);
if (a.parsed === null) {
return {parsed:null, stream:a.stream};
}
let dat = parser(a.stream);
let b = parseSyntax(")")(dat.stream);
if (b.parsed === null) {
throw 'mismatched parens!';
}
dat.parsed.pos.start = a.parsed.pos.start;
dat.parsed.pos.end = b.parsed.pos.end;
return {parsed:dat.parsed, stream:b.stream};
}
/* [parser] */
const many = (parser) => (stream) => {
if (debug) {
console.log("many");
console.log(stream);
}
let parsed = [];
while (true) {
let i = parser(stream);
stream = i.stream;
if (i.parsed === null) {
break;
} else {
parsed.push(i.parsed);
}
}
let startPos = 0;
let endPos = 0;
if (parsed.length > 1) {
startPos = parsed[0].pos.start;
endPos = parsed[parsed.length-1].pos.start;
}
return {parsed:{arr:parsed, pos:{start:startPos, end:endPos}}, stream:stream};
}
/* takes in stream, outputs parsed item or null */
const parseIdent = (stream) => {
if (debug) {
console.log("ident");
console.log(stream);
}
let e = stream[0];
if (e === undefined) {
return {parsed:null, stream:stream};
}
if (e.type !== "ident") {
return {parsed:null, stream:stream};
} else {
stream.shift();
return {parsed:{type:e.type, val:e.name, pos:{start:e.startPos, end:e.endPos}}, stream:stream};
}
}
/* takes in stream, outputs parsed item or null */
const parseNum = (stream) => {
if (debug) {
console.log("num");
console.log(stream);
}
let e = stream[0];
if (e === undefined) {
return {parsed:null, stream:stream};
}
if (e.type !== "num") {
return {parsed:null, stream:stream};
} else {
stream.shift();
return {parsed:{type:e.type, val:e.val, pos:{start:e.startPos, end:e.endPos}}, stream:stream};
}
}
/* takes in stream, outputs parsed item or null */
const parseSyntax = (syntax) => (stream) => {
if (debug) {
console.log("syntax", syntax);
console.log(stream);
}
let e = stream[0];
if (e === undefined) {
return {parsed:null, stream:stream};
}
if (e.type !== "syntax") {
return {parsed:null, stream:stream};
}
if (e.val !== syntax) {
return {parsed:null, stream:stream};
} else {
stream.shift();
return {parsed:{type:"syntax", val:syntax, pos:{start:e.startPos, end:e.endPos}}, stream:stream};
}
}
/* takes in stream, outputs string or null - FAILABLE */
const parseName = (stream) => {
if (debug) {
console.log("name");
console.log(stream);
}
let id = parseIdent(stream);
if (id.parsed === null) {
return {parsed:null, stream:id.stream};
}
let syn = parseSyntax(";")(id.stream);
if (syn.parsed === null) {
throw 'could not parse name!'
}
return {parsed:{name:id.parsed.val, pos:{start:id.parsed.pos.start, end:syn.parsed.pos.end}}, stream:syn.stream};
}
const parseType = (stream) => {
if (debug) {
console.log("type");
console.log(stream);
}
let syn = attempt(parseSyntax("\""))(stream);
if (syn.parsed === null) {
return {parsed:null, stream:syn.stream};
}
let id = or(parseIdent, parseNum)(syn.stream);
if (id.parsed === null) {
return {parsed:null, stream:id.stream};
}
return {parsed:{type:"type", val:id.parsed.val, pos:{start:syn.parsed.pos.start, end:id.parsed.pos.end}}, stream:id.stream};
}
/* takes in stream, outputs parsed item or null - FAILABLE */
const parsePush = (stream) => {
if (debug) {
console.log("push");
console.log(stream);
}
let syn = attempt(parseSyntax("'"))(stream);
if (syn.parsed === null) {
return {parsed:null, stream:syn.stream};
}
let id = parseExpr(syn.stream);
if (id.parsed === null) {
return {parsed:null, stream:id.stream};
}
return {parsed:{type:"push", elem:id.parsed, pos:{start:syn.parsed.pos.start, end:id.parsed.pos.end}}, stream:id.stream};
}
const parseDefn = (stream) => {
if (debug) {
console.log("defn");
console.log(stream);
}
let name = parseName(stream);
if (name.parsed === null) {
throw 'no name found!'
}
let expr = parseExprs(stream);
if (expr.parsed === null) {
throw 'no body found!'
}
return {parsed:{type:"defn", ident:name.parsed.name, defn:expr.parsed.arr, pos:{start:name.parsed.pos.start, end:expr.parsed.pos.end}}, stream:expr.stream}
}
/* takes in stream, outputs parsed item or null - FAILABLE */
const parseLambda = (stream) => {
if (debug) {
console.log("lambda");
console.log(stream);
}
let args = many(parseIdent)(stream);
let syn = parseSyntax("->")(args.stream);
if (syn.parsed === null) {
throw 'no lambda body found!';
}
let body = parseExprs(syn.stream); // .parsed should never be null, but anyway...
if (body.parsed === null) {
throw 'no lambda body found!';
}
return {parsed:{type:"func", args:args.parsed.arr.map(x => x.val), body:body.parsed.arr, pos:{start:args.parsed.pos.start, end:body.parsed.pos.end}}, stream:body.stream};
}
/* takes in stream, outputs parsed item or null */
const parseExpr = or(
attempt(parens(parseDefn)), or(
attempt(parens(parseLambda)), or(
attempt(parseLambda), or(
parseType, or(
parseIdent, or(
parseNum,
parsePush
))))));
/* takes in stream, outputs parsed items */
export const parseExprs = many(parseExpr);

View File

@@ -0,0 +1,158 @@
/*
EVAL.js
-------
reads exprs from AST and executes them
types of elem:
all have type
all have val
TYPE VAL
"closure" {scope:{}, args:['x', 'y'], defn:<function-like>}
<function-like>:
{type:"ins", ins:[]}
{type:"builtin", fn:(scope) => {stack}}
exported functionality:
defnOp(name, function) - define a built-in operator
defn(name, ins, scope) - extend scope with AST
makeOp(name, args, fn) - lift a function to an operator
evalRPN(scope, ast)
*/
let builtins = {};
export const defnOp = (name, data) => {
builtins[name] = data;
}
export const defn = (name, ins, scope) => {
let defscope = Object.create(scope);
let fnForm = {type:"closure", val:{scope:defscope, args:[], defn:{type:"ins", ins:ins}}};
defscope[name] = [fnForm];
let out = execRPN(defscope, ins);
defscope[name] = out.stacks[0];
return defscope;
}
export const makeOp = (args, fn) => {
return [{type:"closure", val:{scope:{}, args:args, defn:{type:"builtin", fn:fn}}}];
}
const lookup = (name, scope) => {
let n = scope[name];
if (n) {
return n;
}
n = builtins[name];
if (n) {
return n;
}
console.log(scope);
throw '"' + name + '" not in scope'
}
const extend = (scope, name, elems) => {
let o = Object.create(scope);
o[name] = elems;
return o;
}
const runFn = (defn, state) => {
if (defn.type === "ins") {
state.calls.push(defn.ins);
state.stacks.push([]);
} else if (defn.type === "builtin") {
let scope = state.scopes[state.scopes.length-1];
let out = defn.fn(scope);
state.calls.push([]);
state.stacks.push(out);
}
}
const giveArg = (arg, elem) => {
let argN = elem.val.args[elem.val.args.length-1];
let newscope = extend(elem.val.scope, argN, [arg]);
return {type:elem.type, val:{scope:newscope, args:elem.val.args.slice(0,-1), defn:elem.val.defn}};
}
const apply = (elem, state) => {
if (elem.type === "closure") {
if (elem.val.args.length === 0) {
state.scopes.push(elem.val.scope);
runFn(elem.val.defn, state);
} else if (state.stacks[state.stacks.length-1].length > 0) {
apply(giveArg(state.stacks[state.stacks.length-1].pop(), elem), state);
} else {
state.stacks[state.stacks.length-1].push(elem);
}
} else {
state.stacks[state.stacks.length-1].push(elem);
}
}
const applyMany = (elems, state) => {
for (let i = 0; i < elems.length; i++) {
apply(elems[i], state);
}
}
const makeStackElems = (ins, state, handler) => {
if (ins.type === "push") {
throw 'nested push error'
} else if (ins.type === "ident") {
return lookup(ins.val, state.scopes[state.scopes.length-1]);
} else if (ins.type === "func") {
return [{type:"closure", val:{scope:state.scopes[state.scopes.length-1], args:ins.args, defn:{type:"ins", ins:ins.body}}}];
} else {
return handler(ins);
}
}
const doIns = (ins, state, handler) => {
if (ins.type === "push") {
state.stacks[state.stacks.length-1] = state.stacks[state.stacks.length-1].concat(makeStackElems(ins.elem, state, handler));
} else if (ins.type === "defn") {
state.scopes[state.scopes.length-1] = defn(ins.ident, ins.defn, state.scopes[state.scopes.length-1]);
} else {
applyMany(makeStackElems(ins, state, handler), state);
}
}
export const step = (state, handler, showIns, maxdepth) => {
if (state.stacks.length > maxdepth) {
throw 'max recursion depth exceeded'
}
if (state.calls[state.calls.length-1].length === 0) {
if (state.calls.length === 1) {
throw 'finished execution'
}
if (state.stacks.length < 2) {
throw 'nothing to return'
}
state.calls.pop();
state.scopes.pop();
let out = state.stacks.pop();
applyMany(out, state);
return {start:0, end:0};
} else {
let ins = state.calls[state.calls.length-1][0];
state.calls[state.calls.length-1] = state.calls[state.calls.length-1].slice(1);
try {
doIns(ins, state, handler);
} catch (error) {
throw error + ' while executing "' + showIns(ins) + '"'
}
return ins.pos;
}
}
export const execRPN = (scope, ins, handler=(x)=>[x], showIns=(x)=>x.name, maxdepth=16384) => {
let state = {scopes:[scope], stacks:[[]], calls:[ins]};
while (state.calls[0].length > 0 || state.calls.length > 1) {
step(state, handler, showIns, maxdepth);
}
return state;
}

View File

@@ -0,0 +1,57 @@
const tokens = (stream) => {
let toks = [];
let currTok = {startPos:0, val:"", type:"str", endPos:0};
for (let i = 0; i < stream.length; i++) {
if ("()';\"".includes(stream[i])) {
if (currTok.val !== "") {
currTok.endPos = i;
toks.push(currTok);
}
toks.push({startPos:i, endPos:i+1, val:stream[i], type:"syntax"});
currTok = {startPos:i+1, val:"", type:"str"};
} else if (stream[i] === "-") {
if (stream[i+1] === ">") {
if (currTok.val !== "") {
currTok.endPos = i;
toks.push(currTok);
}
toks.push({startPos:i, endPos:i+1, val:"->", type:"syntax"});
i++;
currTok = {startPos:i+1, val:"", type:"str"};
} else {
currTok.val += "-";
}
} else if (/\s/.test(stream[i])) {
if (currTok.val !== "") {
currTok.endPos = i;
toks.push(currTok);
}
currTok = {startPos:i+1, val:"", type:"str"};
} else {
currTok.val += stream[i];
}
}
if (currTok.val !== "") {
currTok.endPos = stream.length;
toks.push(currTok);
}
return toks;
}
const classify = (tokens) => {
return tokens.map(tok => {
if (tok.type === "str") {
if (!isNaN(tok.val)) {
return {startPos:tok.startPos, endPos:tok.endPos, val:Number(tok.val), type:"num"};
} else {
return {startPos:tok.startPos, endPos:tok.endPos, name:tok.val, type:"ident"};
}
} else {
return tok;
}
});
}
export const tokenize = (stream) => {
return classify(tokens(stream));
}