From 60c8f74d500e79d86ff116bdf07677dccd593933 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Mon, 9 Feb 2026 12:54:09 -0700 Subject: [PATCH] Letting us redefine functions, reactively --- src/compiler.ts | 151 +++++++++++++++++++++++++++++++++++++++++++++- src/main.ts | 1 + src/runtime-js.ts | 17 +++++- src/stdlib.cg | 3 + 4 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/compiler.ts b/src/compiler.ts index ae3f8c1..11e5204 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,6 +1,10 @@ import type { AST, Pattern, Definition } from './ast'; import { _rt, store } from './runtime-js'; +const definitions: Map = new Map(); +const dependencies: Map> = new Map(); +const dependents: Map> = 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 = new Set()): Set { + 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(); + + 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); + } +} diff --git a/src/main.ts b/src/main.ts index e9b594a..f0a7d41 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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, diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 6c3b7db..4a4cd59 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -1,3 +1,7 @@ +import { tokenize } from './lexer' +import { Parser } from './parser' +import { recompile } from './compiler' + export const store: Record = {}; 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' }; } } diff --git a/src/stdlib.cg b/src/stdlib.cg index b33ce6e..3f0ebf3 100644 --- a/src/stdlib.cg +++ b/src/stdlib.cg @@ -1,3 +1,6 @@ +# TODO delete +double = a \ a * 2; + # map : (a \ b) \ List a \ List b map = f list \ list | [] \ []