import { tokenize } from './lexer' import { Parser } from './parser' import { compile, recompile, definitions, freeVars, dependencies, dependents, astRegistry } from './compiler' import { prettyPrint } from './ast' import type { AST } from './ast' import { measure } from './ui'; const STORAGE_KEY = 'cg-definitions'; export const store: Record = {}; export const _rt = { add: (a: number) => (b: number) => a + b, sub: (a: number) => (b: number) => a - b, mul: (a: number) => (b: number) => a * b, div: (a: number) => (b: number) => a / b, mod: (a: number) => (b: number) => a % b, pow: (a: number) => (b: number) => a ** b, cat: (a: any) => (b: any) => Array.isArray(a) ? [...a, ...b] : a + b, max: (a: number) => (b: number) => Math.max(a, b), min: (a: number) => (b: number) => Math.min(a, b), eq: (a: any) => (b: any) => ({ _tag: deepEqual(a, b) ? 'True' : 'False' }), neq: (a: any) => (b: any) => ({ _tag: deepEqual(a, b) ? 'False' : 'True' }), gt: (a: any) => (b: any) => ({ _tag: a > b ? 'True' : 'False' }), lt: (a: any) => (b: any) => ({ _tag: a < b ? 'True' : 'False' }), gte: (a: any) => (b: any) => ({ _tag: a >= b ? 'True' : 'False' }), lte: (a: any) => (b: any) => ({ _tag: a <= b ? 'True' : 'False' }), ui: { rect: (config: any) => ({ kind: 'rect', ...config }), text: (config: any) => ({ kind: 'text', ...config }), stack: (config: any) => ({ kind: 'stack', ...config }), row: (config: any) => ({ kind: 'row', ...config }), column: (config: any) => ({ kind: 'column', ...config }), padding: (config: any) => ({ kind: 'padding', ...config }), positioned: (config: any) => ({ kind: 'positioned', ...config }), clickable: (config: any) => ({ kind: 'clickable', ...config }), clip: (config: any) => ({ kind: 'clip', ...config }), opacity: (config: any) => ({ kind: 'opacity', ...config }), stateful: (config: any) => ({ kind: 'stateful', ...config }), measure: (config: any) => ({ kind: 'measure', ...config }), measureText: (text: string) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (ctx) { ctx.font = '16px "SF Mono", "Monaco", "Menlo", monospace'; return Math.floor(ctx.measureText(text).width); } return text.length * 10; // fallback }, }, measure: measure, batch: (events: any[]) => ({ _tag: 'Batch', _0: events }), noOp: { _tag: 'NoOp' }, focus: (key: string) => ({ _tag: 'Focus', _0: key }), nth: (i: number) => (xs: any[] | string) => i >= 0 && i < xs.length ? { _tag: 'Some', _0: xs[i] } : { _tag: 'None' }, len: (xs: any[] | string) => xs.length, // str: (x: any) => String(x), show: (value: any): string => { if (value === null || value === undefined) return "None"; if (typeof value === 'string') return value; if (typeof value === 'number') return String(value); if (typeof value === 'boolean') return value ? "True" : "False"; if (value._tag) return value._0 !== undefined ? `${value._tag} ${_rt.show(value._0)}` : value._tag; if (Array.isArray(value)) return `[${value.map(_rt.show).join(", ")}]`; if (typeof value === 'function') return ""; if (typeof value === 'object') { const entries = Object.entries(value).map(([k, v]) => `${k} = ${_rt.show(v)}`); return `{ ${entries.join(", ")} }`; } return String(value); }, chars: (s: string) => s.split(''), join: (delim: string) => (xs: string[]) => xs.join(delim), split: (delim: string) => (xs: string) => xs.split(delim), slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end), debug: (label: string) => (value: any) => { console.log(label, value); return value; }, fuzzyMatch: (query: string) => (target: string) => { const q = query.toLowerCase(); const t = target.toLowerCase(); let qi = 0; for (let ti = 0; ti < t.length && qi < q.length; ti++) { if (t[ti] === q[qi]) qi++; } return { _tag: qi === q.length ? 'True' : 'False' }; }, storeSearch: (query: string) => { return Object.keys(store).filter(name => _rt.fuzzyMatch(query)(name)._tag === 'True'); }, getSource: (name: string) => { const ast = definitions.get(name); if (!ast) return ""; const printed = prettyPrint(ast); return printed; }, rebind: (name: string, pathOrValue: any, maybeValue?: any) => { if (maybeValue === undefined) { store[name] = pathOrValue; } else { const path = pathOrValue as string[]; let obj = store[name]; for (let i = 0; i < path.length - 1; i++) { obj = obj[path[i]]; } obj[path[path.length - 1]] = maybeValue; } syncToAst(name); }, 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' }; }, undefine: (name: string) => { delete store[name]; definitions.delete(name); dependencies.delete(name); dependents.delete(name); saveDefinitions(); return { _tag: 'Ok' }; }, eval: (code: string) => { const trimmed = code.trim(); // is it a definition const defMatch = /^([a-z_][a-zA-Z0-9_]*)\s*=(?![=])/.exec(trimmed); if (defMatch) { try { const fullCode = trimmed.endsWith(';') ? trimmed : trimmed + ';'; const tokens = tokenize(fullCode); const parser = new Parser(tokens, fullCode); const defs = parser.parse(); if (defs.length > 0) { const def = defs[0]; recompile(def.name, def.body); saveDefinitions(); return { _tag: 'Defined', _0: def.name }; } } catch (e: any) { return { _tag: 'Err', _0: e.message }; } } // its an expression try { const wrapped = `_expr = ${trimmed};`; const tokens = tokenize(wrapped); const parser = new Parser(tokens, wrapped); const defs = parser.parse(); const ast = defs[0].body; // validate free vars const free = freeVars(ast); const allowed = new Set([ ...Object.keys(store), ...Object.keys(_rt) ]); const unknown = [...free].filter(v => !allowed.has(v)); if (unknown.length > 0) { return { _tag: 'Err', _0: `Unknown: ${unknown.join(', ')}` }; } const compiled = compile(defs[0].body); const fn = new Function('_rt', 'store', `return ${compiled}`); const result = fn(_rt, store); return { _tag: 'Value', _0: result }; } catch (e: any) { return { _tag: 'Err', _0: e.message }; } } } export function saveDefinitions() { const saved: Record = {}; for (const [name, ast] of definitions) { const source = prettyPrint({ kind: 'definition', name, body: ast }); saved[name] = source; } localStorage.setItem(STORAGE_KEY, JSON.stringify(saved)); } export function loadDefinitions() { const data = localStorage.getItem(STORAGE_KEY); if (!data) return; try { const saved = JSON.parse(data); for (const [_, source] of Object.entries(saved)) { const tokens = tokenize(source as string); const parser = new Parser(tokens, source as string); const defs = parser.parse(); if (defs.length > 0) { recompile(defs[0].name, defs[0].body); } } } catch (e) { console.error('Failed to load definitions:', e); console.log(data); } } function valueToAst(value: any): AST { // Numbers if (typeof value === 'number') { return { kind: 'literal', value: Number.isInteger(value) ? { kind: 'int', value } : { kind: 'float', value} }; } // Strings if (typeof value === 'string') { return { kind: 'literal', value: { kind: 'string', value } }; } // Arrays if (Array.isArray(value)) { return { kind: 'list', elements: value.map(valueToAst) }; } // Constructor if (typeof value === 'object' && value !== null && value._tag) { const tag = value._tag; if ('_0' in value) { return { kind: 'apply', func: { kind: 'constructor', name: tag }, args: [valueToAst(value._0)] }; } return { kind: 'constructor', name: tag }; } // Records if (typeof value === 'object' && value !== null && !value._tag) { const entries = Object.entries(value).map(([k, v]) => ({ kind: 'field' as const, key: k, value: valueToAst(v) })); return { kind: 'record', entries }; } // Functions if (typeof value === 'function') { if (value._astId !== undefined) { return astRegistry.get(value._astId)!; } throw new Error('Cannot serialize function without _astId'); } throw new Error(`Cannot convert to AST: ${typeof value}`); } export function syncToAst(name: string) { if (definitions.has(name)) { definitions.set(name, valueToAst(store[name])); saveDefinitions(); } } function deepEqual(a: any, b: any): boolean { if (a === b) return true; if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false; if (Array.isArray(a) !== Array.isArray(b)) return false; if (Array.isArray(a)) { if (a.length !== b.length) return false; return a.every((v, i) => deepEqual(v, b[i])); } const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every(k => deepEqual(a[k], b[k])); }