deleting interpreter now that we have compiler

master
Dustin Swan 3 weeks ago
parent 60c8f74d50
commit 6f7f06b748
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -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 // Literals and Variables
export type Literal = { export type Literal = {
kind: 'literal' kind: 'literal'
value: Value value: LiteralValue
line?: number line?: number
column?: number column?: number
start?: number start?: number
@ -198,8 +201,6 @@ export function prettyPrint(ast: AST, indent = 0): string {
return `${i}${val.value}`; return `${i}${val.value}`;
case 'string': case 'string':
return `${i}"${val.value}"`; return `${i}"${val.value}"`;
default:
return `${i}${val.kind}`;
} }
} }

@ -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;
}
}
}

@ -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 }

@ -1,3 +0,0 @@
import type { Value } from './types';
export type Env = Map<string, Value>

@ -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<string> } } = {};
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<string>();
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;
}
}
}

@ -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<string, any> = {};
for (const [name, entry] of store) {
data[name] = {
body: entry.body,
source: 'file'
};
}
// localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
export function loadStore(): Record<string, { body: AST, source: string }> | 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);
}

@ -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<string, Set<string>>) {
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<string, ComponentInstance>();
// 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<string> = 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<string>();
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<string> } },
visited: Set<string>,
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);
}
}
}

@ -1,56 +0,0 @@
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;
}

@ -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<string>
}
}
}
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;

@ -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 = { type ClickRegion = {
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
event: Value; event: any;
}; };
let clickRegions: ClickRegion[] = []; let clickRegions: ClickRegion[] = [];

@ -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`);
}
}

@ -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}`);
}
}
Loading…
Cancel
Save