we have reactivity
This commit is contained in:
parent
accf1fd1dd
commit
84ef946281
5 changed files with 113 additions and 19 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
src/main.ts
21
src/main.ts
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
src/store.ts
Normal file
56
src/store.ts
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue