Fixing lots of stuff. pretty printer. saving to localStorage again. store stuff. can't remember now

This commit is contained in:
Dustin Swan 2026-02-11 16:57:51 -07:00
parent 01d8a6d67c
commit b1696499e5
No known key found for this signature in database
GPG key ID: 30D46587E2100467
9 changed files with 300 additions and 102 deletions

View file

@ -2,10 +2,10 @@ import type { AST, Pattern, Definition } from './ast';
import { _rt, store } from './runtime-js';
export const definitions: Map<string, AST> = new Map();
const dependencies: Map<string, Set<string>> = new Map();
const dependents: Map<string, Set<string>> = new Map();
export const dependencies: Map<string, Set<string>> = new Map();
export const dependents: Map<string, Set<string>> = new Map();
export function compile(ast: AST): string {
export function compile(ast: AST, useStore = true, bound = new Set<string>()): string {
switch (ast.kind) {
case 'literal':
if (ast.value.kind === 'string')
@ -14,56 +14,63 @@ export function compile(ast: AST): string {
return JSON.stringify(ast.value.value);
throw new Error(`Cannot compile literal`); // of kind ${ast.value.kind}`);
case 'variable':
return sanitize(ast.name);
case 'variable': {
if (bound.has(ast.name)) {
return sanitizeName(ast.name);
}
return sanitize(ast.name, useStore);
}
case 'lambda':
case 'lambda': {
const newBound = new Set([...bound, ...ast.params]);
const params = ast.params.map(sanitizeName).join(') => (');
return `Object.assign((${params}) => ${compile(ast.body)}, { _ast: (${JSON.stringify(ast)}) })`;
return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound)}, { _ast: (${JSON.stringify(ast)}) })`;
}
case 'apply':
// Constructor
if (ast.func.kind === 'constructor') {
const ctorName = ast.func.name;
const arg = compile(ast.args[0]);
const arg = compile(ast.args[0], useStore, bound);
return `((_a) => _a && typeof _a === 'object' && !Array.isArray(_a) && !_a._tag
? { _tag: "${ctorName}", ..._a }
: { _tag: "${ctorName}", _0: _a })(${arg})`;
}
const args = ast.args.map(compile).join(')(');
return `${compile(ast.func)}(${args})`;
const args = ast.args.map(a => compile(a, useStore, bound)).join(')(');
return `${compile(ast.func, useStore, bound)}(${args})`;
case 'record': {
const parts = ast.entries.map(entry =>
entry.kind === 'spread'
? `...${compile(entry.expr)}`
: `${sanitizeName(entry.key)}: ${compile(entry.value)}`
? `...${compile(entry.expr, useStore, bound)}`
: `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound)}`
)
return `({${parts.join(', ')}})`;
}
case 'list': {
const elements = ast.elements.map(e =>
'spread' in e ? `...${compile(e.spread)}` : compile(e)
'spread' in e ? `...${compile(e.spread, useStore, bound)}` : compile(e, useStore, bound)
);
return `[${elements.join(', ')}]`;
}
case 'record-access':
return `${compile(ast.record)}.${sanitizeName(ast.field)}`;
return `${compile(ast.record, useStore, bound)}.${sanitizeName(ast.field)}`;
case 'record-update':
const updates = Object.entries(ast.updates)
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v)}`);
return `({...${compile(ast.record)}, ${updates.join(', ')}})`;
.map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound)}`);
return `({...${compile(ast.record, useStore, bound)}, ${updates.join(', ')}})`;
case 'let':
const newBound = new Set([...bound, ast.name]);
return `((${sanitizeName(ast.name)}) =>
${compile(ast.body)})(${compile(ast.value)})`;
${compile(ast.body, useStore, newBound)})(${compile(ast.value, useStore, bound)})`;
case 'match':
return compileMatch(ast);
return compileMatch(ast, useStore, bound);
case 'constructor':
return `({ _tag: "${ast.name}" })`;
@ -74,15 +81,20 @@ export function compile(ast: AST): string {
*/
case 'rebind': {
if (ast.target.kind === 'variable') {
return `({ _tag: "Rebind", _0: "${ast.target.name}", _1: ${compile(ast.value)} })`;
} else if (ast.target.kind === 'record-access') {
const field = ast.target.field;
const obj = compile(ast.target.record);
const value = compile(ast.value);
return `(() => { ${obj}.${sanitize(field)} = ${value}; return { _tag: "Rerender" }; })()`;
const rootName = getRootName(ast.target);
const path = getPath(ast.target);
const value = compile(ast.value, useStore, bound);
if (!useStore || !rootName) {
const target = compile(ast.target, useStore, bound);
return `(() => { ${target} = ${value}; return { _tag: "Rerender" }; })()`;
}
if (path.length === 0) {
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${value} })`;
} else {
return `({ _tag: "Rebind", _0: "${rootName}", _1: ${JSON.stringify(path)}, _2: ${value} })`;
}
throw new Error('Invalid rebind target');
}
default:
@ -91,7 +103,7 @@ export function compile(ast: AST): string {
}
}
function sanitize(name: string): string {
function sanitize(name: string, useStore = true): string {
const ops: Record<string, string> = {
'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul',
'div': '_rt.div', 'mod': '_rt.mod', 'eq': '_rt.eq',
@ -101,9 +113,12 @@ function sanitize(name: string): string {
if (ops[name]) return ops[name];
const natives = ['measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui'];
const natives = ['eval', 'measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'undefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui'];
if (natives.includes(name)) return `_rt.${name}`;
if (useStore) {
return `store.${sanitizeName(name)}`;
}
return sanitizeName(name);
}
@ -113,26 +128,29 @@ function sanitizeName(name: string): string {
'if','else','switch','case','for','while','do','break',
'continue','new','delete','typeof','in','this','super',
'import','export','extends','static','yield','await','async',
'try','catch','finally','throw','null','true','false'
'try','catch','finally','throw','null','true','false',
'eval','Function','window','document','console'
];
if (reserved.includes(name)) return `_${name}`;
return name.replace(/-/g, '_');
}
function compileMatch(ast: AST & { kind: 'match'}): string {
const expr = compile(ast.expr);
function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set<string>()): string {
const expr = compile(ast.expr, useStore, bound);
const tmpVar = `_m${Math.floor(Math.random() * 10000)}`;
let code = `((${tmpVar}) => { `;
for (const c of ast.cases) {
const { condition, bindings } = compilePattern(c.pattern, tmpVar);
const patternBound = patternVars(c.pattern);
const newBound = new Set([...bound, ...patternBound]);
code += `if (${condition}) { `;
if (bindings.length > 0) {
code += `const ${bindings.join(', ')}; `;
}
code += `return ${compile(c.result)}; }`;
code += `return ${compile(c.result, useStore, newBound)}; }`;
}
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
@ -182,7 +200,7 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
bindings.push(...sub.bindings);
});
bindings.push(`${sanitize(pattern.spread)} = ${expr}.slice(${pattern.head.length})`);
bindings.push(`${sanitizeName(pattern.spread)} = ${expr}.slice(${pattern.head.length})`);
return { condition, bindings };
}
@ -191,7 +209,7 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi
let condition = 'true';
const bindings: string[] = [];
for (const [field, fieldPattern] of Object.entries(pattern.fields)) {
const sub = compilePattern(fieldPattern, `${expr}.${sanitize(field)}`);
const sub = compilePattern(fieldPattern, `${expr}.${sanitizeName(field)}`);
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
bindings.push(...sub.bindings);
}
@ -227,9 +245,8 @@ export function compileAndRun(defs: Definition[]) {
}
}
for (const def of defs) {
const compiled = `const ${sanitize(def.name)} = ${compile(def.body)};`;
const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false)};`;
compiledDefs.push(compiled);
try {
@ -242,13 +259,13 @@ export function compileAndRun(defs: Definition[]) {
}
const lastName = defs[defs.length - 1].name;
const defNames = defs.map(d => sanitize(d.name)).join(', ');
const defNames = defs.map(d => sanitizeName(d.name)).join(', ');
const code = `${compiledDefs.join('\n')}
return { ${defNames}, __result: ${sanitize(lastName)} };`;
return { ${defNames}, __result: ${sanitizeName(lastName)} };`;
const fn = new Function('_rt', code);
const allDefs = fn(_rt);
const fn = new Function('_rt', 'store', code);
const allDefs = fn(_rt, store);
// Populate store
for (const [name, value] of Object.entries(allDefs)) {
@ -260,7 +277,7 @@ return { ${defNames}, __result: ${sanitize(lastName)} };`;
return allDefs.__result;
}
function freeVars(ast: AST, bound: Set<string> = new Set()): Set<string> {
export function freeVars(ast: AST, bound: Set<string> = new Set()): Set<string> {
switch (ast.kind) {
case 'literal':
case 'constructor':
@ -385,3 +402,17 @@ export function recompile(name: string, newAst: AST) {
store[defName] = fn(_rt, store);
}
}
function getRootName(ast: AST): string | null {
if (ast.kind === 'variable') return ast.name;
if (ast.kind === 'record-access') return getRootName(ast.record);
return null;
}
function getPath(ast: AST): string[] {
if (ast.kind === 'variable') return [];
if (ast.kind === 'record-access') {
return [...getPath(ast.record), ast.field];
}
return [];
}