haku: a very shitty tree-walk interpreter
This commit is contained in:
parent
d813675d47
commit
b505c1bcfe
8 changed files with 403 additions and 26 deletions
42
static/js/components/haku/treewalk.js
Normal file
42
static/js/components/haku/treewalk.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
export const treewalk = {};
|
||||
export const builtins = {};
|
||||
|
||||
treewalk.init = (input) => {
|
||||
return { input };
|
||||
};
|
||||
|
||||
treewalk.eval = (state, node) => {
|
||||
switch (node.kind) {
|
||||
case "integer":
|
||||
let sourceString = state.input.substring(node.start, node.end);
|
||||
return parseInt(sourceString);
|
||||
|
||||
case "list":
|
||||
let functionToCall = node.children[0];
|
||||
let builtin = builtins[state.input.substring(functionToCall.start, functionToCall.end)];
|
||||
return builtin(state, node);
|
||||
|
||||
default:
|
||||
throw new Error(`unhandled node kind: ${node.kind}`);
|
||||
}
|
||||
};
|
||||
|
||||
export function run(input, node) {
|
||||
let state = treewalk.init(input);
|
||||
return treewalk.eval(state, node);
|
||||
}
|
||||
|
||||
function arithmeticBuiltin(op) {
|
||||
return (state, node) => {
|
||||
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]));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
builtins["+"] = arithmeticBuiltin((a, b) => a + b);
|
||||
builtins["-"] = arithmeticBuiltin((a, b) => a - b);
|
||||
builtins["*"] = arithmeticBuiltin((a, b) => a * b);
|
||||
builtins["/"] = arithmeticBuiltin((a, b) => a / b);
|
|
@ -26,7 +26,12 @@ function getLiterateProgramWorkerCommands(name, count) {
|
|||
for (let i = 0; i < count; ++i) {
|
||||
let frame = literateProgram.frames[i];
|
||||
if (frame.mode == "input") {
|
||||
commands.push({ kind: "module", source: frame.textContent });
|
||||
commands.push({
|
||||
kind: "module",
|
||||
source: frame.textContent,
|
||||
language: frame.language,
|
||||
kernelParameters: frame.kernelAttributes,
|
||||
});
|
||||
} else if (frame.mode == "output") {
|
||||
commands.push({ kind: "output" });
|
||||
}
|
||||
|
@ -35,27 +40,42 @@ function getLiterateProgramWorkerCommands(name, count) {
|
|||
return commands;
|
||||
}
|
||||
|
||||
const javascriptJson = await (await fetch(`${TREEHOUSE_SITE}/static/syntax/javascript.json`)).text();
|
||||
let compiledSyntaxes = new Map();
|
||||
|
||||
async function getCompiledSyntax(language) {
|
||||
if (compiledSyntaxes.has(language)) {
|
||||
return compiledSyntaxes.get(language);
|
||||
} else {
|
||||
let json = await (await fetch(TREEHOUSE_SYNTAX_URLS[language])).text();
|
||||
let compiled = compileSyntax(JSON.parse(json));
|
||||
compiledSyntaxes.set(language, compiled);
|
||||
return compiled;
|
||||
}
|
||||
}
|
||||
|
||||
class InputMode {
|
||||
static JAVASCRIPT = compileSyntax(JSON.parse(javascriptJson));
|
||||
|
||||
constructor(frame) {
|
||||
this.frame = frame;
|
||||
|
||||
InputMode.highlight(frame);
|
||||
this.codeJar = CodeJar(frame, InputMode.highlight);
|
||||
getCompiledSyntax(this.frame.language).then((syntax) => {
|
||||
this.syntax = syntax;
|
||||
this.highlight();
|
||||
});
|
||||
|
||||
this.codeJar = CodeJar(frame, (frame) => this.highlight(frame));
|
||||
this.codeJar.onUpdate(() => {
|
||||
for (let handler of frame.program.onChanged) {
|
||||
handler(frame.programName);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
frame.addEventListener("click", event => event.preventDefault());
|
||||
frame.addEventListener("click", (event) => event.preventDefault());
|
||||
}
|
||||
|
||||
static highlight(frame) {
|
||||
highlight(frame, InputMode.JAVASCRIPT, (token, span) => {
|
||||
async highlight() {
|
||||
if (this.syntax == null) return;
|
||||
|
||||
highlight(this.frame, this.syntax, (token, span) => {
|
||||
if (token.kind == "keyword1" && token.string == "export") {
|
||||
// This is something a bit non-obvious about the treehouse's literate programs
|
||||
// so let's document it.
|
||||
|
@ -68,7 +88,7 @@ class InputMode {
|
|||
|
||||
function messageOutputArrayToString(output) {
|
||||
return output
|
||||
.map(x => {
|
||||
.map((x) => {
|
||||
if (typeof x === "object") return JSON.stringify(x);
|
||||
else return x + "";
|
||||
})
|
||||
|
@ -97,7 +117,7 @@ class OutputMode {
|
|||
|
||||
this.iframe.contentWindow.treehouseSandboxInternals = { outputIndex: this.outputIndex };
|
||||
|
||||
this.iframe.contentWindow.addEventListener("message", event => {
|
||||
this.iframe.contentWindow.addEventListener("message", (event) => {
|
||||
let message = event.data;
|
||||
if (message.kind == "ready") {
|
||||
this.evaluate();
|
||||
|
@ -121,14 +141,17 @@ class OutputMode {
|
|||
this.frame.placeholderImage.classList.add("loading");
|
||||
}
|
||||
|
||||
this.frame.program.onChanged.push(_ => this.evaluate());
|
||||
this.frame.program.onChanged.push((_) => this.evaluate());
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
this.requestConsoleClear();
|
||||
this.iframe.contentWindow.postMessage({
|
||||
action: "eval",
|
||||
input: getLiterateProgramWorkerCommands(this.frame.programName, this.frame.frameIndex + 1),
|
||||
input: getLiterateProgramWorkerCommands(
|
||||
this.frame.programName,
|
||||
this.frame.frameIndex + 1,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -161,7 +184,7 @@ class OutputMode {
|
|||
|
||||
// One day this will be more fancy. Today is not that day.
|
||||
line.textContent = output.message
|
||||
.map(x => {
|
||||
.map((x) => {
|
||||
if (typeof x === "object") return JSON.stringify(x);
|
||||
else return x + "";
|
||||
})
|
||||
|
@ -198,6 +221,7 @@ class OutputMode {
|
|||
|
||||
class LiterateProgram extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.language = this.getAttribute("data-language");
|
||||
this.programName = this.getAttribute("data-program");
|
||||
this.frameIndex = this.program.frames.length;
|
||||
this.program.frames.push(this);
|
||||
|
@ -205,6 +229,13 @@ class LiterateProgram extends HTMLElement {
|
|||
this.placeholderImage = this.getElementsByClassName("placeholder-image")[0];
|
||||
this.placeholderConsole = this.getElementsByClassName("placeholder-console")[0];
|
||||
|
||||
this.kernelAttributes = {};
|
||||
for (let name of this.getAttributeNames()) {
|
||||
if (name.startsWith("k-")) {
|
||||
this.kernelAttributes[name] = this.getAttribute(name);
|
||||
}
|
||||
}
|
||||
|
||||
this.mode = this.getAttribute("data-mode");
|
||||
if (this.mode == "input") {
|
||||
this.modeImpl = new InputMode(this);
|
||||
|
|
|
@ -14,9 +14,32 @@ export const domConsole = {
|
|||
},
|
||||
outputIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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 function getKernel() {
|
||||
return kernel;
|
||||
}
|
||||
|
||||
let evaluationComplete = null;
|
||||
|
||||
export async function evaluate(commands, { error, newOutput }) {
|
||||
|
@ -27,17 +50,20 @@ export async function evaluate(commands, { error, newOutput }) {
|
|||
let signalEvaluationComplete;
|
||||
evaluationComplete = new Promise((resolve, _reject) => {
|
||||
signalEvaluationComplete = resolve;
|
||||
})
|
||||
});
|
||||
|
||||
let kernelState = kernel.init();
|
||||
|
||||
outputIndex = 0;
|
||||
try {
|
||||
for (let command of commands) {
|
||||
if (command.kind == "module") {
|
||||
let blobUrl = URL.createObjectURL(new Blob([command.source], { type: "text/javascript" }));
|
||||
let module = await import(blobUrl);
|
||||
for (let exportedKey in module) {
|
||||
globalThis[exportedKey] = module[exportedKey];
|
||||
}
|
||||
await kernel.evalModule(
|
||||
kernelState,
|
||||
command.source,
|
||||
command.language,
|
||||
command.kernelParameters,
|
||||
);
|
||||
} else if (command.kind == "output") {
|
||||
if (newOutput != null) {
|
||||
newOutput(outputIndex);
|
||||
|
@ -63,4 +89,3 @@ export async function evaluate(commands, { error, newOutput }) {
|
|||
}
|
||||
signalEvaluationComplete();
|
||||
}
|
||||
|
||||
|
|
|
@ -42,8 +42,16 @@ function tokenize(text, syntax) {
|
|||
for (let i = 1; i < match.indices.length; ++i) {
|
||||
let [start, end] = match.indices[i];
|
||||
if (match.indices[i] != null) {
|
||||
pushToken(tokens, pattern.is.default, text.substring(lastMatchEnd, start));
|
||||
pushToken(tokens, pattern.is.captures[i], text.substring(start, end));
|
||||
pushToken(
|
||||
tokens,
|
||||
pattern.is.default,
|
||||
text.substring(lastMatchEnd, start),
|
||||
);
|
||||
pushToken(
|
||||
tokens,
|
||||
pattern.is.captures[i - 1],
|
||||
text.substring(start, end),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue