haku continued
This commit is contained in:
parent
e1fe9fde11
commit
5ac11b261b
4 changed files with 826 additions and 52 deletions
|
@ -10,9 +10,7 @@ lexer.init = (input) => {
|
|||
export const eof = "end of file";
|
||||
|
||||
lexer.current = (state) => {
|
||||
return state.position < state.input.length
|
||||
? state.input.charAt(state.position)
|
||||
: eof;
|
||||
return state.position < state.input.length ? state.input.charAt(state.position) : eof;
|
||||
};
|
||||
|
||||
lexer.advance = (state) => ++state.position;
|
||||
|
@ -31,10 +29,7 @@ lexer.skipWhitespaceAndComments = (state) => {
|
|||
continue;
|
||||
}
|
||||
if (c == ";") {
|
||||
while (
|
||||
lexer.current(state) != "\n" &&
|
||||
lexer.current(state) != eof
|
||||
) {
|
||||
while (lexer.current(state) != "\n" && lexer.current(state) != eof) {
|
||||
lexer.advance(state);
|
||||
}
|
||||
lexer.advance(state); // skip over newline, too
|
||||
|
@ -46,8 +41,7 @@ lexer.skipWhitespaceAndComments = (state) => {
|
|||
};
|
||||
|
||||
export const isDigit = (c) => c >= "0" && c <= "9";
|
||||
export const isIdentifier = (c) =>
|
||||
/^[a-zA-Z0-9+~!@$%^&*=<>+?/.,:\\|-]$/.test(c);
|
||||
export const isIdentifier = (c) => /^[a-zA-Z0-9+~!@$%^&*=<>+?/.,:\\|-]$/.test(c);
|
||||
|
||||
lexer.nextToken = (state) => {
|
||||
let c = lexer.current(state);
|
||||
|
@ -151,7 +145,19 @@ parser.parseList = (state, leftParen) => {
|
|||
};
|
||||
};
|
||||
|
||||
parser.parseRoot = parser.parseExpr;
|
||||
parser.parseToplevel = (state) => {
|
||||
let children = [];
|
||||
while (parser.current(state).kind != eof) {
|
||||
children.push(parser.parseExpr(state));
|
||||
}
|
||||
return {
|
||||
kind: "toplevel",
|
||||
children,
|
||||
// Don't bother with start..end for now.
|
||||
};
|
||||
};
|
||||
|
||||
parser.parseRoot = (state) => parser.parseToplevel(state);
|
||||
|
||||
export function parse(input) {
|
||||
let state = parser.init(input);
|
||||
|
@ -184,3 +190,15 @@ export function exprToString(expr, input) {
|
|||
return `<error ${expr.start}..${expr.end} '${inputSubstring}': ${expr.error}>`;
|
||||
}
|
||||
}
|
||||
|
||||
export function insertSources(node, input) {
|
||||
if (node.start != null) {
|
||||
node.source = input.substring(node.start, node.end);
|
||||
}
|
||||
|
||||
if (node.children != null) {
|
||||
for (let child of node.children) {
|
||||
insertSources(child, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
export const treewalk = {};
|
||||
export const builtins = {};
|
||||
|
||||
treewalk.init = (input) => {
|
||||
return { input, scopes: [new Map(Object.entries(builtins))] };
|
||||
treewalk.init = (env, input) => {
|
||||
return {
|
||||
input,
|
||||
scopes: [new Map(Object.entries(builtins)), env],
|
||||
env,
|
||||
};
|
||||
};
|
||||
|
||||
treewalk.lookupVariable = (state, name) => {
|
||||
|
@ -12,34 +16,46 @@ treewalk.lookupVariable = (state, name) => {
|
|||
return scope.get(name);
|
||||
}
|
||||
}
|
||||
console.log(new Error().stack);
|
||||
throw new Error(`variable ${name} is undefined`);
|
||||
};
|
||||
|
||||
treewalk.eval = (state, node) => {
|
||||
switch (node.kind) {
|
||||
case "integer":
|
||||
let sourceString = state.input.substring(node.start, node.end);
|
||||
return parseInt(sourceString);
|
||||
return parseInt(node.source);
|
||||
|
||||
case "identifier":
|
||||
return treewalk.lookupVariable(state, state.input.substring(node.start, node.end));
|
||||
return treewalk.lookupVariable(state, node.source);
|
||||
|
||||
case "list":
|
||||
let functionToCall = treewalk.eval(state, node.children[0]);
|
||||
return functionToCall(state, node);
|
||||
|
||||
case "toplevel":
|
||||
let result = undefined;
|
||||
for (let i = 0; i < node.children.length; ++i) {
|
||||
result = treewalk.eval(state, node.children[i]);
|
||||
if (result !== undefined && i != node.children.length - 1)
|
||||
throw new Error(`expression ${i + 1} had a result despite not being the last`);
|
||||
}
|
||||
return result;
|
||||
|
||||
default:
|
||||
throw new Error(`unhandled node kind: ${node.kind}`);
|
||||
}
|
||||
};
|
||||
|
||||
export function run(input, node) {
|
||||
let state = treewalk.init(input);
|
||||
export function run(env, input, node) {
|
||||
let state = treewalk.init(env, input);
|
||||
return treewalk.eval(state, node);
|
||||
}
|
||||
|
||||
function arithmeticBuiltin(op) {
|
||||
return (state, node) => {
|
||||
if (node.children.length < 3)
|
||||
throw new Error("arithmetic operations require at least two arguments");
|
||||
|
||||
let result = treewalk.eval(state, node.children[1]);
|
||||
for (let i = 2; i < node.children.length; ++i) {
|
||||
result = op(result, treewalk.eval(state, node.children[i]));
|
||||
|
@ -48,11 +64,25 @@ function arithmeticBuiltin(op) {
|
|||
};
|
||||
}
|
||||
|
||||
function comparisonBuiltin(op) {
|
||||
return (state, node) => {
|
||||
if (node.children.length != 3)
|
||||
throw new Error("comparison operators require exactly two arguments");
|
||||
|
||||
let a = treewalk.eval(state, node.children[1]);
|
||||
let b = treewalk.eval(state, node.children[2]);
|
||||
return op(a, b) ? 1 : 0;
|
||||
};
|
||||
}
|
||||
|
||||
builtins["+"] = arithmeticBuiltin((a, b) => a + b);
|
||||
builtins["-"] = arithmeticBuiltin((a, b) => a - b);
|
||||
builtins["*"] = arithmeticBuiltin((a, b) => a * b);
|
||||
builtins["/"] = arithmeticBuiltin((a, b) => a / b);
|
||||
|
||||
builtins["="] = comparisonBuiltin((a, b) => a === b);
|
||||
builtins["<"] = comparisonBuiltin((a, b) => a < b);
|
||||
|
||||
export function makeFunction(state, paramNames, bodyExpr) {
|
||||
let capturedScopes = [];
|
||||
// Start from 1 to skip builtins, which are always present anyways.
|
||||
|
@ -95,10 +125,36 @@ builtins.fn = (state, node) => {
|
|||
if (param.kind != "identifier") {
|
||||
throw new Error("`fn` parameters must be identifiers");
|
||||
}
|
||||
paramNames.push(state.input.substring(param.start, param.end));
|
||||
paramNames.push(param.source);
|
||||
}
|
||||
|
||||
let expr = node.children[2];
|
||||
|
||||
return makeFunction(state, paramNames, expr);
|
||||
};
|
||||
|
||||
builtins["if"] = (state, node) => {
|
||||
if (node.children.length != 4)
|
||||
throw new Error("an `if` must have a condition, true expression, and false expression");
|
||||
|
||||
let condition = treewalk.eval(state, node.children[1]);
|
||||
if (condition !== 0) {
|
||||
return treewalk.eval(state, node.children[2]);
|
||||
} else {
|
||||
return treewalk.eval(state, node.children[3]);
|
||||
}
|
||||
};
|
||||
|
||||
builtins.def = (state, node) => {
|
||||
if (node.children.length != 3)
|
||||
throw new Error(
|
||||
"a `def` expects the name of the variable to assign, and the value to assign to the variable",
|
||||
);
|
||||
|
||||
if (node.children[1].kind != "identifier")
|
||||
throw new Error("variable name must be an identifier");
|
||||
|
||||
let name = node.children[1];
|
||||
let value = treewalk.eval(state, node.children[2]);
|
||||
state.env.set(name.source, value);
|
||||
};
|
||||
|
|
|
@ -2,6 +2,13 @@ let outputIndex = 0;
|
|||
|
||||
export const jsConsole = console;
|
||||
|
||||
const loggingEnabled = false;
|
||||
function log(...message) {
|
||||
if (loggingEnabled) {
|
||||
jsConsole.log("[eval]", ...message);
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite globalThis.console with domConsole to redirect output to the DOM console.
|
||||
// To always output to the JavaScript console regardless, use jsConsole.
|
||||
export const domConsole = {
|
||||
|
@ -17,23 +24,21 @@ export const domConsole = {
|
|||
},
|
||||
};
|
||||
|
||||
let kernel = {
|
||||
init() {
|
||||
return {};
|
||||
},
|
||||
|
||||
async evalModule(_state, source, language, _params) {
|
||||
if (language == "javascript") {
|
||||
let blobUrl = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
|
||||
let module = await import(blobUrl);
|
||||
for (let exportedKey in module) {
|
||||
globalThis[exportedKey] = module[exportedKey];
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
export async function defaultEvalModule(_state, source, language, _params) {
|
||||
if (language == "javascript") {
|
||||
let blobUrl = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
|
||||
let module = await import(blobUrl);
|
||||
for (let exportedKey in module) {
|
||||
globalThis[exportedKey] = module[exportedKey];
|
||||
}
|
||||
},
|
||||
return _state;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let kernel = {
|
||||
evalModule: defaultEvalModule,
|
||||
};
|
||||
|
||||
export function getKernel() {
|
||||
|
@ -52,11 +57,11 @@ export async function evaluate(commands, { error, newOutput }) {
|
|||
signalEvaluationComplete = resolve;
|
||||
});
|
||||
|
||||
let kernelState = kernel.init();
|
||||
|
||||
outputIndex = 0;
|
||||
try {
|
||||
let kernelState = {};
|
||||
for (let command of commands) {
|
||||
log(`frame ${treehouseSandboxInternals.outputIndex} module`, command);
|
||||
if (command.kind == "module") {
|
||||
await kernel.evalModule(
|
||||
kernelState,
|
||||
|
@ -71,10 +76,12 @@ export async function evaluate(commands, { error, newOutput }) {
|
|||
++outputIndex;
|
||||
}
|
||||
}
|
||||
log(`frame ${treehouseSandboxInternals.outputIndex} evalComplete`);
|
||||
postMessage({
|
||||
kind: "evalComplete",
|
||||
});
|
||||
} catch (err) {
|
||||
log(`frame ${treehouseSandboxInternals.outputIndex} error`, err);
|
||||
postMessage({
|
||||
kind: "output",
|
||||
output: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue