more on haku, highlighting fixes
This commit is contained in:
parent
e9ae153266
commit
e1fe9fde11
|
@ -1211,6 +1211,337 @@ scripts = ["treehouse/vendor/codejar.js", "treehouse/components/literate-program
|
||||||
13
|
13
|
||||||
```
|
```
|
||||||
|
|
||||||
|
% id = "01J3REN79KVAPHJGXYYG1MJQ7K"
|
||||||
|
- anyways, it's time to turn haku into a real programming language!
|
||||||
|
|
||||||
|
% id = "01J3REN79KQ0RXG4FBPCBBKPQT"
|
||||||
|
- programming languages as we use them in the real world are [_Turing-complete_](https://en.wikipedia.org/wiki/Turing_completeness) - roughly speaking, a language is Turing-complete if it can simulate a Turing machine.
|
||||||
|
|
||||||
|
% id = "01J3REN79KEN5TZ8101GNTSKWV"
|
||||||
|
- this is not an accurate definition at all - for that, I strongly suggest reading the appropriate Wikipedia articles.
|
||||||
|
|
||||||
|
% id = "01J3REN79K76X933BKT9NH3H4H"
|
||||||
|
- the TL;DR is that conditional loops are all you really need for Turing-completeness.
|
||||||
|
|
||||||
|
% id = "01J3REN79KD5XJ7CJ7E0GF7RMW"
|
||||||
|
- there exist two main models for modeling Turing-complete abstract machines: Turing machines, and lambda calculus.
|
||||||
|
|
||||||
|
% id = "01J3REN79KEKF91X5HR85N7WH4"
|
||||||
|
- Turing machines are the core of imperative programming languages - a Turing machine basically just models a state machine.
|
||||||
|
similar to what you may find in a modern processor.
|
||||||
|
|
||||||
|
% id = "01J3REN79KH9MBRGGTG5VV084V"
|
||||||
|
- lambda calculus on the other hand is a declarative system, a skinned down version of math if you will.
|
||||||
|
an expression in lambda calculus computes a result, and that's it.
|
||||||
|
no states, no side effects.
|
||||||
|
just like functional programming.
|
||||||
|
|
||||||
|
% id = "01J3REN79K7TQ2K9ZCV3FW7ZSJ"
|
||||||
|
- which is why we'll use it for haku!
|
||||||
|
|
||||||
|
% id = "01J3REN79KZYEQWCTM8A8QA5VE"
|
||||||
|
- at the core of lambda calculus is the _lambda_ - yes, that one from your favorite programming language!
|
||||||
|
there are a few operations we can do on lambdas.
|
||||||
|
|
||||||
|
% id = "01J3REN79KJKQ865P2205770AD"
|
||||||
|
- first of all, a lambda is a function which takes one argument, and produces one result - both of which can be other lambdas.
|
||||||
|
|
||||||
|
in haku, we will write down lambdas like so:
|
||||||
|
|
||||||
|
```haku
|
||||||
|
(fn (a) r)
|
||||||
|
```
|
||||||
|
|
||||||
|
where `a` is the name of the argument, and `r` is the resulting expression.
|
||||||
|
|
||||||
|
% id = "01J3REN79KCV615KP159KCD057"
|
||||||
|
- in fact, haku will extend this idea by permitting multiple arguments.
|
||||||
|
|
||||||
|
```haku
|
||||||
|
(fn (a b c) r)
|
||||||
|
```
|
||||||
|
|
||||||
|
% id = "01J3REN79KZCMGRW652JCMS25X"
|
||||||
|
- a lambda can be _applied_, which basically corresponds to a function call in your favorite programming language.
|
||||||
|
|
||||||
|
we write application down like so:
|
||||||
|
|
||||||
|
```haku
|
||||||
|
(f x)
|
||||||
|
```
|
||||||
|
|
||||||
|
where `f` is any expression producing a lambda, and `x` is the argument to pass to that lambda.
|
||||||
|
|
||||||
|
% id = "01J3REN79K9QEW18H7KJ9FTQ3A"
|
||||||
|
- what's also important is that nested lambdas capture their outer lambdas' arguments!
|
||||||
|
so the result of this:
|
||||||
|
|
||||||
|
```haku
|
||||||
|
(((fn (x) (fn (y) (+ x y))) 1) 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
is 3.
|
||||||
|
|
||||||
|
% id = "01J3REN79KFMM7TKTBDVY8YF8Y"
|
||||||
|
- this is by no means a formal explanation, just my intuition as to how it works.
|
||||||
|
formal definitions don't really matter for us anyways, since we're just curious little cats playing around with computer :cowboy:
|
||||||
|
|
||||||
|
% id = "01J3REN79KAFD4CVT56671TQFT"
|
||||||
|
- we'll start out with a way to define variables.
|
||||||
|
variables generally have _scope_ - look at the following JavaScript, for example:
|
||||||
|
|
||||||
|
{:program=scope-example}
|
||||||
|
```javascript
|
||||||
|
let x = 0;
|
||||||
|
console.log(x);
|
||||||
|
{
|
||||||
|
let x = 1;
|
||||||
|
console.log(x);
|
||||||
|
}
|
||||||
|
console.log(x);
|
||||||
|
```
|
||||||
|
|
||||||
|
{:program=scope-example}
|
||||||
|
```output
|
||||||
|
0
|
||||||
|
1
|
||||||
|
0
|
||||||
|
```
|
||||||
|
|
||||||
|
% id = "01J3REN79KXGFPQR5YZSKFVDRM"
|
||||||
|
- the same thing happens in haku (though we don't have a runtime for this yet, so you'll have to take my word for it.)
|
||||||
|
|
||||||
|
```haku
|
||||||
|
((fn (x)
|
||||||
|
((fn (x)
|
||||||
|
x)
|
||||||
|
2))
|
||||||
|
1)
|
||||||
|
```
|
||||||
|
|
||||||
|
this is perfectly fine, and the result should be 2 - not 1!
|
||||||
|
|
||||||
|
try evaluating this in your head, and you'll see what I mean.
|
||||||
|
it's better than me telling you all about it.
|
||||||
|
|
||||||
|
% id = "01J3REN79KQGCGPXVANVFYTXF7"
|
||||||
|
- so to represent scope, we'll introduce a new variable to our interpreter's state.
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```javascript
|
||||||
|
treewalk.init = (input) => {
|
||||||
|
return { input, scopes: [new Map(Object.entries(builtins))] };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`scopes` will be a stack of [`Map`][Map]s, each representing a single scope.
|
||||||
|
|
||||||
|
our builtins will now live at the bottom of all scopes, as an ever-present scope living in the background.
|
||||||
|
|
||||||
|
[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
|
||||||
|
|
||||||
|
% id = "01J3REN79KS7612NEDY8NCAQS5"
|
||||||
|
- variable lookup will be performed by walking the scope stack _from top to bottom_, until we find a variable that matches.
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```javascript
|
||||||
|
treewalk.lookupVariable = (state, name) => {
|
||||||
|
for (let i = state.scopes.length; i-- > 0; ) {
|
||||||
|
let scope = state.scopes[i];
|
||||||
|
if (scope.has(name)) {
|
||||||
|
return scope.get(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`variable ${name} is undefined`);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
we're stricter than JavaScript here and will error out on any variables that are not defined.
|
||||||
|
|
||||||
|
% id = "01J3REN79KZC6SWHJBGG8FK17S"
|
||||||
|
- now we can go ahead and add variable lookups to our `eval` function!
|
||||||
|
we'll also go ahead and replace our bodged-in builtin support with proper evaluation of the first list element.
|
||||||
|
|
||||||
|
in most cases, such as `(+ 1 2)`, this will result in a variable lookup.
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```javascript
|
||||||
|
treewalk.eval = (state, node) => {
|
||||||
|
switch (node.kind) {
|
||||||
|
case "integer":
|
||||||
|
let sourceString = state.input.substring(node.start, node.end);
|
||||||
|
return parseInt(sourceString);
|
||||||
|
|
||||||
|
case "identifier":
|
||||||
|
return treewalk.lookupVariable(state, state.input.substring(node.start, node.end));
|
||||||
|
|
||||||
|
case "list":
|
||||||
|
let functionToCall = treewalk.eval(state, node.children[0]);
|
||||||
|
return functionToCall(state, node);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`unhandled node kind: ${node.kind}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
% id = "01J3REN79K8HQX3XGSMJ039KPJ"
|
||||||
|
- if we didn't screw anything up, we should still be getting 13 here:
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```haku
|
||||||
|
(+ (* 2 1) 1 (/ 6 2) (- 10 3))
|
||||||
|
```
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```output
|
||||||
|
13
|
||||||
|
```
|
||||||
|
|
||||||
|
looks like all's working correctly!
|
||||||
|
|
||||||
|
% id = "01J3REN79KM4ZS66S5915E40E6"
|
||||||
|
- time to build our `fn` builtin.
|
||||||
|
|
||||||
|
we'll split the work into two functions: the actual builtin, which will parse the node's structure into some useful variables...
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```javascript
|
||||||
|
builtins.fn = (state, node) => {
|
||||||
|
if (node.children.length != 3)
|
||||||
|
throw new Error("an `fn` must have an argument list and a result expression");
|
||||||
|
|
||||||
|
let params = node.children[1];
|
||||||
|
if (node.children[1].kind != "list")
|
||||||
|
throw new Error("expected parameter list as second argument to `fn`");
|
||||||
|
|
||||||
|
let paramNames = [];
|
||||||
|
for (let param of params.children) {
|
||||||
|
if (param.kind != "identifier") {
|
||||||
|
throw new Error("`fn` parameters must be identifiers");
|
||||||
|
}
|
||||||
|
paramNames.push(state.input.substring(param.start, param.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
let expr = node.children[2];
|
||||||
|
|
||||||
|
return makeFunction(state, paramNames, expr);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
% id = "01J3REN79K43RXJ974CBCY5EFP"
|
||||||
|
- and `makeFunction`, which will take that data, and assemble it into a function that follows our `(state, node) => result` calling convention.
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```javascript
|
||||||
|
export function makeFunction(state, paramNames, bodyExpr) {
|
||||||
|
return (state, node) => {
|
||||||
|
if (node.children.length != paramNames.length + 1)
|
||||||
|
throw new Error(
|
||||||
|
`incorrect number of arguments: expected ${paramNames.length}, but got ${node.children.length - 1}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let scope = new Map();
|
||||||
|
for (let i = 0; i < paramNames.length; ++i) {
|
||||||
|
scope.set(paramNames[i], treewalk.eval(state, node.children[i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.scopes.push(scope);
|
||||||
|
let result = treewalk.eval(state, bodyExpr);
|
||||||
|
state.scopes.pop();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
% id = "01J3REN79KEPTVDDWMFZR16PWC"
|
||||||
|
- now let's try using that new `fn` builtin!
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```haku
|
||||||
|
((fn (a b)
|
||||||
|
(+ a b))
|
||||||
|
1 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```output
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
nice!
|
||||||
|
|
||||||
|
% id = "01J3REN79KWD313K6R91B33PBH"
|
||||||
|
- but, remember that lambdas are supposed to capture their outer variables! I wonder if that works.
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```haku
|
||||||
|
((fn (f)
|
||||||
|
((f 1) 2))
|
||||||
|
(fn (x)
|
||||||
|
(fn (y)
|
||||||
|
(+ x y))))
|
||||||
|
```
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```output
|
||||||
|
Error: variable x is undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
...I was being sarcastic here of course, of course it doesn't work. :ralsei_dead:
|
||||||
|
|
||||||
|
% id = "01J3REN79K62M94MSMRKVDAYGM"
|
||||||
|
- so to add support for that, we'll clone the entire scope stack into the closure, and then restore it when necessary.
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```javascript
|
||||||
|
export function makeFunction(state, paramNames, bodyExpr) {
|
||||||
|
let capturedScopes = [];
|
||||||
|
// Start from 1 to skip builtins, which are always present anyways.
|
||||||
|
for (let i = 1; i < state.scopes.length; ++i) {
|
||||||
|
// We don't really mutate the scopes after pushing them onto the stack, so keeping
|
||||||
|
// references to them is okay.
|
||||||
|
capturedScopes.push(state.scopes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (state, node) => {
|
||||||
|
if (node.children.length != paramNames.length + 1)
|
||||||
|
throw new Error(
|
||||||
|
`incorrect number of arguments: expected ${paramNames.length}, but got ${node.children.length - 1}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let scope = new Map();
|
||||||
|
for (let i = 0; i < paramNames.length; ++i) {
|
||||||
|
scope.set(paramNames[i], treewalk.eval(state, node.children[i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.scopes.push(...capturedScopes); // <--
|
||||||
|
state.scopes.push(scope);
|
||||||
|
let result = treewalk.eval(state, bodyExpr);
|
||||||
|
state.scopes.pop();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
with that, our program now works correctly:
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```haku
|
||||||
|
((fn (f)
|
||||||
|
((f 1) 2))
|
||||||
|
(fn (x)
|
||||||
|
(fn (y)
|
||||||
|
(+ x y))))
|
||||||
|
```
|
||||||
|
|
||||||
|
{:program=haku}
|
||||||
|
```output
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
% stage = "Draft"
|
% stage = "Draft"
|
||||||
id = "01J3K8A0D1D0NTT3JYYFMRYVSC"
|
id = "01J3K8A0D1D0NTT3JYYFMRYVSC"
|
||||||
- ### tests
|
- ### tests
|
||||||
|
@ -1249,7 +1580,13 @@ scripts = ["treehouse/vendor/codejar.js", "treehouse/components/literate-program
|
||||||
import { lex, parse, exprToString } from "haku/sexp.js";
|
import { lex, parse, exprToString } from "haku/sexp.js";
|
||||||
import { run } from "haku/treewalk.js";
|
import { run } from "haku/treewalk.js";
|
||||||
|
|
||||||
let input = "(+ (* 2 1) 1 (/ 6 2) (- 10 3))";
|
let input = `
|
||||||
|
((fn (f)
|
||||||
|
((f 1) 2))
|
||||||
|
(fn (x)
|
||||||
|
(fn (y)
|
||||||
|
(+ x y))))
|
||||||
|
`;
|
||||||
let tokens = lex(input);
|
let tokens = lex(input);
|
||||||
|
|
||||||
let ast = parse(tokens);
|
let ast = parse(tokens);
|
||||||
|
@ -1259,3 +1596,13 @@ scripts = ["treehouse/vendor/codejar.js", "treehouse/components/literate-program
|
||||||
{:program=test-treewalk}
|
{:program=test-treewalk}
|
||||||
```output
|
```output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
% stage = "Draft"
|
||||||
|
id = "01J3REN79K08JWA7FKQ94YTB5Y"
|
||||||
|
+ ### design notes to self
|
||||||
|
|
||||||
|
% id = "01J3REN79KT9MEFYAZ39WT49V3"
|
||||||
|
- if I ever get to the point where haku compiles and runs itself, the interpreter shall be called `haha`
|
||||||
|
|
||||||
|
% id = "01J3REN79KDGD50J9VBGVMV6AB"
|
||||||
|
- if I ever get to the point where haku compiles itself to wasm, the compiler should be called `wah`
|
||||||
|
|
|
@ -174,6 +174,12 @@ async fn sandbox(State(state): State<Arc<Server>>) -> Response {
|
||||||
.extensions_mut()
|
.extensions_mut()
|
||||||
.insert(live_reload::DisableLiveReload);
|
.insert(live_reload::DisableLiveReload);
|
||||||
}
|
}
|
||||||
|
// Debounce requests a bit. There's a tendency to have very many sandboxes on a page, and
|
||||||
|
// loading this page as many times as there are sandboxes doesn't seem like the best way to do
|
||||||
|
// things.
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(CACHE_CONTROL, HeaderValue::from_static("max-age=10"));
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,17 @@ export const treewalk = {};
|
||||||
export const builtins = {};
|
export const builtins = {};
|
||||||
|
|
||||||
treewalk.init = (input) => {
|
treewalk.init = (input) => {
|
||||||
return { input };
|
return { input, scopes: [new Map(Object.entries(builtins))] };
|
||||||
|
};
|
||||||
|
|
||||||
|
treewalk.lookupVariable = (state, name) => {
|
||||||
|
for (let i = state.scopes.length; i-- > 0; ) {
|
||||||
|
let scope = state.scopes[i];
|
||||||
|
if (scope.has(name)) {
|
||||||
|
return scope.get(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`variable ${name} is undefined`);
|
||||||
};
|
};
|
||||||
|
|
||||||
treewalk.eval = (state, node) => {
|
treewalk.eval = (state, node) => {
|
||||||
|
@ -11,10 +21,12 @@ treewalk.eval = (state, node) => {
|
||||||
let sourceString = state.input.substring(node.start, node.end);
|
let sourceString = state.input.substring(node.start, node.end);
|
||||||
return parseInt(sourceString);
|
return parseInt(sourceString);
|
||||||
|
|
||||||
|
case "identifier":
|
||||||
|
return treewalk.lookupVariable(state, state.input.substring(node.start, node.end));
|
||||||
|
|
||||||
case "list":
|
case "list":
|
||||||
let functionToCall = node.children[0];
|
let functionToCall = treewalk.eval(state, node.children[0]);
|
||||||
let builtin = builtins[state.input.substring(functionToCall.start, functionToCall.end)];
|
return functionToCall(state, node);
|
||||||
return builtin(state, node);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`unhandled node kind: ${node.kind}`);
|
throw new Error(`unhandled node kind: ${node.kind}`);
|
||||||
|
@ -40,3 +52,53 @@ 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["*"] = arithmeticBuiltin((a, b) => a * b);
|
||||||
builtins["/"] = arithmeticBuiltin((a, b) => a / b);
|
builtins["/"] = arithmeticBuiltin((a, b) => a / b);
|
||||||
|
|
||||||
|
export function makeFunction(state, paramNames, bodyExpr) {
|
||||||
|
let capturedScopes = [];
|
||||||
|
// Start from 1 to skip builtins, which are always present anyways.
|
||||||
|
for (let i = 1; i < state.scopes.length; ++i) {
|
||||||
|
// We don't really mutate the scopes after pushing them onto the stack, so keeping
|
||||||
|
// references to them is okay.
|
||||||
|
capturedScopes.push(state.scopes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (state, node) => {
|
||||||
|
if (node.children.length != paramNames.length + 1)
|
||||||
|
throw new Error(
|
||||||
|
`incorrect number of arguments: expected ${paramNames.length}, but got ${node.children.length - 1}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let scope = new Map();
|
||||||
|
for (let i = 0; i < paramNames.length; ++i) {
|
||||||
|
scope.set(paramNames[i], treewalk.eval(state, node.children[i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.scopes.push(...capturedScopes);
|
||||||
|
state.scopes.push(scope);
|
||||||
|
let result = treewalk.eval(state, bodyExpr);
|
||||||
|
state.scopes.pop();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
builtins.fn = (state, node) => {
|
||||||
|
if (node.children.length != 3)
|
||||||
|
throw new Error("an `fn` must have an argument list and a result expression");
|
||||||
|
|
||||||
|
let params = node.children[1];
|
||||||
|
if (node.children[1].kind != "list")
|
||||||
|
throw new Error("expected parameter list as second argument to `fn`");
|
||||||
|
|
||||||
|
let paramNames = [];
|
||||||
|
for (let param of params.children) {
|
||||||
|
if (param.kind != "identifier") {
|
||||||
|
throw new Error("`fn` parameters must be identifiers");
|
||||||
|
}
|
||||||
|
paramNames.push(state.input.substring(param.start, param.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
let expr = node.children[2];
|
||||||
|
|
||||||
|
return makeFunction(state, paramNames, expr);
|
||||||
|
};
|
||||||
|
|
|
@ -79,7 +79,9 @@ export async function evaluate(commands, { error, newOutput }) {
|
||||||
kind: "output",
|
kind: "output",
|
||||||
output: {
|
output: {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
message: [err.toString()],
|
message: [
|
||||||
|
err.stack.length > 0 ? err.toString() + "\n\n" + err.stack : err.toString(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
outputIndex,
|
outputIndex,
|
||||||
});
|
});
|
||||||
|
|
|
@ -53,7 +53,13 @@ function tokenize(text, syntax) {
|
||||||
text.substring(start, end),
|
text.substring(start, end),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
lastMatchEnd = end;
|
||||||
}
|
}
|
||||||
|
pushToken(
|
||||||
|
tokens,
|
||||||
|
pattern.is.default,
|
||||||
|
text.substring(lastMatchEnd, match.indices[0][1]),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
pushToken(tokens, pattern.is, match[0]);
|
pushToken(tokens, pattern.is, match[0]);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue