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