Letting us redefine functions, reactively
This commit is contained in:
parent
33f3b2cfc2
commit
60c8f74d50
4 changed files with 169 additions and 3 deletions
151
src/compiler.ts
151
src/compiler.ts
|
|
@ -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…
Add table
Add a link
Reference in a new issue