diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..80eaf39 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,3 @@ +aidan-network.duckdns.org { + file_server +} \ No newline at end of file diff --git a/eval.js b/eval.mjs similarity index 70% rename from eval.js rename to eval.mjs index 5a75bb7..39f4afc 100644 --- a/eval.js +++ b/eval.mjs @@ -64,22 +64,36 @@ const pair = (_, args) => { return [{type:"pair", val:{fst:args[0], snd:args[1]}}]; } -const fst = (args) => [args[0].fst]; +const fst = (_, args) => [args[0].fst]; -const snd = (args) => [args[0].snd]; +const snd = (_, args) => [args[0].snd]; + +const tru = (_, args) => [args[0]]; +const fls = (_, args) => [args[1]]; + +const eq = (_, args) => { + if (args[0].type === args[1].type && args[0].val === args[1].val) { + return [{type:"ident", val:"true"}]; + } else { + return [{type:"ident", val:"false"}]; + } +} let builtinDefn = { "+": makeEval(["int", "int"], add), "-": makeEval(["int", "int"], sub), "/": makeEval(["int", "int"], div), "*": makeEval(["int", "int"], mult), + "true": makeEval(2, tru), + "false": makeEval(2, fls), + "==": makeEval(2, eq), "typeof": makeEval(1, type), "pair": makeEval(2, pair), "fst": makeEval(["pair"], fst), "snd": makeEval(["pair"], snd) } -const addDefn = (name, sign, func) => { +export const addDefn = (name, sign, func) => { builtinDefn[name] = makeEval(sign, func); } @@ -94,16 +108,38 @@ const makeLambda = (lambda) => { } const makeObj = (elem) => { - if (elem.type === "builtin") { - let fn = builtinDefn[elem.op]; - return {type:"closure", args:[], func:fn}; - } else if (elem.type === "func") { + if (elem.type === "func") { return {type:"closure", args:[], func:makeLambda(elem)}; } else { return elem; } } +const cloneElem = (elem) => { + if (elem.type === "closure") { + let argsClone = []; + for (let i = 0; i < elem.args.length; i++) { + argsClone.push(cloneElem(elem.args[i])); + } + return {type:"closure", args:argsClone, func:elem.func}; + } else { + return elem; + } +} + +const lookupScope = (name, scope) => { + let n = scope[name]; + if (n) { + return cloneElem(n); + } + n = builtinDefn[name]; + if (n) { + return {type:"closure", args:[], func:n}; + } else { + throw "var " + n + " not in scope" + } +} + const giveArg = (closure, arg, scope) => { closure.args.push(arg); if (closure.args.length === closure.func.nargs) { @@ -115,10 +151,14 @@ const giveArg = (closure, arg, scope) => { const apply = (elem, stack) => { if (elem.type === "closure") { - let out = giveArg(elem, stack.stack.pop(), stack.scope); - applyMany(out, stack); + if (stack.stack.length > 0) { + let out = giveArg(elem, stack.stack.pop(), stack.scope); + applyMany(out, stack); + } else { + stack.stack.push(elem); + } } else if (elem.type === "ident") { - let id = stack.scope[elem.val]; + let id = lookupScope(elem.val, stack.scope); apply(id, stack); } else { stack.stack.push(elem); @@ -133,7 +173,7 @@ const applyMany = (outstack, stack) => { const pushS = (elem, stack) => { if (elem.type === "ident") { - let id = stack.scope[elem.name]; + let id = lookupScope(elem.val, stack.scope); stack.stack.push(id); } else { stack.stack.push(elem); @@ -144,7 +184,7 @@ const defn = (elem, name, stack) => { stack.scope[name] = makeObj(elem); } -const doStep = (ins, stack) => { +export const doStep = (ins, stack) => { let instruction = ins.shift(); if (instruction.type === "push") { pushS(makeObj(instruction.elem), stack); @@ -155,7 +195,8 @@ const doStep = (ins, stack) => { } } -const execRPN = (scope, ins) => { +export const execRPN = (scope, i) => { + let ins = JSON.parse(JSON.stringify(i)); let stack = {scope:scope, stack:[]}; while (ins.length > 0) { doStep(ins, stack); diff --git a/index.html b/index.html new file mode 100644 index 0000000..71caa88 --- /dev/null +++ b/index.html @@ -0,0 +1,34 @@ + +

RPNCalc V4 Public Testing

+

@ me (aidanprattewart@protonmail.com) if you have any errors, with console output please.

+

+Example Programs: +
+(swap; a b -> a b) 1 2 swap N.B. args are in reverse order +
+1 + 2 (v partial -> 'v partial) Evaluate a partial +

+ + +

+ +

+

Docs

+

Lambda/Function Syntax

+named function (added to scope, not applied): (swap; a b -> b a) +
+can be called later like: 1 2 swap +
+unnamed function, applied immediately: 1 2 3 (a b c -> a b c) +
+when lambdas return, they return entire stacks, which are sequentially applied back onto the stack +

Partial Application

+1 + will push a partially-applied function onto the stack +
+this could be called with: 1 + (partial -> 2 partial) +

Pairs

+pairs of 2 values can be created with pair +
+they can be deconstructed using fst or snd +

+ \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..04d5582 --- /dev/null +++ b/main.js @@ -0,0 +1,50 @@ +import {execRPN} from './eval.mjs'; +import {parseExprs} from './parse.mjs'; +import {tokenize} from './token.mjs'; + +const inbox = document.getElementById("inbox") +const outbox = document.getElementById("outbox") +const submit = document.getElementById("submit") + +const show = (elem) => { + if (elem.type === "int") { + return elem.val + } else if (elem.type === "pair") { + return "{" + show(elem.val.fst) + ", " + show(elem.val.snd) + "}" + } else if (elem.type === "closure") { + return "(args: {" + prettyprint(elem.args) + "} of " + elem.func.nargs + ")" + } else if (elem.type === "string") { + return elem.val + } +} + +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; +} + +submit.onclick = (event) => { + const input = inbox.value; + let toks = tokenize(input); + if (!toks) { + outbox.innerHTML = "could not parse input: " + input; + return; + } + let ast = parseExprs(toks); + if (!ast.parsed) { + outbox.innerHTML = "incorrect syntax somewhere"; + return; + } + let out = execRPN({}, ast.parsed); + if (!out) { + outbox.innerHTML = "failed to execute"; + return; + } + outbox.innerHTML = prettyprint(out.stack); +} \ No newline at end of file diff --git a/parse.js b/parse.mjs similarity index 86% rename from parse.js rename to parse.mjs index 1a039ce..97b9196 100644 --- a/parse.js +++ b/parse.mjs @@ -41,17 +41,6 @@ EXPORTED FUNCTIONS: parseExprs - takes in tokenstream, outputs AST */ -const builtin = [ - "+", - "-", - "*", - "/", - "typeof", - "pair", - "fst", - "snd" -]; - /* make parser safe */ const attempt = (parser) => (stream) => { let streamclone = [...stream]; @@ -98,23 +87,6 @@ const many = (parser) => (stream) => { return {parsed:parsed, stream:stream}; } -/* takes in stream, outputs {parsed, stream} */ -const parseBuiltin = (stream) => { - let e = stream[0]; - if (e === undefined) { - return {parsed:null, stream:stream}; - } - if (e.type !== "ident") { - return {parsed:null, stream:stream}; - } - if (builtin.includes(e.name)) { - stream.shift(); - return {parsed:{type:"builtin", val:e.name}, stream:stream}; - } else { - return {parsed:null, stream:stream}; - } -} - /* takes in stream, outputs parsed item or null */ const parseIdent = (stream) => { let e = stream[0]; @@ -173,10 +145,24 @@ const parseName = (stream) => { return {parsed:id.parsed.val, stream:syn.stream}; } +const parseString = (stream) => { + + let syn = attempt(parseSyntax("\""))(stream); + if (syn.parsed === null) { + return {parsed:null, stream:syn.stream}; + } + let id = parseIdent(syn.stream); + if (id.parsed === null) { + return {parsed:null, stream:id.stream}; + } + return {parsed:{type:"string", val:id.parsed.val}, stream:id.stream}; + + //return {parsed:null, stream:stream}; +} + /* takes in stream, outputs parsed item or null - FAILABLE */ const parsePush = (stream) => { let syn = attempt(parseSyntax("'"))(stream); - console.log(syn); if (syn.parsed === null) { return {parsed:null, stream:syn.stream}; } @@ -203,12 +189,12 @@ const parseLambda = (stream) => { if (name.parsed === null) { return {parsed:func, stream:body.stream}; } else { - return {type:"defn", ident:name.parsed, defn:func}; + return {parsed:{type:"defn", ident:name.parsed, defn:func}, stream:body.stream}; } } /* takes in stream, outputs parsed item or null */ -const parseExpr = or(parseBuiltin, or(parseIdent, or(parseInteger, or(parsePush, attempt(parens(parseLambda)))))); +const parseExpr = or(parseString, or(parseIdent, or(parseInteger, or(parsePush, attempt(parens(parseLambda)))))); /* takes in stream, outputs parsed items */ export const parseExprs = many(parseExpr); \ No newline at end of file diff --git a/token.js b/token.mjs similarity index 68% rename from token.js rename to token.mjs index 80914bb..7072776 100644 --- a/token.js +++ b/token.mjs @@ -1,7 +1,7 @@ -function tokenize(input) { - var i; - var inputWithAddedSpaces = ""; - const syntax = /[`;()]/; // -> is a special syntax (because it uses 2 characters) so is coded separately +export function tokenize(input) { + let i; + let inputWithAddedSpaces = ""; + const syntax = /['";()]/; // -> is a special syntax (because it uses 2 characters) so is coded separately for(i = 0; i + if(/^\d+$/.test(splitInput[i])){ // didn't need /[0-9]/, but that would be helpful to stop numbers from being in identifiers, but the ability to call a function "function_add_1" is easier to type than "function_add_one" + output.push({type: "int", val:parseInt(splitInput[i])}); // also, /[a-zA-Z]/ wasn't necessary as my code uses anything that isn't `;() or -> } else if(syntax.test(splitInput[i]) || splitInput[i] === "->"){// needs a || as -> wasn't included in the syntax regexp. it wasn't in because -> uses two characters so i wanted to have separate code for it. (also because regexps are confusing) output.push({type: "syntax", val:splitInput[i]}); } else if(splitInput[i] != '') { // if syntax is next to the end of the string or other bits of syntax the two spaces are in inputWithAddedSpaces so .split returns '' as one element. this makes sure that it is not read as an identifier