diff --git a/experiments/apioform/index.html b/experiments/apioform/index.html
new file mode 100644
index 0000000..4784c41
--- /dev/null
+++ b/experiments/apioform/index.html
@@ -0,0 +1,686 @@
+---
+title: Apioform Game
+description: A game about... apioforms... by Heavpoot.
+---
+
+
Kills:
+
\ No newline at end of file
diff --git a/experiments/rpncalc-v4/builtin.mjs b/experiments/rpncalc-v4/builtin.mjs
new file mode 100644
index 0000000..0c5ca5a
--- /dev/null
+++ b/experiments/rpncalc-v4/builtin.mjs
@@ -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");
\ No newline at end of file
diff --git a/experiments/rpncalc-v4/index.html b/experiments/rpncalc-v4/index.html
new file mode 100644
index 0000000..39d9045
--- /dev/null
+++ b/experiments/rpncalc-v4/index.html
@@ -0,0 +1,48 @@
+---
+title: RPNCalc v4
+slug: rpncalc4
+description: Reverse Polish Notation calculator, version 4 - increasingly esoteric and incomprehensible. Contributed by Aidan.
+---
+
+
+
+
+
+
+
+
+
+
+
documentation
+
use (name; value) to define something. the definition can be recursive. value is executed and name is set to the final state of the stack, i.e. (name; 1 3) is possible
+
use ' to push instead of apply to the stack, e.g. '(a -> a). This is useful for lazyness, i.e. '(->lazy evaluated thing)
+
+
+, -, *, /, ^, sqrt: mathematical operations
+
==: equality (automatically derived for all types); returns a b -> a if true, a b -> b if false
+
typeof: returns the type of the object
+
pair, fst, snd: pairs two objects, gets first or second item of pair
+
tuple: used like ... 3 tuple; creates an n tuple of n items on the stack
+
!!: index into a tuple
+
len: length of a tuple
+
+
stdlib
+
+
stop; "stop
+
inv; x -> 1 x /
+
fold; x acc fn -> acc '(-> x acc fn 'fn fold) 'x \"stop ==
+
range; x y -> x '(->x x 1 + y range) 'x y ==
+
listthen; fn -> (internal; x acc -> '(->acc fn) '(->x acc pair internal) x stop ==) 0 tuple internal
+
list; (a -> a) listthen
+
lmap; list fn -> list '(->list fst fn list snd 'fn lmap pair) list 0 tuple ==
+
unlist; l -> (internal; list -> '(->) '(->list fst list snd internal) list 0 tuple ==) stop l internal
+
map; fn -> '(l->l 'fn lmap unlist) listthen
+
\ No newline at end of file
diff --git a/experiments/rpncalc-v4/main.js b/experiments/rpncalc-v4/main.js
new file mode 100644
index 0000000..57d2814
--- /dev/null
+++ b/experiments/rpncalc-v4/main.js
@@ -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) + `` + str.slice(start, end) + "" + 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 += " 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;
+}
\ No newline at end of file
diff --git a/experiments/rpncalc-v4/parse.mjs b/experiments/rpncalc-v4/parse.mjs
new file mode 100644
index 0000000..2d2ddfe
--- /dev/null
+++ b/experiments/rpncalc-v4/parse.mjs
@@ -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);
\ No newline at end of file
diff --git a/experiments/rpncalc-v4/shiny.mjs b/experiments/rpncalc-v4/shiny.mjs
new file mode 100644
index 0000000..fb5ae80
--- /dev/null
+++ b/experiments/rpncalc-v4/shiny.mjs
@@ -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:}
+
+:
+{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;
+}
\ No newline at end of file
diff --git a/experiments/rpncalc-v4/token.mjs b/experiments/rpncalc-v4/token.mjs
new file mode 100644
index 0000000..b1c0ad9
--- /dev/null
+++ b/experiments/rpncalc-v4/token.mjs
@@ -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));
+}
\ No newline at end of file
diff --git a/openring.html b/openring.html
index 388f2fc..83d9a70 100644
--- a/openring.html
+++ b/openring.html
@@ -2,9 +2,7 @@
{{range .Articles}}