Letting us redefine functions, reactively

master
Dustin Swan 3 weeks ago
parent 33f3b2cfc2
commit 60c8f74d50
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -1,6 +1,10 @@
import type { AST, Pattern, Definition } from './ast';
import { _rt, store } from './runtime-js';
const definitions: Map<string, AST> = new Map();
const dependencies: Map<string, Set<string>> = new Map();
const dependents: Map<string, Set<string>> = new Map();
export function compile(ast: AST): string {
switch (ast.kind) {
case 'literal':
@ -15,7 +19,7 @@ export function compile(ast: AST): string {
case 'lambda':
const params = ast.params.map(sanitize).join(') => (');
return `((${params}) => ${compile(ast.body)})`;
return `Object.assign((${params}) => ${compile(ast.body)}, { _ast: (${JSON.stringify(ast)}) })`;
case 'apply':
// Constructor
@ -99,7 +103,7 @@ function sanitize(name: string): string {
if (ops[name]) return ops[name];
const natives = ['measure', 'measureText', 'storeSearch', 'debug', 'len', 'slice', 'str'];
const natives = ['measure', 'measureText', 'storeSearch', 'debug', 'len', 'slice', 'str', 'redefine'];
if (natives.includes(name)) return `_rt.${name}`;
const reserved = [
@ -201,6 +205,27 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi
export function compileAndRun(defs: Definition[]) {
const compiledDefs: string[] = [];
const topLevel = new Set(defs.map(d => d.name));
for (const def of defs) {
definitions.set(def.name, def.body);
const free = freeVars(def.body);
const deps = new Set([...free].filter(v => topLevel.has(v)));
dependencies.set(def.name, deps);
}
dependents.clear();
for (const name of topLevel) {
dependents.set(name, new Set());
}
for (const [name, deps] of dependencies) {
for (const dep of deps) {
dependents.get(dep)?.add(name);
}
}
for (const def of defs) {
const compiled = `const ${sanitize(def.name)} = ${compile(def.body)};`;
compiledDefs.push(compiled);
@ -232,3 +257,125 @@ return { ${defNames}, __result: ${sanitize(lastName)} };`;
return allDefs.__result;
}
function freeVars(ast: AST, bound: Set<string> = new Set()): Set<string> {
switch (ast.kind) {
case 'literal':
case 'constructor':
return new Set();
case 'variable':
return bound.has(ast.name) ? new Set() : new Set([ast.name]);
case 'lambda': {
const newBound = new Set([...bound, ...ast.params]);
return freeVars(ast.body, newBound);
}
case 'let': {
const valueVars = freeVars(ast.value, bound);
const bodyVars = freeVars(ast.body, new Set([...bound, ast.name]));
return new Set([...valueVars, ...bodyVars]);
}
case 'apply': {
const funcVars = freeVars(ast.func, bound);
const argVars = ast.args.flatMap(a => [...freeVars(a, bound)])
return new Set([...funcVars, ...argVars]);
}
case 'record': {
const allVars = Object.values(ast.fields).flatMap(v => [...freeVars(v, bound)]);
return new Set(allVars);
}
case 'list': {
const allVars = ast.elements.flatMap(e =>
'spread' in e ? [...freeVars(e.spread, bound)]
: [...freeVars(e, bound)]);
return new Set(allVars);
}
case 'record-access':
return freeVars(ast.record, bound);
case 'record-update': {
const recordVars = freeVars(ast.record, bound);
const updateVars = Object.values(ast.updates).flatMap(v => [...freeVars(v, bound)]);
return new Set([...recordVars, ...updateVars]);
}
case 'match': {
const exprVars = freeVars(ast.expr, bound);
const caseVars = ast.cases.flatMap(c => {
const patternBindings = patternVars(c.pattern);
const newBound = new Set([...bound, ...patternBindings]);
return [...freeVars(c.result, newBound)];
})
return new Set([...exprVars, ...caseVars]);
}
case 'rebind':
return freeVars(ast.value, bound);
default:
return new Set();
}
}
function patternVars(pattern: Pattern): string[] {
switch (pattern.kind) {
case 'var':
return [pattern.name];
case 'constructor':
return pattern.args.flatMap(patternVars);
case 'list':
return pattern.elements.flatMap(patternVars);
case 'list-spread':
return [...pattern.head.flatMap(patternVars), pattern.spread];
case 'record':
return Object.values(pattern.fields).flatMap(patternVars);
default:
return [];
}
}
export function recompile(name: string, newAst: AST) {
definitions.set(name, newAst);
const topLevel = new Set(definitions.keys());
const free = freeVars(newAst);
const newDeps = new Set([...free].filter(v => topLevel.has(v)));
const oldDeps = dependencies.get(name) || new Set();
for (const dep of oldDeps) {
dependents.get(dep)?.delete(name);
}
for (const dep of newDeps) {
dependents.get(dep)?.add(name);
}
dependencies.set(name, newDeps);
const toRecompile: string[] = [];
const visited = new Set<string>();
function collectDependents(n: string) {
if (visited.has(n)) return;
visited.add(n);
toRecompile.push(n);
for (const dep of dependents.get(n) || []) {
collectDependents(dep);
}
}
collectDependents(name);
for (const defName of toRecompile) {
const ast = definitions.get(defName)!;
const compiled = compile(ast);
const fn = new Function('_rt', 'store', `return ${compiled}`);
store[defName] = fn(_rt, store);
}
}

@ -22,6 +22,7 @@ try {
const parser = new Parser(tokens, cgCode);
const defs = parser.parse();
const os = compileAndRun(defs);
runAppCompiled(
{ init: os.init, update: os.update, view: os.view },
canvas,

@ -1,3 +1,7 @@
import { tokenize } from './lexer'
import { Parser } from './parser'
import { recompile } from './compiler'
export const store: Record<string, any> = {};
export const _rt = {
@ -42,7 +46,11 @@ export const _rt = {
},
rebind: (name: string, pathOrValue: any, maybeValue?: any) => {
if (maybeValue === undefined) {
store[name] = pathOrValue;
if (pathOrValue && pathOrValue.ast) {
recompile(name, pathOrValue._ast);
} else {
store[name] = pathOrValue;
}
} else {
const path = pathOrValue as string[];
let obj = store[name];
@ -109,5 +117,12 @@ export const _rt = {
default:
return { width: 0, height: 0 };
}
},
redefine: (name: string) => (code: string) => {
const tokens = tokenize(`_tmp = ${code};`);
const parser = new Parser(tokens, "");
const defs = parser.parse();
recompile(name, defs[0]. body);
return { _tag: 'Ok' };
}
}

@ -1,3 +1,6 @@
# TODO delete
double = a \ a * 2;
# map : (a \ b) \ List a \ List b
map = f list \ list
| [] \ []

Loading…
Cancel
Save