From 6f7f06b7484eb957c723d3fc4ac84f3b2727b40e Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Mon, 9 Feb 2026 13:11:39 -0700 Subject: [PATCH] deleting interpreter now that we have compiler --- src/ast.ts | 9 +- src/builtins.ts | 495 --------------------------------------------- src/counter.cg | 17 -- src/env.ts | 3 - src/interpreter.ts | 348 ------------------------------- src/persistence.ts | 29 --- src/runtime.ts | 469 ------------------------------------------ src/store.ts | 56 ----- src/types.ts | 68 ------- src/ui.ts | 16 +- src/valueToAST.ts | 45 ----- src/valueToUI.ts | 141 ------------- 12 files changed, 19 insertions(+), 1677 deletions(-) delete mode 100644 src/builtins.ts delete mode 100644 src/counter.cg delete mode 100644 src/env.ts delete mode 100644 src/interpreter.ts delete mode 100644 src/persistence.ts delete mode 100644 src/runtime.ts delete mode 100644 src/store.ts delete mode 100644 src/types.ts delete mode 100644 src/valueToAST.ts delete mode 100644 src/valueToUI.ts diff --git a/src/ast.ts b/src/ast.ts index c1ced95..0cecb81 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,10 +1,13 @@ -import type { Value } from './types'; +type LiteralValue = + | { kind: 'int', value: number } + | { kind: 'float', value: number } + | { kind: 'string', value: string }; // Literals and Variables export type Literal = { kind: 'literal' - value: Value + value: LiteralValue line?: number column?: number start?: number @@ -198,8 +201,6 @@ export function prettyPrint(ast: AST, indent = 0): string { return `${i}${val.value}`; case 'string': return `${i}"${val.value}"`; - default: - return `${i}${val.kind}`; } } diff --git a/src/builtins.ts b/src/builtins.ts deleted file mode 100644 index cbc527c..0000000 --- a/src/builtins.ts +++ /dev/null @@ -1,495 +0,0 @@ -import type { Value } from './types' -import { measure } from './ui' -import { valueToUI } from './valueToUI' - -const measureCanvas = document.createElement('canvas'); -const measureCtx = measureCanvas.getContext('2d')!; -if (!measureCtx) - throw new Error('Failed to create canvas'); -measureCtx.font = '16px "SF Mono", "Monaco", "Menlo", "Consolas", "Courier New", monospace'; - -function expectInt(v: Value, name: string): number { - if (v.kind !== 'int') - throw new Error(`${name} expects int, got ${v.kind}`); - return v.value; -} - -// function expectFloat(v: Value, name: string): number { -// if (v.kind !== 'float') -// throw new Error(`${name} expects float, got ${v.kind}`); -// return v.value; -// } - -function expectNumber(v: Value, name: string): number { - if (v.kind !== 'float' && v.kind !== 'int') - throw new Error(`${name} expects number, got ${v.kind}`); - return v.value; -} - -function expectString(v: Value, name: string): string { - if (v.kind !== 'string') - throw new Error(`${name} expects string, got ${v.kind}`); - return v.value; -} - -// function expectList(v: Value, name: string): Value[] { -// if (v.kind !== 'list') -// throw new Error(`${name} expects list, got ${v.kind}`); -// return v.elements; -// } - -export const builtins: { [name: string]: Value } = { - // Arithmetic - 'add': { - kind: 'native', - name: 'add', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'add'); - const y = expectNumber(b, 'add'); - return { kind: 'int', value: x + y }; - } - }, - - 'sub': { - kind: 'native', - name: 'sub', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'sub'); - const y = expectNumber(b, 'sub'); - return { kind: 'int', value: x - y }; - } - }, - - 'mul': { - kind: 'native', - name: 'mul', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'mul'); - const y = expectNumber(b, 'mul'); - return { kind: 'int', value: x * y }; - } - }, - - 'div': { - kind: 'native', - name: 'div', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'div'); - const y = expectNumber(b, 'div'); - return { kind: 'int', value: Math.floor(x / y) }; - } - }, - - 'mod': { - kind: 'native', - name: 'mod', - arity: 2, - fn: (a, b) => { - const x = expectInt(a, 'mod'); - const y = expectInt(b, 'mod'); - return { kind: 'int', value: x % y }; - } - }, - - 'pow': { - kind: 'native', - name: 'pow', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'add'); - const y = expectNumber(b, 'add'); - return { kind: 'int', value: Math.pow(x, y) }; - } - }, - - // Comparison - 'eq': { - kind: 'native', - name: 'eq', - arity: 2, - fn: (a, b) => { - return { - kind: 'constructor', - name: JSON.stringify(a) === JSON.stringify(b) ? 'True' : 'False', - args: [] - }; - } - }, - - 'neq': { - kind: 'native', - name: 'eq', - arity: 2, - fn: (a, b) => { - return { - kind: 'constructor', - name: JSON.stringify(a) !== JSON.stringify(b) ? 'True' : 'False', - args: [] - }; - } - }, - - 'lt': { - kind: 'native', - name: 'lt', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'lt'); - const y = expectNumber(b, 'lt'); - return { - kind: 'constructor', - name: x < y ? 'True' : 'False', - args: [] - }; - } - }, - - 'gt': { - kind: 'native', - name: 'gt', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'gt'); - const y = expectNumber(b, 'gt'); - return { - kind: 'constructor', - name: x > y ? 'True' : 'False', - args: [] - }; - } - }, - - 'lte': { - kind: 'native', - name: 'lt', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'lte'); - const y = expectNumber(b, 'lt'); - return { - kind: 'constructor', - name: x <= y ? 'True' : 'False', - args: [] - }; - } - }, - - 'gte': { - kind: 'native', - name: 'gte', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'gte'); - const y = expectNumber(b, 'gte'); - return { - kind: 'constructor', - name: x >= y ? 'True' : 'False', - args: [] - }; - } - }, - - // String & List - 'cat': { - kind: 'native', - name: 'cat', - arity: 2, - fn: (a, b) => { - if (a.kind === 'string' && b.kind === 'string') { - return { kind: 'string', value: a.value + b.value }; - } - if (a.kind === 'list' && b.kind === 'list') { - return { kind: 'list', elements: [...a.elements, ...b.elements] }; - } - throw new Error('cat requires 2 lists or 2 strings'); - } - }, - - 'len': { - kind: 'native', - name: 'len', - arity: 1, - fn: (seq) => { - if (seq.kind === 'string') { - return { kind: 'int', value: seq.value.length }; - } - if (seq.kind === 'list') { - return { kind: 'int', value: seq.elements.length }; - } - throw new Error('cat requires a list or a string'); - } - }, - - 'at': { - kind: 'native', - name: 'at', - arity: 2, - fn: (seq, idx) => { - const i = expectInt(idx, 'at'); - - if (seq.kind === 'string') { - return { kind: 'string', value: seq.value[i] || '' }; - } - if (seq.kind === 'list') { - return seq.elements[i]; - } - throw new Error('at requires a list or a string'); - } - }, - - 'slice': { - kind: 'native', - name: 'slice', - arity: 3, - fn: (seq, start, end) => { - const s = expectInt(start, 'slice'); - const e = expectInt(end, 'slice'); - - if (seq.kind === 'string') { - return { kind: 'string', value: seq.value.slice(s, e) }; - } - if (seq.kind === 'list') { - return { kind: 'list', elements: seq.elements.slice(s, e) }; - } - throw new Error('slice requires a list or a string'); - } - }, - - 'head': { - kind: 'native', - name: 'head', - arity: 1, - fn: (seq) => { - if (seq.kind === 'string') { - if (seq.value.length === 0) { - return { kind: 'constructor', name: 'None', args: [] }; - } - return { kind: 'constructor', name: 'Some', args: [{ kind: 'string', value: seq.value[0] }] }; - } - if (seq.kind === 'list') { - if (seq.elements.length === 0) { - return { kind: 'constructor', name: 'None', args: [] }; - } - return { kind: 'constructor', name: 'Some', args: [seq.elements[0]] }; - } - throw new Error('head requires a list or a string'); - } - }, - - 'tail': { - kind: 'native', - name: 'tail', - arity: 1, - fn: (seq) => { - if (seq.kind === 'string') { - if (seq.value.length === 0) { - return { kind: 'constructor', name: 'None', args: [] }; - } - return { kind: 'constructor', name: 'Some', args: [{ kind: 'string', value: seq.value.slice(1) }] }; - } - if (seq.kind === 'list') { - if (seq.elements.length === 0) { - return { kind: 'constructor', name: 'None', args: [] }; - } - return { kind: 'constructor', name: 'Some', args: [{ kind: 'list', elements: seq.elements.slice(1) }] }; - } - throw new Error('tail requires a list or a string'); - } - }, - - // Types - 'str': { - kind: 'native', - name: 'str', - arity: 1, - fn: (val) => { - if (val.kind === 'int' || val.kind === 'float') - return { kind: 'string', value: val.value.toString() } - - if (val.kind === 'string') - return val; - - throw new Error('str: cannot convert to string'); - } - }, - - 'int': { - kind: 'native', - name: 'int', - arity: 1, - fn: (val) => { - if (val.kind === 'int') - return val; - - if (val.kind === 'float') - return { kind: 'int', value: Math.floor(val.value) }; - - if (val.kind === 'string') { - const parsed = parseInt(val.value, 10); - - if (isNaN(parsed)) - throw new Error(`int: cannot parse "${val.value}"`); - - return { kind: 'int', value: parsed } - } - - throw new Error(`int: cannot convert to int`); - } - }, - - 'float': { - kind: 'native', - name: 'float', - arity: 1, - fn: (val) => { - if (val.kind === 'float') - return val; - - if (val.kind === 'int') - return { kind: 'float', value: val.value }; - - if (val.kind === 'string') { - const parsed = parseFloat(val.value); - - if (isNaN(parsed)) - throw new Error(`float: cannot parse "${val.value}"`); - - return { kind: 'float', value: parsed } - } - - throw new Error(`float: cannot convert to float`); - } - }, - - // Math - 'sqrt': { - kind: 'native', - name: 'sqrt', - arity: 1, - fn: (val) => { - const x = expectNumber(val, 'sqrt'); - return { kind: 'float', value: Math.sqrt(x) }; - } - }, - - 'abs': { - kind: 'native', - name: 'abs', - arity: 1, - fn: (val) => { - if (val.kind === 'int') - return { kind: 'int', value: Math.abs(val.value) }; - - if (val.kind === 'float') - return { kind: 'float', value: Math.abs(val.value) }; - - throw new Error('abs expects a number'); - } - }, - - 'floor': { - kind: 'native', - name: 'floor', - arity: 1, - fn: (val) => { - const x = expectNumber(val, 'floor'); - return { kind: 'int', value: Math.floor(x) }; - } - }, - - 'ceil': { - kind: 'native', - name: 'ceil', - arity: 1, - fn: (val) => { - const x = expectNumber(val, 'ceil'); - return { kind: 'int', value: Math.ceil(x) }; - } - }, - - 'round': { - kind: 'native', - name: 'round', - arity: 1, - fn: (val) => { - const x = expectNumber(val, 'round'); - return { kind: 'int', value: Math.round(x) }; - } - }, - - 'min': { - kind: 'native', - name: 'min', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'min'); - const y = expectNumber(b, 'min'); - const result = Math.min(x, y); - - if (a.kind === 'float' || b.kind === 'float') - return { kind: 'float', value: result }; - - return { kind: 'int', value: result }; - } - }, - - 'max': { - kind: 'native', - name: 'max', - arity: 2, - fn: (a, b) => { - const x = expectNumber(a, 'max'); - const y = expectNumber(b, 'max'); - const result = Math.max(x, y); - - if (a.kind === 'float' || b.kind === 'float') - return { kind: 'float', value: result }; - - return { kind: 'int', value: result }; - } - }, - - 'measureText': { - kind: 'native', - name: 'measureText', - arity: 1, - fn: (text) => { - const str = expectString(text, 'measureText'); - // TODO - const metrics = measureCtx.measureText(str); - return { kind: 'float', value: metrics.width }; - } - }, - - 'measure': { - kind: 'native', - name: 'measure', - arity: 1, - fn: (uiValue) => { - const ui = valueToUI(uiValue); - const size = measure(ui); - return { - kind: 'record', - fields: { - width: { kind: 'int', value: size.width }, - height: { kind: 'int', value: size.height } - } - }; - } - }, - - 'debug': { - kind: 'native', - name: 'debug', - arity: 2, - fn: (label, value) => { - const str = expectString(label, 'debug'); - console.log(str, value); - return value; - } - } -} diff --git a/src/counter.cg b/src/counter.cg deleted file mode 100644 index 3a33850..0000000 --- a/src/counter.cg +++ /dev/null @@ -1,17 +0,0 @@ -init = 0; - -update = state event \ state + 1; - -view = count \ - Column { - gap = 20, - children = [ - Text({ content = str(count), x = 0, y = 20 }), - Clickable { - event = Increment, - child = Rect { w = 100, h = 40, color = "blue" } - } - ] - }; - -{ init = init, update = update, view = view } diff --git a/src/env.ts b/src/env.ts deleted file mode 100644 index b9b5756..0000000 --- a/src/env.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Value } from './types'; - -export type Env = Map diff --git a/src/interpreter.ts b/src/interpreter.ts deleted file mode 100644 index 850f4d8..0000000 --- a/src/interpreter.ts +++ /dev/null @@ -1,348 +0,0 @@ -import type { AST, Pattern } from './ast'; -import type { Env } from './env'; -import type { Value } from './types'; -import { RuntimeError } from './error'; -import { recordDependency } from './store'; - -export function evaluate(ast: AST, env: Env, source: string): Value { - switch (ast.kind) { - case 'literal': - return ast.value; - - case 'variable': { - const val = env.get(ast.name); - - if (val === undefined) - throw RuntimeError(`Unknown variable: ${ast.name}`, ast.line, ast.column, source); - - recordDependency(ast.name); - - return val; - } - - case 'list': { - const elements: Value[] = []; - - for (const item of ast.elements) { - // Spread - if ('spread' in item) { - const spreadValue = evaluate(item.spread, env, source); - - if (spreadValue.kind !== 'list') - throw RuntimeError('can only spread lists', ast.line, ast.column, source); - - elements.push(...spreadValue.elements); - } else { - elements.push(evaluate(item, env, source)); - } - } - - return { kind: 'list', elements }; - } - - case 'record': { - const fields: { [key: string]: Value } = {}; - const fieldMeta: { [key: string]: { body: AST, dependencies: Set } } = {}; - const recordEnv = new Map(env); - const fieldNames = Object.keys(ast.fields); - - for (const [k, fieldAst] of Object.entries(ast.fields)) { - // Track which siblings are accessed - const deps = new Set(); - - const trackingEnv = new Map(recordEnv); - const originalGet = trackingEnv.get.bind(trackingEnv); - trackingEnv.get = (name: string) => { - if (fieldNames.includes(name) && name !== k) { - deps.add(name); - } - return originalGet(name); - } - - const value = evaluate(fieldAst, trackingEnv, source); - - fields[k] = value; - fieldMeta[k] = { body: fieldAst, dependencies: deps }; - recordEnv.set(k, value); - } - - return { kind: 'record', fields, fieldMeta }; - } - - case 'record-access': { - const record = evaluate(ast.record, env, source); - - if (record.kind !== 'record') - throw RuntimeError('Not a record', ast.line, ast.column, source); - - const value = record.fields[ast.field]; - - if (value === undefined) { - throw RuntimeError(`Field ${ast.field} not found`, ast.line, ast.column, source); - } - - return value; - } - - case 'record-update': { - const record = evaluate(ast.record, env, source); - - if (record.kind !== 'record') - throw RuntimeError('Not a record', ast.line, ast.column, source); - - const newFields: { [key: string]: Value } = { ...record.fields }; - - for (const [field, expr] of Object.entries(ast.updates)) { - newFields[field] = evaluate(expr, env, source); - } - - return { kind: 'record', fields: newFields }; - } - - case 'constructor': - return { - kind: 'constructor', - name: ast.name, - args: [] - }; - - case 'let': { - const newEnv = new Map(env); - - const val = evaluate(ast.value, newEnv, source); - - // Don't bind _ - if (ast.name !== '_') { - newEnv.set(ast.name, val); - } - - newEnv.set(ast.name, val); - - return evaluate(ast.body, newEnv, source); - } - - case 'lambda': - return { - kind: 'closure', - params: ast.params, - body: ast.body, - env - } - - case 'apply': { - const func = evaluate(ast.func, env, source); - const argValues = ast.args.map(arg => evaluate(arg, env, source)); - - // Native functions - if (func.kind === 'native') { - // Exact args - if (argValues.length === func.arity) { - return func.fn(...argValues); - } - - // Partial application - if (argValues.length < func.arity) { - const capturedArgs = argValues; - - return { - kind: 'native', - name: func.name, - arity: func.arity - argValues.length, - fn: (...restArgs: Value[]) => { - return func.fn(...capturedArgs, ...restArgs); - - } - }; - } - - throw RuntimeError(`Function expects ${func.arity} args, but got ${argValues.length}`, ast.line, ast.column, source); - } - - // Constructor application - if (func.kind === 'constructor') { - const argValues = ast.args.map(arg => evaluate(arg, env, source)); - return { - kind: 'constructor', - name: func.name, - args: [...func.args, ...argValues] - }; - } - - if (func.kind !== 'closure') - throw RuntimeError('Not a function', ast.line, ast.column, source); - - // Too few args (Currying) - if (argValues.length < func.params.length) { - // Bind the params we have - const newEnv = new Map(func.env); - - for (let i = 0; i < argValues.length; i++) { - newEnv.set(func.params[i], argValues[i]); - } - - return { - kind: 'closure', - params: func.params.slice(argValues.length), - body: func.body, - env: newEnv - }; - } - - // Too many args - if (argValues.length > func.params.length) - throw RuntimeError('Too many arguments', ast.line, ast.column, source); - - // Exact number of args - const callEnv = new Map(func.env); - for (let i = 0; i < argValues.length; i++) { - callEnv.set(func.params[i], argValues[i]); - } - - return evaluate(func.body, callEnv, source); - } - - case 'match': { - const value = evaluate(ast.expr, env, source); - - for (const matchCase of ast.cases) { - const bindings = matchPattern(value, matchCase.pattern); - - if (bindings !== null) { - const newEnv = new Map(env); - for (const [name, val] of Object.entries(bindings)) { - newEnv.set(name, val); - } - return evaluate(matchCase.result, newEnv, source); - } - } - - throw RuntimeError('Non-exhaustive pattern match', ast.line, ast.column, source); - } - - case 'rebind': { - const value = evaluate(ast.value, env, source); - - if (ast.target.kind === 'variable') { - const name = ast.target.name; - return { - kind: 'constructor', - name: 'Rebind', - args: [{ kind: 'string', value: name }, value] - }; - } - - if (ast.target.kind === 'record-access') { - let current: AST = ast.target; - const path: string[] = []; - - while (current.kind === 'record-access') { - path.unshift(current.field); - current = current.record; - } - - if (current.kind !== 'variable') - throw RuntimeError('Rebind target must be a variable or field access', ast.line, ast.column, source); - - const rootName = current.name; - const rootValue = env.get(rootName); - - if (!rootValue) - throw RuntimeError(`Unknown variable: ${rootName}`, ast.line, ast.column, source); - - return { - kind: 'constructor', - name: 'Rebind', - args: [ - { kind: 'string', value: rootName }, - { kind: 'list', elements: path.map(p => ({ kind: 'string', value: p })) }, - value - ] - }; - } - - throw RuntimeError('Rebind target must be a variable or field access', ast.line, ast.column, source); - } - - default: - throw RuntimeError('Syntax Error', ast.line, ast.column, source); - } -} - -type Bindings = { [key: string]: Value }; - -function matchPattern(value: Value, pattern: Pattern): Bindings | null { - switch (pattern.kind) { - case 'wildcard': - return {}; - - case 'var': - return { [pattern.name]: value }; - - case 'literal': - if (value.kind === 'int' || value.kind === 'float' || value.kind === 'string') { - if (value.value === pattern.value) { - return {}; - } - } - return null; - - case 'constructor': { - if (value.kind !== 'constructor') return null; - if (value.name !== pattern.name) return null; - if (value.args.length !== pattern.args.length) return null; - - const bindings: Bindings = {}; - for (let i = 0; i < pattern.args.length; i++) { - const argBindings = matchPattern(value.args[i], pattern.args[i]); - if (argBindings === null) return null; - Object.assign(bindings, argBindings); - } - return bindings; - } - - case 'list': { - if (value.kind !== 'list') return null; - if (value.elements.length !== pattern.elements.length) return null; - - const bindings: Bindings = {}; - for (let i = 0; i < pattern.elements.length; i++) { - const elemBindings = matchPattern(value.elements[i], pattern.elements[i]); - if (elemBindings === null) return null; - Object.assign(bindings, elemBindings); - } - return bindings; - } - - case 'list-spread': { - if (value.kind !== 'list') return null; - if (value.elements.length < pattern.head.length) return null; - - const bindings: Bindings = {}; - for (let i = 0; i < pattern.head.length; i++) { - const elemBindings = matchPattern(value.elements[i], pattern.head[i]); - if (elemBindings === null) return null; - Object.assign(bindings, elemBindings); - } - - const rest = value.elements.slice(pattern.head.length); - bindings[pattern.spread] = { kind: 'list', elements: rest }; - - return bindings; - } - - case 'record': { - if (value.kind !== 'record') return null; - - const bindings: Bindings = {}; - for (const [fieldName, fieldPattern] of Object.entries(pattern.fields)) { - const fieldValue = value.fields[fieldName]; - if (fieldValue === undefined) return null; - - const fieldBindings = matchPattern(fieldValue, fieldPattern); - if (fieldBindings === null) return null; - Object.assign(bindings, fieldBindings); - } - return bindings; - } - } -} diff --git a/src/persistence.ts b/src/persistence.ts deleted file mode 100644 index d6d57f3..0000000 --- a/src/persistence.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { AST } from './ast' -import type { Store } from './store' - -const STORAGE_KEY = 'cg_store'; - -export function saveStore(store: Store) { - const data: Record = {}; - for (const [name, entry] of store) { - data[name] = { - body: entry.body, - source: 'file' - }; - } - // localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); -} - -export function loadStore(): Record | null { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return null; - return JSON.parse(raw); - } catch(e) { - return null; - } -} - -export function clearStore() { - localStorage.removeItem(STORAGE_KEY); -} diff --git a/src/runtime.ts b/src/runtime.ts deleted file mode 100644 index dcc743f..0000000 --- a/src/runtime.ts +++ /dev/null @@ -1,469 +0,0 @@ -import type { Value, UIValue } from './types'; -import { valueToUI } from './valueToUI'; -import { render, hitTest } from './ui'; -import { evaluate } from './interpreter'; -import { CGError } from './error'; -import type { AST } from './ast'; -import type { Env } from './env'; -import type { Store } from './store'; -import { saveStore } from './persistence'; -import { valueToAST } from './valueToAST'; - -export type App = { - init: Value; - update: Value; // State / Event / State - view: Value; // State / UI -} - -export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: Env, store: Store, dependents: Map>) { - let state = app.init; - - type ComponentInstance = { - state: Value; - update: Value; - view: Value; - }; - - // Store-related builtins - env.set('storeSearch', { - kind: 'native', - name: 'storeNames', - arity: 1, - fn: (query) => { - const names: Value[] = []; - const searchTerm = query.kind === 'string' ? query.value.toLowerCase() : ''; - - for (const name of store.keys()) { - if (searchTerm === '' || name.toLowerCase().includes(searchTerm)) { - names.push({ kind: 'string', value: name }); - } - } - return { kind: 'list', elements: names }; - } - }); - - - const componentInstances = new Map(); - - // Focus tracking - let focusedComponentKey: string | null = null; - - function setFocus(componentKey: string | null) { - if (focusedComponentKey === componentKey) return; - - const oldFocus = focusedComponentKey; - focusedComponentKey = componentKey; - - // Blur event to the previous - if (oldFocus && componentInstances.has(oldFocus)) { - handleComponentEvent(oldFocus, { - kind: 'constructor', - name: 'Blurred', - args: [] - }); - } - - // Focus event to the new - if (componentKey && componentInstances.has(componentKey)) { - handleComponentEvent(componentKey, { - kind: 'constructor', - name: 'Focused', - args: [] - }); - } - - rerender(); - } - - function recomputeDependents(changedName: string, visited: Set = new Set()) { - const toRecompute = dependents.get(changedName); - if (!toRecompute) return; - - for (const depName of toRecompute) { - // Cycle detection - if (visited.has(depName)) { - console.warn(`Cycle detected ${depName} already recomputed`); - continue; - } - visited.add(depName); - - const entry = store.get(depName); - if (entry) { - const newValue = evaluate(entry.body, env, source); - env.set(depName, newValue); - entry.value = newValue; - - recomputeDependents(depName, visited); - } - } - } - - function handleComponentEvent(componentKey: string, event: Value) { - const instance = componentInstances.get(componentKey); - if (!instance) return; - - if (instance.update.kind !== 'closure') - throw new Error('Component update must be a closure'); - - try { - const callEnv = new Map(instance.update.env); - callEnv.set(instance.update.params[0], instance.state); - callEnv.set(instance.update.params[1], event); - const result = evaluate(instance.update.body, callEnv, source); - - if (result.kind !== 'record') - throw new Error('Component update must return { state, emit }'); - - const newState = result.fields.state; - const emitList = result.fields.emit; - - instance.state = newState; - - if (emitList && emitList.kind === 'list') { - for (const event of emitList.elements) { - handleEvent(event); - } - } - - rerender(); - - } catch(error) { - if (error instanceof CGError) { - console.error(error.format()); - } else { - throw error; - - } - } - } - - function expandStateful(ui: UIValue, path: number[]): UIValue { - switch (ui.kind) { - case 'stateful': { - const fullKey = [...path, ui.key].join('.'); - - let instance = componentInstances.get(fullKey); - if (!instance) { - // first time, create it - if (ui.init.kind !=='record') - throw new Error('Stateful init must be a record'); - - instance = { - state: ui.init, - update: ui.update, - view: ui.view - }; - componentInstances.set(fullKey, instance); - } else { - // refresh closures, pick up new values - instance.update = ui.update; - instance.view = ui.view; - } - - if (instance.view.kind !== 'closure') - throw new Error('Stateful view must be a closure'); - - const callEnv = new Map(instance.view.env); - callEnv.set(instance.view.params[0], instance.state); - const viewResult = evaluate(instance.view.body, callEnv, source); - let viewUI = valueToUI(viewResult); - - if (ui.focusable) { - viewUI = { - kind: 'clickable', - child: viewUI, - event: { - kind: 'constructor', - name: 'FocusAndClick', - args: [{ kind: 'string', value: fullKey }] - } - }; - } - - return expandStateful(viewUI, path); - } - - case 'stack': - case 'row': - case 'column': { - return { - ...ui, - children: ui.children.map((child: UIValue, i: number) => - expandStateful(child, [...path, i]) - ) - } - } - - case 'clickable': - case 'padding': - case 'positioned': - case 'opacity': - case 'clip': { - return { - ...ui, - child: expandStateful((ui as any).child, [...path, 0]) - }; - } - - default: - // leaf nodes - return ui; - } - - } - - function setupCanvas() { - const dpr = window.devicePixelRatio || 1; - - canvas.width = window.innerWidth * dpr; - canvas.height = window.innerHeight * dpr; - - canvas.style.width = window.innerWidth + 'px'; - canvas.style.height = window.innerHeight + 'px'; - } - - setupCanvas(); - - function rerender() { - if (app.view.kind !== 'closure') - throw new Error('view must be a function'); - - const viewport: Value = { - kind: 'record', - fields: { - width: { kind: 'int', value: window.innerWidth }, - height: { kind: 'int', value: window.innerHeight } - } - }; - - try { - const callEnv = new Map(env); - callEnv.set(app.view.params[0], state); - callEnv.set(app.view.params[1], viewport); - const uiValue = evaluate(app.view.body, callEnv, source); - const ui = valueToUI(uiValue); - const expandedUI = expandStateful(ui, []); - - render(expandedUI, canvas); - } catch (error) { - if (error instanceof CGError) { - console.error(error.format()); - } else { - throw error; - } - } - } - - function handleEvent(event: Value) { - handleEventInner(event); - rerender(); - } - - function handleEventInner(event: Value) { - if (event.kind === 'constructor' && event.name === 'Batch') { - if (event.args.length === 1 && event.args[0].kind === 'list') { - for (const subEvent of event.args[0].elements) { - handleEventInner(subEvent); - } - return; - } - } - - if (event.kind === 'constructor' && event.name === 'FocusAndClick') { - if (event.args.length === 2 && event.args[0].kind === 'string') { - const componentKey = event.args[0].value; - const coords = event.args[1]; - - setFocus(componentKey); - - handleComponentEvent(componentKey, { - kind: 'constructor', - name: 'Clicked', - args: [coords] - }); - - return; - } - } - - if (event.kind === 'constructor' && event.name === 'Rebind') { - if (event.args[0].kind !== 'string') return; - const name = event.args[0].value; - - let newValue: Value; - if (event.args.length === 2) { - // Rebind "name" value - newValue = event.args[1]; - } else if (event.args.length === 3 && event.args[1].kind === 'list') { - // Rebind "name" ["path"] - const pathList = event.args[1] as { elements: Value[] }; - const path = pathList.elements.map((e: Value) => e.kind === 'string' ? e.value : ''); - const currentValue = env.get(name); - if (!currentValue) return; - newValue = updatePath(currentValue, path, event.args[2], env); - } else { - return; - } - - env.set(name, newValue); - const entry = store.get(name); - if (entry) { - entry.value = newValue; - entry.body = valueToAST(newValue); - } - - recomputeDependents(name); - - saveStore(store); - return; - } - - if (event.kind === 'constructor' && event.name === 'Focus') { - if (event.args.length === 1 && event.args[0].kind === 'string') { - setFocus(event.args[0].value) - } - return; - } - - if (event.kind === 'constructor' && event.name === 'NoOp') - return; - - if (app.update.kind !== 'closure') - throw new Error('update must be a function'); - - if (app.update.params.length !== 2) - throw new Error('update must have 2 parameters'); - - try { - const callEnv = new Map(app.update.env); - callEnv.set(app.update.params[0], state); - callEnv.set(app.update.params[1], event); - const newState = evaluate(app.update.body, callEnv, source); - - state = newState; - } catch (error) { - if (error instanceof CGError) { - console.error(error.format()); - } else { - throw error; - } - } - } - - canvas.addEventListener('click', (e) => { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const hitResult = hitTest(x, y); - if (hitResult) { - const { event, relativeX, relativeY } = hitResult; - - if (event.kind === 'constructor' && (event.name === 'Focus' || event.name === 'Rebind')) { - handleEvent(event); - } else if (event.kind === 'constructor' && event.name === 'FocusAndClick') { - const eventWithCoords: Value = { - kind: 'constructor', - name: event.name, - args: [ - event.args[0], - { - kind: 'record', - fields: { - x: { kind: 'int', value: Math.floor(relativeX) }, - y: { kind: 'int', value: Math.floor(relativeY) }, - } - } - ] - }; - handleEvent(eventWithCoords); - } else { - handleEvent(event); - } - } - }); - - window.addEventListener('keydown', (e) => { - let event: Value | null = null; - - if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { - event = { - kind: 'constructor', - name: 'Char', - args: [{ kind: 'string', value: e.key }] - } - } else { - event = { - kind: 'constructor', - name: e.key, - args: [] - } - } - - if (focusedComponentKey) { - handleComponentEvent(focusedComponentKey, event); - } else { - handleEvent(event); - } - e.preventDefault(); - }); - - window.addEventListener('resize', () => { - setupCanvas(); - rerender(); - }) - - rerender(); -} - -function updatePath(obj: Value, path: string[], value: Value, env: Env): Value { - if (path.length === 0) return value; - - if (obj.kind !== 'record') - throw new Error('Cannot access field on non-record'); - - const [field, ...rest] = path; - - const newFields = { - ...obj.fields, - [field]: updatePath(obj.fields[field], rest, value, env) - }; - - // Reevaluate any dependent fields - if (rest.length === 0 && obj.fieldMeta) { - const visited = new Set(); - recomputeRecordFields(field, newFields, obj.fieldMeta, visited, env); - } - - return { - kind: 'record', - fields: newFields, - fieldMeta: obj.fieldMeta - }; -} - -function recomputeRecordFields( - changedField: string, - fields: { [key: string]: Value }, - fieldMeta: { [key: string]: { body: AST, dependencies: Set } }, - visited: Set, - env: Env -) { - for (const [fieldName, meta] of Object.entries(fieldMeta)) { - if (visited.has(fieldName)) continue; - - if (meta.dependencies.has(changedField)) { - visited.add(fieldName); - - const fieldEnv: Env = new Map(env); - for (const [k, v] of Object.entries(fields)) { - fieldEnv.set(k, v); - } - - const newValue = evaluate(meta.body, fieldEnv, ''); - fields[fieldName] = newValue; - - recomputeRecordFields(fieldName, fields, fieldMeta, visited, env); - } - } -} diff --git a/src/store.ts b/src/store.ts deleted file mode 100644 index 5afc90b..0000000 --- a/src/store.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Value } from './types'; -import type { AST } from './ast'; - -export type StoreEntry = { - value: Value, - body: AST, - dependencies: Set; -}; - -export type Store = Map; - -export function createStore(): Store { - return new Map(); -} - -let currentlyEvaluating: string | null = null; -let currentDependencies: Set | null = null; - -export function startTracking(name: string): Set { - currentlyEvaluating = name; - currentDependencies = new Set(); - return currentDependencies; -} - -export function stopTracking() { - currentlyEvaluating = null; - currentDependencies = null; -} - -export function recordDependency(name: string) { - if (currentDependencies && name !== currentlyEvaluating) { - currentDependencies.add(name); - } -} - -export function isTracking(): boolean { - return currentlyEvaluating !== null; -} - -export function buildDependents(store: Store): Map> { - const dependents = new Map>(); - - for (const name of store.keys()) { - dependents.set(name, new Set()); - } - - for (const [name, entry] of store) { - for (const dep of entry.dependencies) { - if (dependents.has(dep)) { - dependents.get(dep)!.add(name); - } - } - } - - return dependents; -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 151357b..0000000 --- a/src/types.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { AST } from './ast' -import type { Env } from './env' - -export type IntValue = { - kind: 'int' - value: number -} - -export type FloatValue = { - kind: 'float' - value: number -} - -export type StringValue = { - kind: 'string' - value: string -} - -export type Closure = { - kind: 'closure' - params: string[] - body: AST - env: Env -} - -export type ListValue = { - kind: 'list' - elements: Value[] -} - -export type RecordValue = { - kind: 'record' - fields: { [key: string]: Value } - fieldMeta?: { - [key: string]: { - body: AST - dependencies: Set - } - } -} - -export type ConstructorValue = { - kind: 'constructor' - name: string - args: Value[] -} - -export type NativeFunction = { - kind: 'native' - name: string - arity: number - fn: (...args: Value[]) => Value -} - -export type UIValue = - | { kind: 'rect', w: number, h: number, color?: string, strokeColor?: string, strokeWidth?: number, radius?: number } - | { kind: 'text', content: string, color?: string } - | { kind: 'row', children: UIValue[], gap: number } - | { kind: 'column', children: UIValue[], gap: number } - | { kind: 'clickable', child: UIValue, event: Value } - | { kind: 'padding', child: UIValue, amount: number } - | { kind: 'positioned', x: number, y: number, child: UIValue } - | { kind: 'opacity', child: UIValue, opacity: number } - | { kind: 'clip', child: UIValue, w: number, h: number } - | { kind: 'stack', children: UIValue[] } - | { kind: 'stateful', key: string, focusable: boolean, init: Value, update: Value, view: Value } - -export type Value = IntValue | FloatValue | StringValue | Closure | ListValue | RecordValue | ConstructorValue | NativeFunction; diff --git a/src/ui.ts b/src/ui.ts index 292f08c..ee90913 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,11 +1,23 @@ -import type { UIValue, Value } from './types'; +// import type { UIValue, Value } from './types'; +export type UIValue = + | { kind: 'rect', w: number, h: number, color?: string, strokeColor?: string, strokeWidth?: number, radius?: number } + | { kind: 'text', content: string, color?: string } + | { kind: 'row', children: UIValue[], gap: number } + | { kind: 'column', children: UIValue[], gap: number } + | { kind: 'clickable', child: UIValue, event: any } + | { kind: 'padding', child: UIValue, amount: number } + | { kind: 'positioned', x: number, y: number, child: UIValue } + | { kind: 'opacity', child: UIValue, opacity: number } + | { kind: 'clip', child: UIValue, w: number, h: number } + | { kind: 'stack', children: UIValue[] } + | { kind: 'stateful', key: string, focusable: boolean, init: any, update: any, view: any } type ClickRegion = { x: number; y: number; width: number; height: number; - event: Value; + event: any; }; let clickRegions: ClickRegion[] = []; diff --git a/src/valueToAST.ts b/src/valueToAST.ts deleted file mode 100644 index dd1992d..0000000 --- a/src/valueToAST.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { AST } from './ast' -import type { Value } from './types' - -export function valueToAST(value: Value): AST { - switch (value.kind) { - case 'int': - case 'float': - case 'string': - return { kind: 'literal', value }; - - case 'list': - return { - kind: 'list', - elements: value.elements.map(valueToAST) - }; - - case 'record': - const fields: { [key: string]: AST } = {}; - for (const [k, v] of Object.entries(value.fields)) { - if (value.fieldMeta && value.fieldMeta?.[k]?.dependencies?.size > 0) { - fields[k] = value.fieldMeta[k].body; - } else { - fields[k] = valueToAST(v); - } - } - - return { kind: 'record', fields }; - - case 'constructor': - return { - kind: 'constructor', - name: value.name - }; - - case 'closure': - return { - kind: 'lambda', - params: value.params, - body: value.body - }; - - default: - throw new Error(`Cannot convert ${(value as any).kind} to AST`); - } -} diff --git a/src/valueToUI.ts b/src/valueToUI.ts deleted file mode 100644 index 07514aa..0000000 --- a/src/valueToUI.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { Value, UIValue } from './types'; - -export function valueToUI(value: Value): UIValue { - if (value.kind !== 'constructor') { - throw new Error('UI value must be a constructor'); - } - - if (value.args.length !== 1 || value.args[0].kind !== 'record') - throw new Error('UI constructor must have 1 record argument'); - - const fields = value.args[0].fields; - - switch (value.name) { - case 'Rect': { - const { w, h, color, radius, strokeColor, strokeWidth } = fields; - - if (w.kind !== 'int' || h.kind !== 'int') - throw new Error('Invalid Rect fields'); - - return { - kind: 'rect', - w: w.value, - h: h.value, - color: color && color.kind === 'string' ? color.value : undefined, - strokeColor: strokeColor && strokeColor.kind === 'string' ? strokeColor.value : undefined, - strokeWidth: strokeWidth && strokeWidth.kind === 'int' ? strokeWidth.value : undefined, - radius: radius && radius.kind === 'int' ? radius.value : 0 - }; - } - - case 'Text': { - const { content, color } = fields; - - if (content.kind !== 'string') - throw new Error('Invalid Text fields'); - - return { - kind: 'text', - content: content.value, - color: color && color.kind === 'string' ? color.value : undefined - }; - } - - case 'Column': { - const children = fields.children; - const gap = fields.gap; - - if (children.kind !== 'list' || gap.kind !== 'int') - throw new Error('Invalid Column fields'); - - return { kind: 'column', gap: gap.value, children: children.elements.map(valueToUI) }; - } - - case 'Row': { - const children = fields.children; - const gap = fields.gap; - - if (children.kind !== 'list' || gap.kind !== 'int') - throw new Error('Invalid Row fields'); - - return { kind: 'row', gap: gap.value, children: children.elements.map(valueToUI) }; - } - - case 'Clickable': { - const child = fields.child; - const event = fields.event; - - if (event.kind !== 'constructor') - throw new Error('Clickable event must be a constructor'); - - return { kind: 'clickable', event: event, child: valueToUI(child) }; - } - - case 'Padding': { - const child = fields.child; - const amount = fields.amount; - - if (amount.kind !== 'int') - throw new Error('Invalid Padding fields'); - - return { kind: 'padding', amount: amount.value, child: valueToUI(child) }; - } - - case 'Positioned': { - const { x, y, child } = fields; - - if (x.kind !== 'int' || y.kind !== 'int') - throw new Error('Invalid Positioned fields'); - - return { kind: 'positioned', x: x.value, y: y.value, child: valueToUI(child) }; - } - - case 'Opacity': { - const { child, opacity } = fields; - - if (opacity.kind !== 'int') - throw new Error('Invalid Opacity fields'); - - return { kind: 'opacity', opacity: opacity.value, child: valueToUI(child) }; - } - - case 'Clip': { - const { child, w, h } = fields; - - if (w.kind !== 'int' || h.kind !== 'int') - throw new Error('Invalid Clip fields'); - - return { kind: 'clip', w: w.value, h: h.value, child: valueToUI(child) }; - } - - case 'Stack': { - const children = fields.children; - - if (children.kind !== 'list') - throw new Error('Invalid Stack fields'); - - return { kind: 'stack', children: children.elements.map(valueToUI) }; - } - - case 'Stateful': { - const { key, focusable, init, update, view } = fields; - - if (key.kind !== 'string') - throw new Error('Stateful key must be a string'); - - const isFocusable = focusable?.kind === 'constructor' && focusable.name === 'True'; - - return { - kind: 'stateful', - key: key.value, - focusable: isFocusable, - init, - update, - view - }; - } - - default: - throw new Error(`Unknown UI constructor: ${value.name}`); - } -}