From 84ef94628125ff950c2cf51a144cad7074a4d32f Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Fri, 6 Feb 2026 21:03:59 -0700 Subject: [PATCH] we have reactivity --- src/interpreter.ts | 10 ++++---- src/main.ts | 21 ++++++++++++---- src/runtime.ts | 40 +++++++++++++++++++++++++------ src/store.ts | 56 +++++++++++++++++++++++++++++++++++++++++++ src/textinput-test.cg | 5 +++- 5 files changed, 113 insertions(+), 19 deletions(-) create mode 100644 src/store.ts diff --git a/src/interpreter.ts b/src/interpreter.ts index a87b424..a753910 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -2,6 +2,7 @@ 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) { @@ -12,12 +13,9 @@ export function evaluate(ast: AST, env: Env, source: string): Value { const val = env.get(ast.name); if (val === undefined) - throw RuntimeError( - `Unknown variable: ${ast.name}`, - ast.line, - ast.column, - source - ); + throw RuntimeError( `Unknown variable: ${ast.name}`, ast.line, ast.column, source); + + recordDependency(ast.name); return val; } diff --git a/src/main.ts b/src/main.ts index 12a3c26..90d4211 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,9 @@ import { Parser } from './parser' import { runApp } from './runtime'; import { builtins } from './builtins'; import { CGError } from './error'; +import { createStore, startTracking, stopTracking, buildDependents } from './store'; +// import type { Store } from './store' + import stdlibCode from './stdlib.cg?raw'; import designTokensCode from './design-tokens.cg?raw'; @@ -26,17 +29,27 @@ try { const tokens = tokenize(cgCode); const parser = new Parser(tokens, cgCode); const definitions = parser.parse(); - // console.log(ast); const env: Env = new Map(Object.entries(builtins)); + const store = createStore(); for (const def of definitions) { + const deps = startTracking(def.name); const value = evaluate(def.body, env, cgCode); + stopTracking(); + env.set(def.name, value); + + store.set(def.name, { + value, + body: def.body, + dependencies: deps + }); } + const dependents = buildDependents(store); + const appRecord = env.get('os'); - console.log("appRecord", appRecord); if (!appRecord || appRecord.kind !== 'record') throw new Error('Expected record'); @@ -45,10 +58,8 @@ try { const update = appRecord.fields.update; const view = appRecord.fields.view; - runApp({ init, update, view }, canvas, cgCode, env); + runApp({ init, update, view }, canvas, cgCode, env, store, dependents); } catch(error) { - console.log('CAUGHT ERROR:', error); - console.log('Is CGError??', error instanceof CGError); if (error instanceof CGError) { console.error(error.format()); } else { diff --git a/src/runtime.ts b/src/runtime.ts index aaaf284..b0d2bb6 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -4,6 +4,7 @@ import { render, hitTest } from './ui'; import { evaluate } from './interpreter'; import { CGError } from './error'; import type { Env } from './env'; +import type { Store } from './store'; export type App = { init: Value; @@ -11,7 +12,7 @@ export type App = { view: Value; // State / UI } -export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: Env) { +export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: Env, store: Store, dependents: Map>) { let state = app.init; type ComponentInstance = { @@ -52,6 +53,22 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: rerender(); } + function recomputeDependents(changedName: string) { + const toRecompute = dependents.get(changedName); + if (!toRecompute) return; + + for (const depName of toRecompute) { + const entry = store.get(depName); + if (entry) { + const newValue = evaluate(entry.body, env, source); + env.set(depName, newValue); + entry.value = newValue; + + recomputeDependents(depName); + } + } + } + function handleComponentEvent(componentKey: string, event: Value) { const instance = componentInstances.get(componentKey); if (!instance) return; @@ -226,20 +243,29 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env: if (event.args[0].kind !== 'string') return; const name = event.args[0].value; + let newValue: Value; if (event.args.length === 2) { // Rebind "name" value - env.set(name, event.args[1]); + 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 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) { - const newValue = updatePath(currentValue, path, event.args[2]); - env.set(name, newValue); - } + if (!currentValue) return; + newValue = updatePath(currentValue, path, event.args[2]); + } else { + return; } + env.set(name, newValue); + const entry = store.get(name); + if (entry) { + entry.value = newValue; + } + + recomputeDependents(name); + rerender(); return; } diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..5afc90b --- /dev/null +++ b/src/store.ts @@ -0,0 +1,56 @@ +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/textinput-test.cg b/src/textinput-test.cg index a6d85f0..b543260 100644 --- a/src/textinput-test.cg +++ b/src/textinput-test.cg @@ -5,6 +5,8 @@ testApp = { password = "" }; +combinedText = testApp.email & " " & testApp.password; + update = state event \ event | _ \ state; @@ -32,7 +34,8 @@ view = state viewport \ onChange = text \ testApp.password := text }, Text { content = "Username: " & testApp.email, x = 8, y = 16 }, - Text { content = "Password: " & testApp.password, x = 8, y = 16 } + Text { content = "Password: " & testApp.password, x = 8, y = 16 }, + Text { content = "Combined: " & combinedText, x = 8, y = 16 } ] } };