we have reactivity

master
Dustin Swan 6 hours ago
parent accf1fd1dd
commit 84ef946281
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -2,6 +2,7 @@ import type { AST, Pattern } from './ast';
import type { Env } from './env'; import type { Env } from './env';
import type { Value } from './types'; import type { Value } from './types';
import { RuntimeError } from './error'; import { RuntimeError } from './error';
import { recordDependency } from './store';
export function evaluate(ast: AST, env: Env, source: string): Value { export function evaluate(ast: AST, env: Env, source: string): Value {
switch (ast.kind) { switch (ast.kind) {
@ -12,12 +13,9 @@ export function evaluate(ast: AST, env: Env, source: string): Value {
const val = env.get(ast.name); const val = env.get(ast.name);
if (val === undefined) if (val === undefined)
throw RuntimeError( throw RuntimeError( `Unknown variable: ${ast.name}`, ast.line, ast.column, source);
`Unknown variable: ${ast.name}`,
ast.line, recordDependency(ast.name);
ast.column,
source
);
return val; return val;
} }

@ -5,6 +5,9 @@ import { Parser } from './parser'
import { runApp } from './runtime'; import { runApp } from './runtime';
import { builtins } from './builtins'; import { builtins } from './builtins';
import { CGError } from './error'; import { CGError } from './error';
import { createStore, startTracking, stopTracking, buildDependents } from './store';
// import type { Store } from './store'
import stdlibCode from './stdlib.cg?raw'; import stdlibCode from './stdlib.cg?raw';
import designTokensCode from './design-tokens.cg?raw'; import designTokensCode from './design-tokens.cg?raw';
@ -26,17 +29,27 @@ try {
const tokens = tokenize(cgCode); const tokens = tokenize(cgCode);
const parser = new Parser(tokens, cgCode); const parser = new Parser(tokens, cgCode);
const definitions = parser.parse(); const definitions = parser.parse();
// console.log(ast);
const env: Env = new Map(Object.entries(builtins)); const env: Env = new Map(Object.entries(builtins));
const store = createStore();
for (const def of definitions) { for (const def of definitions) {
const deps = startTracking(def.name);
const value = evaluate(def.body, env, cgCode); const value = evaluate(def.body, env, cgCode);
stopTracking();
env.set(def.name, value); env.set(def.name, value);
store.set(def.name, {
value,
body: def.body,
dependencies: deps
});
} }
const dependents = buildDependents(store);
const appRecord = env.get('os'); const appRecord = env.get('os');
console.log("appRecord", appRecord);
if (!appRecord || appRecord.kind !== 'record') if (!appRecord || appRecord.kind !== 'record')
throw new Error('Expected record'); throw new Error('Expected record');
@ -45,10 +58,8 @@ try {
const update = appRecord.fields.update; const update = appRecord.fields.update;
const view = appRecord.fields.view; const view = appRecord.fields.view;
runApp({ init, update, view }, canvas, cgCode, env); runApp({ init, update, view }, canvas, cgCode, env, store, dependents);
} catch(error) { } catch(error) {
console.log('CAUGHT ERROR:', error);
console.log('Is CGError??', error instanceof CGError);
if (error instanceof CGError) { if (error instanceof CGError) {
console.error(error.format()); console.error(error.format());
} else { } else {

@ -4,6 +4,7 @@ import { render, hitTest } from './ui';
import { evaluate } from './interpreter'; import { evaluate } from './interpreter';
import { CGError } from './error'; import { CGError } from './error';
import type { Env } from './env'; import type { Env } from './env';
import type { Store } from './store';
export type App = { export type App = {
init: Value; init: Value;
@ -11,7 +12,7 @@ export type App = {
view: Value; // State / UI 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<string, Set<string>>) {
let state = app.init; let state = app.init;
type ComponentInstance = { type ComponentInstance = {
@ -52,6 +53,22 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
rerender(); 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) { function handleComponentEvent(componentKey: string, event: Value) {
const instance = componentInstances.get(componentKey); const instance = componentInstances.get(componentKey);
if (!instance) return; if (!instance) return;
@ -226,20 +243,29 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
if (event.args[0].kind !== 'string') return; if (event.args[0].kind !== 'string') return;
const name = event.args[0].value; const name = event.args[0].value;
let newValue: Value;
if (event.args.length === 2) { if (event.args.length === 2) {
// Rebind "name" value // Rebind "name" value
env.set(name, event.args[1]); newValue = event.args[1];
} else if (event.args.length === 3 && event.args[1].kind === 'list') { } else if (event.args.length === 3 && event.args[1].kind === 'list') {
// Rebind "name" ["path"] // 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 path = pathList.elements.map((e: Value) => e.kind === 'string' ? e.value : '');
const currentValue = env.get(name); const currentValue = env.get(name);
if (currentValue) { if (!currentValue) return;
const newValue = updatePath(currentValue, path, event.args[2]); newValue = updatePath(currentValue, path, event.args[2]);
env.set(name, newValue); } else {
} return;
} }
env.set(name, newValue);
const entry = store.get(name);
if (entry) {
entry.value = newValue;
}
recomputeDependents(name);
rerender(); rerender();
return; return;
} }

@ -0,0 +1,56 @@
import type { Value } from './types';
import type { AST } from './ast';
export type StoreEntry = {
value: Value,
body: AST,
dependencies: Set<string>;
};
export type Store = Map<string, StoreEntry>;
export function createStore(): Store {
return new Map();
}
let currentlyEvaluating: string | null = null;
let currentDependencies: Set<string> | null = null;
export function startTracking(name: string): Set<string> {
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<string, Set<string>> {
const dependents = new Map<string, Set<string>>();
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;
}

@ -5,6 +5,8 @@ testApp = {
password = "" password = ""
}; };
combinedText = testApp.email & " " & testApp.password;
update = state event \ event update = state event \ event
| _ \ state; | _ \ state;
@ -32,7 +34,8 @@ view = state viewport \
onChange = text \ testApp.password := text onChange = text \ testApp.password := text
}, },
Text { content = "Username: " & testApp.email, x = 8, y = 16 }, 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 }
] ]
} }
}; };

Loading…
Cancel
Save