compiling. interpreting was too slow

master
Dustin Swan 3 weeks ago
parent 6edf592637
commit 2cd5a609bb
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -0,0 +1,245 @@
import type { AST, Pattern, Definition } from './ast';
import { _rt, store } from './runtime-js';
export function compile(ast: AST): string {
switch (ast.kind) {
case 'literal':
if (ast.value.kind === 'string')
return JSON.stringify(ast.value.value);
if (ast.value.kind === 'int' || ast.value.kind === 'float')
return JSON.stringify(ast.value.value);
throw new Error(`Cannot compile literal of kind ${ast.value.kind}`);
case 'variable':
return sanitize(ast.name);
case 'lambda':
const params = ast.params.map(sanitize).join(') => (');
return `((${params}) => ${compile(ast.body)})`;
case 'apply':
// Constructor
if (ast.func.kind === 'constructor') {
const ctorName = ast.func.name;
const arg = compile(ast.args[0]);
return `((_a) => _a && typeof _a === 'object' && !Array.isArray(_a) && !_a._tag
? { _tag: "${ctorName}", ..._a }
: { _tag: "${ctorName}", _0: _a })(${arg})`;
}
const args = ast.args.map(compile).join(')(');
return `${compile(ast.func)}(${args})`;
case 'record': {
const fields = Object.entries(ast.fields)
.map(([k, v]) => `${sanitize(k)}: ${compile(v)}`);
return `({${fields.join(', ')}})`;
}
case 'list': {
const elements = ast.elements.map(e =>
'spread' in e ? `...${compile(e.spread)}` : compile(e)
);
return `[${elements.join(', ')}]`;
}
case 'record-access':
return `${compile(ast.record)}.${sanitize(ast.field)}`;
case 'record-update':
const updates = Object.entries(ast.updates)
.map(([k, v]) => `${sanitize(k)}: ${compile(v)}`);
return `({...${compile(ast.record)}, ${updates.join(', ')}})`;
case 'let':
return `((${sanitize(ast.name)}) =>
${compile(ast.body)})(${compile(ast.value)})`;
case 'match':
return compileMatch(ast);
case 'constructor':
return `({ _tag: "${ast.name}" })`;
/*
return `((arg) => arg && typeof arg === 'object' && !arg._tag
? { _tag: "${ast.name}", ...arg }
: { _tag: "${ast.name}", _0: arg })`;
*/
case 'rebind': {
if (ast.target.kind === 'variable') {
return `({ _tag: "Rebind", _0: "${ast.target.name}", _1: ${compile(ast.value)} })`;
} else 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') {
return `({ _tag: "Rebind", _0: "${current.name}", _1: ${JSON.stringify(path)}, _2: ${compile(ast.value)} })`;
}
}
throw new Error('Invalid rebind target');
}
default:
throw new Error(`Cannot compile ${ast.kind}`);
}
}
function sanitize(name: string): string {
const ops: Record<string, string> = {
'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul',
'div': '_rt.div', 'mod': '_rt.mod', 'eq': '_rt.eq',
'cat': '_rt.cat', 'gt': '_rt.gt', 'lt': '_rt.lt',
'gte': '_rt.gte', 'lte': '_rt.lte', 'max': '_rt.max', 'min': '_rt.min',
};
if (ops[name]) return ops[name];
const natives = ['measureText', 'storeSearch', 'debug', 'len', 'slice', 'str'];
if (natives.includes(name)) return `_rt.${name}`;
const reserved = [
'default','class','function','return','const','let','var',
'if','else','switch','case','for','while','do','break',
'continue','new','delete','typeof','in','this','super',
'import','export','extends','static','yield','await','async',
'try','catch','finally','throw','null','true','false'
];
if (reserved.includes(name)) return `_${name}`;
return name.replace(/-/g, '_');
}
function compileMatch(ast: AST & { kind: 'match'}): string {
const expr = compile(ast.expr);
const tmpVar = `_m${Math.floor(Math.random() * 10000)}`;
let code = `((${tmpVar}) => { `;
for (const c of ast.cases) {
const { condition, bindings } = compilePattern(c.pattern, tmpVar);
code += `if (${condition}) { `;
if (bindings.length > 0) {
code += `const ${bindings.join(', ')}; `;
}
code += `return ${compile(c.result)}; }`;
}
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
return code;
}
function compilePattern(pattern: Pattern, expr: string): { condition: string, bindings: string[] } {
switch (pattern.kind) {
case 'wildcard':
return { condition: 'true', bindings: [] };
case 'var':
return { condition: 'true', bindings: [`${sanitize(pattern.name)} = ${expr}`] };
case 'literal':
return { condition: `${expr} === ${JSON.stringify(pattern.value)}`, bindings: [] };
case 'constructor': {
let condition = `${expr}?._tag === "${pattern.name}"`;
const bindings: string[] = [];
pattern.args.forEach((argPattern, i) => {
const sub = compilePattern(argPattern, `${expr}._${i}`);
condition += ` && ${sub.condition}`;
bindings.push(...sub.bindings);
});
return { condition, bindings };
}
case 'list': {
let condition = `Array.isArray(${expr}) && ${expr}.length === ${pattern.elements.length}`;
const bindings: string[] = [];
pattern.elements.forEach((elemPattern, i) => {
const sub = compilePattern(elemPattern, `${expr}[${i}]`);
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
bindings.push(...sub.bindings);
});
return { condition, bindings };
}
case 'list-spread': {
let condition = `Array.isArray(${expr}) && ${expr}.length >= ${pattern.head.length}`;
const bindings: string[] = [];
pattern.head.forEach((elemPattern, i) => {
const sub = compilePattern(elemPattern, `${expr}[${i}]`);
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
bindings.push(...sub.bindings);
});
bindings.push(`${sanitize(pattern.spread)} = ${expr}.slice(${pattern.head.length})`);
return { condition, bindings };
}
case 'record': {
let condition = 'true';
const bindings: string[] = [];
for (const [field, fieldPattern] of Object.entries(pattern.fields)) {
const sub = compilePattern(fieldPattern, `${expr}.${sanitize(field)}`);
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
bindings.push(...sub.bindings);
}
return { condition, bindings };
}
default:
return { condition: 'true', bindings: [] };
}
}
export function compileAndRun(defs: Definition[]) {
const compiledDefs: string[] = [];
for (const def of defs) {
const compiled = `const ${sanitize(def.name)} = ${compile(def.body)};`;
compiledDefs.push(compiled);
try {
new Function('_rt', compiled);
} catch (e) {
console.error(`=== BROKEN: ${def.name} ===`);
console.error(compiled);
throw e;
}
}
/*
const compiledDefs = defs.map(def =>
`const ${sanitize(def.name)} = ${compile(def.body)};`
).join('\n');
*/
const lastName = defs[defs.length - 1].name;
const defNames = defs.map(d => sanitize(d.name)).join(', ');
const code = `${compiledDefs.join('\n')}
return { ${defNames}, __result: ${sanitize(lastName)} };`;
// console.log('--- Compiled Code ---');
// console.log(code);
// console.log('=====================');
const fn = new Function('_rt', code);
const allDefs = fn(_rt);
// Populate store
for (const [name, value] of Object.entries(allDefs)) {
if (name !== '__result') {
store[name] = value;
}
}
return allDefs.__result;
}

@ -1,26 +1,23 @@
import type { Env } from './env'
import { evaluate } from './interpreter'
import { compileAndRun } from './compiler'
import { tokenize } from './lexer'
import { Parser } from './parser'
import { runApp } from './runtime';
import { builtins } from './builtins';
import { CGError } from './error';
import { createStore, startTracking, stopTracking, buildDependents } from './store';
import { loadStore, clearStore } from './persistence';
import { runAppCompiled } from './runtime-compiled'
import { _rt } from './runtime-js'
import stdlibCode from './stdlib.cg?raw';
import designTokensCode from './design-tokens.cg?raw';
import uiComponentsCode from './ui-components.cg?raw';
import osCode from './os.cg?raw';
const canvas = document.createElement('canvas') as HTMLCanvasElement;
document.body.appendChild(canvas);
/*
const clearButton = document.getElementById('clear-storage');
if (clearButton) {
clearButton.onclick = () => clearStore();
}
*/
const cgCode = stdlibCode + '\n' +
designTokensCode + '\n' +
@ -30,8 +27,16 @@ const cgCode = stdlibCode + '\n' +
try {
const tokens = tokenize(cgCode);
const parser = new Parser(tokens, cgCode);
const definitions = parser.parse();
const defs = parser.parse();
const os = compileAndRun(defs);
console.log("Compiled os:", os);
runAppCompiled(
{ init: os.init, update: os.update, view: os.view },
canvas,
_rt
);
/*
const env: Env = new Map(Object.entries(builtins));
const store = createStore();
@ -76,10 +81,9 @@ try {
const view = appRecord.fields.view;
runApp({ init, update, view }, canvas, cgCode, env, store, dependents);
*/
} catch(error) {
if (error instanceof CGError) {
console.error(error.format());
} else {
throw error;
}
console.error(error);
}

@ -20,13 +20,13 @@ listRow = config \
child = Stack {
children = [
Rect { w = config.w, h = config.h, color = color },
centerV config.h (
# centerV config.h (
Positioned {
x = 10,
y = 0,
y = 10,
child = Text { content = config.child, color = "white" }
}
)
# )
]
}
};
@ -34,6 +34,8 @@ listRow = config \
palette = state viewport \
results = take 10 (storeSearch osState.palette.query);
_ = debug "palette results" results;
state = osState.palette;
padding = 0;

@ -11,7 +11,7 @@ export function saveStore(store: Store) {
source: 'file'
};
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
// localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
export function loadStore(): Record<string, { body: AST, source: string }> | null {

@ -0,0 +1,227 @@
// import type { UIValue } from './types';
import { valueToUI } from './valueToUI-compiled';
import { render, hitTest } from './ui';
type UIValue = any;
type App = {
init: any;
update: (state: any) => (event: any) => any;
view: (state: any) => (viewport: any) => any;
}
type ComponentInstance = {
state: any;
update: (state: any) => (event: any) => any;
view: (state: any) => any;
};
export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) {
let state = app.init;
const componentInstances = new Map<string, ComponentInstance>();
let focusedComponentKey: string | null = null;
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 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, { _tag: 'Blurred' });
}
// Focus event to the new
if (componentKey && componentInstances.has(componentKey)) {
handleComponentEvent(componentKey, { name: 'Focused' });
}
rerender();
}
function handleComponentEvent(componentKey: string, event: any) {
const instance = componentInstances.get(componentKey);
if (!instance) return;
try {
const result = instance.update(instance.state)(event);
instance.state = result.state;
if (result.emit && Array.isArray(result.emit)) {
for (const e of result.emit) {
handleEvent(e);
}
}
rerender();
} catch(error) {
console.error('Component event error:', 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) {
console.log('Creating stateful', fullKey);
console.log('ui.init:', ui.init);
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;
}
console.log('Instance state', instance.state);
const viewResult = instance.view(instance.state);
let viewUI = valueToUI(viewResult);
if (ui.focusable) {
viewUI = {
kind: 'clickable',
child: viewUI,
event: { _tag: 'FocusAndClick', _0: 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 rerender() {
const viewport = { width: window.innerWidth, height: window.innerHeight };
try {
const uiValue = app.view(state)(viewport);
const ui = valueToUI(uiValue);
const expandedUI = expandStateful(ui, []);
render(expandedUI, canvas);
} catch (error) {
console.error('Render error:', error);
}
}
function handleEvent(event: any) {
if (!event || !event._tag) return;
if (event._tag === 'Batch' && event._0) {
for (const e of event._0) {
handleEvent(e);
}
return;
}
if (event._tag === 'FocusAndClick') {
const componentKey = event._0;
const coords = event._1;
setFocus(componentKey);
handleComponentEvent(componentKey, { _tag: 'Clicked', _0: coords });
return;
}
if (event._tag === 'Rebind') {
rt.rebind(event._0, event._1, event._2);
rerender();
return;
}
if (event._tag === 'Focus') {
setFocus(event._0);
return;
}
if (event._tag === 'NoOp')
return;
state = app.update(state)(event);
rerender();
}
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._tag === 'FocusAndClick') {
handleEvent({
_tag: 'FocusAndClick',
_0: event._0,
_1: { x: Math.floor(relativeX), y: Math.floor(relativeY) }
});
} else {
handleEvent(event);
}
}
});
window.addEventListener('keydown', (e) => {
let event: any;
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
event = { _tag: 'Char', _0: e.key };
} else {
event = { _tag: e.key };
}
if (focusedComponentKey) {
handleComponentEvent(focusedComponentKey, event);
} else {
handleEvent(event);
}
e.preventDefault();
});
window.addEventListener('resize', () => {
setupCanvas();
rerender();
})
rerender();
}

@ -0,0 +1,56 @@
export const store: Record<string, any> = {};
export const _rt = {
add: (a: number) => (b: number) => a + b,
sub: (a: number) => (b: number) => a - b,
mul: (a: number) => (b: number) => a * b,
div: (a: number) => (b: number) => a / b,
mod: (a: number) => (b: number) => a % b,
cat: (a: string) => (b: string) => a + b,
max: (a: number) => (b: number) => Math.max(a, b),
min: (a: number) => (b: number) => Math.min(a, b),
eq: (a: any) => (b: any) => ({ _tag: a === b ? 'True' : 'False' }),
neq: (a: any) => (b: any) => ({ _tag: a !== b ? 'True' : 'False' }),
gt: (a: any) => (b: any) => ({ _tag: a > b ? 'True' : 'False' }),
lt: (a: any) => (b: any) => ({ _tag: a < b ? 'True' : 'False' }),
gte: (a: any) => (b: any) => ({ _tag: a >= b ? 'True' : 'False' }),
lte: (a: any) => (b: any) => ({ _tag: a <= b ? 'True' : 'False' }),
len: (list: any[]) => list.length,
str: (x: any) => String(x),
slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end),
debug: (label: string) => (value: any) => { console.log(label, value); return value; },
measureText: (text: string) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.font = '16px "SF Mono", "Monaco", "Menlo", monospace';
return Math.floor(ctx.measureText(text).width);
}
return text.length * 10; // fallback
},
storeSearch: (query: string) => {
const results: string[] = [];
const searchTerm = query.toLowerCase();
console.log("in storeSearch. query: ", searchTerm);
for (const name of Object.keys(store)) {
if (searchTerm === '' || name.toLowerCase().includes(searchTerm)) {
results.push(name);
}
}
return results;
},
rebind: (name: string, pathOrValue: any, maybeValue?: any) => {
if (maybeValue === undefined) {
store[name] = pathOrValue;
} else {
const path = pathOrValue as string[];
let obj = store[name];
for (let i = 0; i < path.length - 1; i++) {
obj = obj[path[i]];
}
obj[path[path.length - 1]] = maybeValue;
}
},
}

@ -31,8 +31,7 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb
if (ui.radius && ui.radius > 0) {
const r = Math.min(ui.radius, ui.w / 2, ui.h / 2);
const inset = ui.strokeWidth ? ui.strokeWidth / 2 : 0;
// TODO
// const inset = ui.strokeWidth ? ui.strokeWidth / 2 : 0; TODO
ctx.beginPath();
ctx.moveTo(x + r, y);
@ -201,7 +200,7 @@ export function measure(ui: UIValue): { width: number, height: number } {
}
}
export function hitTest(x: number, y: number): { event: Value, relativeX: number, relativeY: number } | null {
export function hitTest(x: number, y: number): { event: any, relativeX: number, relativeY: number } | null {
for (const region of clickRegions) {
if (x >= region.x && x < region.x + region.width &&
y >= region.y && y < region.y + region.height) {

@ -0,0 +1,96 @@
// import type { UIValue } from './types'
export function valueToUI(value: any): any {
if (!value || !value._tag)
throw new Error(`Expected UI constructor, got: ${JSON.stringify(value)}`);
switch(value._tag) {
case 'Rect':
return {
kind: 'rect',
w: value.w,
h: value.h,
color: value.color,
radius: value.radius,
strokeWidth: value.strokeWidth,
strokeColor: value.strokeColor,
};
case 'Text':
return {
kind: 'text',
content: value.content,
color: value.color,
};
case 'Row':
return {
kind: 'row',
gap: value.gap || 0,
children: value.children.map(valueToUI),
};
case 'Column':
return {
kind: 'column',
gap: value.gap || 0,
children: value.children.map(valueToUI),
};
case 'Stack':
return {
kind: 'stack',
children: value.children.map(valueToUI),
};
case 'Positioned':
return {
kind: 'positioned',
x: value.x || 0,
y: value.p || 0,
child: valueToUI(value.child),
};
case 'Padding':
return {
kind: 'padding',
amount: value.amount || 0,
child: valueToUI(value.child),
};
case 'Clickable':
return {
kind: 'clickable',
event: value.event,
child: valueToUI(value.child),
};
case 'Clip':
return {
kind: 'clip',
w: value.w,
h: value.h,
child: valueToUI(value.child),
};
case 'Opacity':
return {
kind: 'opacity',
opacity: value.opacity,
child: valueToUI(value.child),
};
case 'Stateful':
return {
kind: 'stateful',
key: value.key,
focusable: value.focusable,
init: value.init,
update: value.update,
view: value.view,
};
default:
throw new Error(`Unknown UI constructor: ${value._tag}`);
}
}
Loading…
Cancel
Save