deleting interpreter now that we have compiler
parent
60c8f74d50
commit
6f7f06b748
@ -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,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…
Reference in New Issue