diff --git a/src/cg/06-inspector.cg b/src/cg/06-inspector.cg index 48141c3..6ce2198 100644 --- a/src/cg/06-inspector.cg +++ b/src/cg/06-inspector.cg @@ -24,9 +24,7 @@ inspector = config \ w = contentWidth, h = textInputHeight, # onChange = text \ batch [config.state.query := text, config.state.focusedIndex := 0], - onChange = text \ batch [], - onKeyDown = key \ key - | _ \ noOp + onChange = text \ batch [] } ) sourceLines }; diff --git a/src/compiler.ts b/src/compiler.ts index 324f510..d63cf79 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -3,13 +3,20 @@ import { store } from './runtime-js'; let matchCounter = 0; +type CompileCtx = { + useStore: boolean; + bound: Set; + topLevel: Set; +}; +const defaultCtx: CompileCtx = { useStore: true, bound: new Set(), topLevel: new Set() }; + export const definitions: Map = new Map(); export const dependencies: Map> = new Map(); export const dependents: Map> = new Map(); export const astRegistry = new Map(); let astIdCounter = 0; -export function compile(ast: AST, useStore = true, bound = new Set(), topLevel = new Set()): string { +export function compile(ast: AST, ctx: CompileCtx = defaultCtx): string { switch (ast.kind) { case 'literal': if (ast.value.kind === 'string') @@ -19,80 +26,78 @@ export function compile(ast: AST, useStore = true, bound = new Set(), to throw new Error(`Cannot compile literal`); // of kind ${ast.value.kind}`); case 'variable': { - if (bound.has(ast.name)) { + if (ctx.bound.has(ast.name)) { return sanitizeName(ast.name); } - return sanitize(ast.name, useStore, topLevel); + return sanitize(ast.name, ctx); } case 'lambda': { - const newBound = new Set([...bound, ...ast.params]); + const newBound = new Set([...ctx.bound, ...ast.params]); + const newCtx = { ...ctx, bound: newBound }; const params = ast.params.map(sanitizeName).join(') => ('); const id = astIdCounter++; astRegistry.set(id, ast); - return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound, topLevel)}, { _astId: (${id}) })`; + return `Object.assign((${params}) => ${compile(ast.body, newCtx)}, { _astId: (${id}) })`; } case 'apply': // Constructor if (ast.func.kind === 'constructor') { const ctorName = ast.func.name; - const arg = compile(ast.args[0], useStore, bound, topLevel); + const arg = compile(ast.args[0], ctx); return `({ _tag: "${ctorName}", _0: ${arg} })`; } - const args = ast.args.map(a => compile(a, useStore, bound, topLevel)).join(')('); - return `${compile(ast.func, useStore, bound, topLevel)}(${args})`; + const args = ast.args.map(a => compile(a, ctx)).join(')('); + return `${compile(ast.func, ctx)}(${args})`; case 'record': { const parts = ast.entries.map(entry => entry.kind === 'spread' - ? `...${compile(entry.expr, useStore, bound, topLevel)}` - : `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound, topLevel)}` + ? `...${compile(entry.expr, ctx)}` + : `${sanitizeName(entry.key)}: ${compile(entry.value, ctx)}` ) return `({${parts.join(', ')}})`; } case 'list': { const elements = ast.elements.map(e => - 'spread' in e ? `...${compile(e.spread, useStore, bound, topLevel)}` : compile(e, useStore, bound, topLevel) + 'spread' in e ? `...${compile(e.spread, ctx)}` : compile(e, ctx) ); return `[${elements.join(', ')}]`; } case 'record-access': - return `${compile(ast.record, useStore, bound, topLevel)}.${sanitizeName(ast.field)}`; + return `${compile(ast.record, ctx)}.${sanitizeName(ast.field)}`; case 'record-update': const updates = Object.entries(ast.updates) - .map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound, topLevel)}`); - return `({...${compile(ast.record, useStore, bound, topLevel)}, ${updates.join(', ')}})`; + .map(([k, v]) => `${sanitizeName(k)}: ${compile(v, ctx)}`); + return `({...${compile(ast.record, ctx)}, ${updates.join(', ')}})`; - case 'let': - const newBound = new Set([...bound, ast.name]); - return `(() => { let ${sanitizeName(ast.name)} = ${compile(ast.value, useStore, newBound, topLevel)}; - return ${compile(ast.body, useStore, newBound, topLevel)}; })()`; + case 'let': { + const newBound = new Set([...ctx.bound, ast.name]); + const newCtx = { ...ctx, bound: newBound }; + return `(() => { let ${sanitizeName(ast.name)} = ${compile(ast.value, newCtx)}; + return ${compile(ast.body, newCtx)}; })()`; + } case 'match': - return compileMatch(ast, useStore, bound, topLevel); + return compileMatch(ast, ctx); 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': { const rootName = getRootName(ast.target); const path = getPath(ast.target); - const value = compile(ast.value, useStore, bound, topLevel); + const value = compile(ast.value, ctx); if (!rootName) throw new Error('Rebind target must be a variable'); - if (bound.has(rootName)) { - const target = compile(ast.target, useStore, bound, topLevel); + if (ctx.bound.has(rootName)) { + const target = compile(ast.target, ctx); return `(() => { ${target} = ${value}; return { _tag: "NoOp" }; })()`; } @@ -109,7 +114,7 @@ export function compile(ast: AST, useStore = true, bound = new Set(), to } } -function sanitize(name: string, useStore = true, topLevel: Set): string { +function sanitize(name: string, { useStore, topLevel }: CompileCtx): string { if (!useStore && topLevel.has(name)) return sanitizeName(name); return `store[${JSON.stringify(name)}]` } @@ -128,8 +133,8 @@ function sanitizeName(name: string): string { return name.replace(/-/g, '_'); } -function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set(), topLevel = new Set()): string { - const expr = compile(ast.expr, useStore, bound, topLevel); +function compileMatch(ast: AST & { kind: 'match'}, ctx: CompileCtx): string { + const expr = compile(ast.expr, ctx); const tmpVar = `_m${matchCounter++}`; let code = `((${tmpVar}) => { `; @@ -137,12 +142,13 @@ function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new for (const c of ast.cases) { const { condition, bindings } = compilePattern(c.pattern, tmpVar); const patternBound = patternVars(c.pattern); - const newBound = new Set([...bound, ...patternBound]); + const newBound = new Set([...ctx.bound, ...patternBound]); + const newCtx = { ...ctx, bound: newBound }; code += `if (${condition}) { `; if (bindings.length > 0) { code += `const ${bindings.join(', ')}; `; } - code += `return ${compile(c.result, useStore, newBound, topLevel)}; }`; + code += `return ${compile(c.result, newCtx)}; }`; } code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`; @@ -238,7 +244,8 @@ export function compileAndRun(defs: Definition[]) { } for (const def of defs) { - const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false, new Set(), topLevel)};`; + const ctx: CompileCtx = { useStore: false, topLevel, bound: new Set() }; + const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, ctx)};`; compiledDefs.push(compiled); try { diff --git a/src/parser.ts b/src/parser.ts index a984fd3..4ba3717 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -202,6 +202,22 @@ export class Parser { return expr; } + private parseCommaSeparated(closeToken: Token['kind'], parseItem: () => T): T[] { + const items: T[] = []; + let first = true; + + while (this.current().kind !== closeToken) { + if (!first) { + this.expect('comma'); + if (this.current().kind === closeToken) break; // trailing commas + } + first = false; + items.push(parseItem()); + } + + return items; + } + private parseMatch(expr: AST): AST { const token = this.current(); const cases: MatchCase[] = []; @@ -288,23 +304,16 @@ export class Parser { // Record if (token.kind === 'open-brace') { this.advance(); - const fields: { [key: string]: Pattern } = {}; - let first = true; - - while (this.current().kind !== 'close-brace') { - if (!first) { - this.expect('comma'); - if (this.current().kind === 'close-brace') break; // trailing commas - } - first = false; - + const items = this.parseCommaSeparated('close-brace', () => { const keyToken = this.expect('ident'); const key = (keyToken as { value: string }).value; this.expect('equals'); - fields[key] = this.parsePattern(); - } + return { key, pattern: this.parsePattern() }; + }); this.expect('close-brace'); + const fields: { [key: string]: Pattern } = {}; + for (const item of items) fields[item.key] = item.pattern; return { kind: 'record', fields }; } @@ -429,23 +438,17 @@ export class Parser { if (this.current().kind === 'open-brace') { // Record update this.advance(); - const updates: { [key: string]: AST } = {}; - let first = true; - - while (this.current().kind !== 'close-brace') { - if (!first) { - this.expect('comma'); - if (this.current().kind === 'close-brace') break; // trailing commas - } - first = false; + const items = this.parseCommaSeparated('close-brace', () => { const keyToken = this.expect('ident'); const key = (keyToken as { value: string }).value; this.expect('equals'); - updates[key] = this.parseExpression(); - } + return { key, value: this.parseExpression() }; + }); this.expect('close-brace'); + const updates: { [key: string]: AST } = {}; + for (const item of items) updates[item.key] = item.value; expr = { kind: 'record-update', record: expr, updates, ...this.getPos(token) } } else { @@ -475,26 +478,15 @@ export class Parser { if (token.kind === 'open-bracket') { this.advance(); - const items: AST[] = []; - let first = true; - - while (this.current().kind !== 'close-bracket') { - if (!first) { - this.expect('comma'); - if (this.current().kind === 'close-bracket') break; // trailing commas - } - first = false; - + const items = this.parseCommaSeparated('close-bracket', () => { // Spread if (this.current().kind === 'dot-dot-dot') { const spreadToken = this.current(); this.advance(); - const expr = this.parseExpression(); - items.push({ kind: 'list-spread', spread: expr, ...this.getPos(spreadToken) }) - } else { - items.push(this.parseExpression()); + return { kind: 'list-spread' as const, spread: this.parseExpression(), ...this.getPos(spreadToken) }; } - } + return this.parseExpression(); + }); this.expect('close-bracket'); return { kind: 'list', elements: items, ...this.getPos(token) }; @@ -502,28 +494,16 @@ export class Parser { if (token.kind === 'open-brace') { this.advance(); - - const entries: Array<{ kind: 'field', key: string, value: AST } | { kind: 'spread', expr: AST }> = []; - let first = true; - - while (this.current().kind !== 'close-brace') { - if (!first) { - this.expect('comma'); - if (this.current().kind === 'close-brace') break; // trailing commas - } - first = false; - + const entries = this.parseCommaSeparated('close-brace', () => { if (this.current().kind === 'dot-dot-dot') { this.advance(); - const expr = this.parseExpression(); - entries.push({ kind: 'spread', expr }); - } else { - const keyToken = this.expect('ident'); - const key = (keyToken as { value: string }).value; - this.expect('equals'); - entries.push({ kind: 'field', key, value: this.parseExpression() }); + return { kind: 'spread' as const, expr: this.parseExpression() }; } - } + const keyToken = this.expect('ident'); + const key = (keyToken as { value: string }).value; + this.expect('equals'); + return { kind: 'field' as const, key, value: this.parseExpression() }; + }); this.expect('close-brace'); return { kind: 'record', entries, ...this.getPos(token) }; diff --git a/src/runtime-compiled.ts b/src/runtime-compiled.ts index 4081f13..616b9c7 100644 --- a/src/runtime-compiled.ts +++ b/src/runtime-compiled.ts @@ -262,16 +262,6 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) { handleEvent(hit.onScroll(delta)); } - /* - dispatchToFocused({ - _tag: 'Scroll', - _0: { - deltaX: Math.round(e.deltaX), - deltaY: Math.round(e.deltaY) - } - }); - */ - e.preventDefault(); rerender(); }); diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 17a968a..6f2d242 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -117,7 +117,7 @@ export const _rt = { const tokens = tokenize(`_tmp = ${code};`); const parser = new Parser(tokens, ""); const defs = parser.parse(); - recompile(name, defs[0]. body); + recompile(name, defs[0].body); return { _tag: 'Ok' }; }, undefine: (name: string) => { diff --git a/src/ui.ts b/src/ui.ts index a6d6016..1601ff1 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -249,29 +249,27 @@ export function _measure(ui: UIValue): { width: number, height: number } { } } -export function hitTest(x: number, y: number): { onClick: any, relativeX: number, relativeY: number } | null { - for (let i = clickRegions.length - 1; i >= 0; i--) { - const region = clickRegions[i]; +function findRegion(regions: T[], x: number, y: number ): T | null { + for (let i = regions.length - 1; i >= 0; i--) { + const r = regions[i]; - if (x >= region.x && x < region.x + region.width && - y >= region.y && y < region.y + region.height) { - return { - onClick: region.onClick, - relativeX: x - region.x, - relativeY: y - region.y, - }; + if (x >= r.x && x < r.x + r.width && + y >= r.y && y < r.y + r.height) { + return r; } } return null; + +} + +export function hitTest(x: number, y: number): { onClick: any, relativeX: number, relativeY: number } | null { + const region = findRegion(clickRegions, x, y); + if (!region) return null; + return { onClick: region.onClick, relativeX: x - region.x, relativeY: y - region.y }; } export function scrollHitTest(x: number, y: number): { onScroll: any } | null { - for (let i = scrollRegions.length - 1; i >= 0; i--) { - const region = scrollRegions[i]; - if (x >= region.x && x < region.x + region.width && - y >= region.y && y < region.y + region.height) { - return { onScroll: region.onScroll }; - } - } - return null; + const region = findRegion(scrollRegions, x, y); + if (!region) return null; + return { onScroll: region.onScroll }; }