diff --git a/src/cg/10-os.cg b/src/cg/10-os.cg index 835d281..5d5b33a 100644 --- a/src/cg/10-os.cg +++ b/src/cg/10-os.cg @@ -2,68 +2,107 @@ osState = { palette = { visible = True }, - inspector = { - visible = False, - name = "" - } + windows = [], + wm = { + focusedIndex = 0, + scrollOffset = 0 + }, + nextId = 0 }; -run = cmd \ - _ = debug "run" cmd; - result = eval cmd; - _ = debug "result" result; - result - | Defined name \ debug "defined" name - | Value v \ debug "result" v - | Err msg \ debug "error" msg - | _ \ noOp; - -onSelect = item \ - # for now, if it matches a store item, inspect it - # if not, eval it - res = storeSearch item; - zeroth = nth 0 res; - (and (len res > 0) (nth 0 res == Some item)) - | True \ inspect item - | _ \ run item; - -inspect = item \ +openWindow = title content \ + id = osState.nextId; batch [ - osState.palette.visible := False, - osState.inspector.visible := True, - osState.inspector.name := item + osState.nextId := id + 1, + osState.windows := osState.windows & [{ id = id, title = title, content = content, width = 400 }], + osState.palette.visible := False ]; -init = {}; +closeFocused = _ \ + i = osState.wm.focusedIndex; + batch [ + osState.windows := (take i osState.windows) & (drop (i + 1) osState.windows), + osState.wm.focusedIndex := max 0 (osState.wm.focusedIndex - 1) + ]; -update = state event \ event - | Key { key = "p", meta = True } \ - { state = state, emit = [osState.palette.visible := not (osState.palette.visible)] } - | _ \ { state = state, emit = [] }; +onSelect = input \ + result = eval input; + result + | Value v \ openWindow input (ui.text { content = show v, color = "white" }) + | Defined name \ noOp + | Err msg \ openWindow "Error" (ui.text { content = msg, color = "red" }); -view = state viewport \ +renderWindow = window isActive \ + _ = debug "renderWindow" window.title; + titleBarHeight = 30; ui.stack { children = [ - ui.rect { w = viewport.width, h = viewport.height, color = "#012" }, + # background + ui.rect { w = window.width, h = viewport.height, color = (isActive | True \ "#0a2a3a" | False \ "#061820")}, - osState.inspector.visible - | True \ inspector { - name = osState.inspector.name, - viewport = viewport, + # title bar + ui.positioned { + x = 0, y = 0, + child = ui.stack { + children = [ + ui.rect { w = window.width, h = titleBarHeight, color = (isActive | True \ "#a15f80" | False \ "#0f3348") }, + ui.positioned { x = 8, y = 8, child = ui.text { content = window.title, color = "white" } }, + ] } - | False \ empty, + }, - # keep palette at the end so it's on top - osState.palette.visible - | True \ palette { - # state = osState.palette, - # query = osState.palette.query, - search = storeSearch, - onSelect = onSelect, - viewport = viewport, + # content + ui.positioned { + x = 0, y = titleBarHeight, + child = ui.clip { + w = window.width, + h = viewport.height - titleBarHeight, + child = window.content } - | False \ empty, + } ] }; -os = { init = init, update = update, view = view } +renderWindows = _ \ + windows = osState.windows; + focused = osState.wm.focusedIndex; + ui.row { + gap = 1, + children = mapWithIndex (w i \ renderWindow w (i == focused)) windows + }; + +os = ui.stateful { + key = "os", + autoFocus = True, + + init = {}, + + update = state event \ event + | Key { key = "p", meta = True } \ + { state = state, emit = [osState.palette.visible := not (osState.palette.visible)] } + | Key { key = "ArrowLeft", meta = True } \ + { state = state, emit = [osState.wm.focusedIndex := max 0 (osState.wm.focusedIndex - 1)] } + | Key { key = "ArrowRight", meta = True } \ + { state = state, emit = [osState.wm.focusedIndex := min (len osState.windows - 1) (osState.wm.focusedIndex + 1)] } + | Key { key = "d", meta = True } \ + { state = state, emit = [closeFocused 0] } + | _ \ _ = debug "key" []; { state = state, emit = [] }, + + view = state \ + ui.stack { + children = [ + ui.rect { w = viewport.width, h = viewport.height, color = "#012" }, + + renderWindows 0, + + # keep palette at the end so it's on top + osState.palette.visible + | True \ palette { + search = storeSearch, + onSelect = onSelect, + viewport = viewport, + } + | False \ empty, + ] + } + }; diff --git a/src/compiler.ts b/src/compiler.ts index 47e09b4..dc7bc02 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,13 +1,15 @@ import type { AST, Pattern, Definition } from './ast'; -import { _rt, store } from './runtime-js'; +import { store } from './runtime-js'; let matchCounter = 0; 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()): string { +export function compile(ast: AST, useStore = true, bound = new Set(), topLevel = new Set()): string { switch (ast.kind) { case 'literal': if (ast.value.kind === 'string') @@ -18,61 +20,62 @@ export function compile(ast: AST, useStore = true, bound = new Set()): s case 'variable': { if (bound.has(ast.name)) { - return sanitizeName(ast.name); + return sanitizeName(ast.name); } - return sanitize(ast.name, useStore); + return sanitize(ast.name, useStore, topLevel); } case 'lambda': { const newBound = new Set([...bound, ...ast.params]); const params = ast.params.map(sanitizeName).join(') => ('); - return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound)}, { _ast: (${JSON.stringify(ast)}) })`; + const id = astIdCounter++; + return `Object.assign((${params}) => ${compile(ast.body, useStore, newBound, topLevel)}, { _astId: (${id}) })`; } case 'apply': // Constructor if (ast.func.kind === 'constructor') { const ctorName = ast.func.name; - const arg = compile(ast.args[0], useStore, bound); + const arg = compile(ast.args[0], useStore, bound, topLevel); return `((_a) => _a && typeof _a === 'object' && !Array.isArray(_a) && !_a._tag ? { _tag: "${ctorName}", ..._a } : { _tag: "${ctorName}", _0: _a })(${arg})`; } - const args = ast.args.map(a => compile(a, useStore, bound)).join(')('); - return `${compile(ast.func, useStore, bound)}(${args})`; + const args = ast.args.map(a => compile(a, useStore, bound, topLevel)).join(')('); + return `${compile(ast.func, useStore, bound, topLevel)}(${args})`; case 'record': { const parts = ast.entries.map(entry => entry.kind === 'spread' - ? `...${compile(entry.expr, useStore, bound)}` - : `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound)}` + ? `...${compile(entry.expr, useStore, bound, topLevel)}` + : `${sanitizeName(entry.key)}: ${compile(entry.value, useStore, bound, topLevel)}` ) return `({${parts.join(', ')}})`; } case 'list': { const elements = ast.elements.map(e => - 'spread' in e ? `...${compile(e.spread, useStore, bound)}` : compile(e, useStore, bound) + 'spread' in e ? `...${compile(e.spread, useStore, bound, topLevel)}` : compile(e, useStore, bound, topLevel) ); return `[${elements.join(', ')}]`; } case 'record-access': - return `${compile(ast.record, useStore, bound)}.${sanitizeName(ast.field)}`; + return `${compile(ast.record, useStore, bound, topLevel)}.${sanitizeName(ast.field)}`; case 'record-update': const updates = Object.entries(ast.updates) - .map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound)}`); - return `({...${compile(ast.record, useStore, bound)}, ${updates.join(', ')}})`; + .map(([k, v]) => `${sanitizeName(k)}: ${compile(v, useStore, bound, topLevel)}`); + return `({...${compile(ast.record, useStore, bound, topLevel)}, ${updates.join(', ')}})`; case 'let': const newBound = new Set([...bound, ast.name]); return `((${sanitizeName(ast.name)}) => - ${compile(ast.body, useStore, newBound)})(${compile(ast.value, useStore, bound)})`; + ${compile(ast.body, useStore, newBound, topLevel)})(${compile(ast.value, useStore, bound, topLevel)})`; case 'match': - return compileMatch(ast, useStore, bound); + return compileMatch(ast, useStore, bound, topLevel); case 'constructor': return `({ _tag: "${ast.name}" })`; @@ -85,10 +88,15 @@ export function compile(ast: AST, useStore = true, bound = new Set()): s case 'rebind': { const rootName = getRootName(ast.target); const path = getPath(ast.target); - const value = compile(ast.value, useStore, bound); + const value = compile(ast.value, useStore, bound, topLevel); if (!rootName) throw new Error('Rebind target must be a variable'); + if (bound.has(rootName)) { + const target = compile(ast.target, useStore, bound, topLevel); + return `(() => { ${target} = ${value}; return { _tag: "NoOp" }; })()`; + } + if (path.length === 0) { return `({ _tag: "Rebind", _0: "${rootName}", _1: ${value} })`; } else { @@ -102,23 +110,9 @@ export function compile(ast: AST, useStore = true, bound = new Set()): s } } -function sanitize(name: string, useStore = true): string { - const ops: Record = { - 'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul', 'div': '_rt.div', - 'mod': '_rt.mod', 'pow': '_rt.pow', - 'eq': '_rt.eq', 'gt': '_rt.gt', 'lt': '_rt.lt', 'gte': '_rt.gte', 'lte': '_rt.lte', - 'cat': '_rt.cat', 'max': '_rt.max', 'min': '_rt.min', - }; - - if (ops[name]) return ops[name]; - - const natives = ['eval', 'measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'undefine', 'batch', 'noOp', 'focus', 'ui']; - if (natives.includes(name)) return `_rt.${name}`; - - if (useStore) { - return `store.${sanitizeName(name)}`; - } - return sanitizeName(name); +function sanitize(name: string, useStore = true, topLevel: Set): string { + if (!useStore && topLevel.has(name)) return sanitizeName(name); + return `store[${JSON.stringify(name)}]` } function sanitizeName(name: string): string { @@ -135,8 +129,8 @@ function sanitizeName(name: string): string { return name.replace(/-/g, '_'); } -function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set()): string { - const expr = compile(ast.expr, useStore, bound); +function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new Set(), topLevel = new Set()): string { + const expr = compile(ast.expr, useStore, bound, topLevel); const tmpVar = `_m${matchCounter++}`; let code = `((${tmpVar}) => { `; @@ -149,7 +143,7 @@ function compileMatch(ast: AST & { kind: 'match'}, useStore = true, bound = new if (bindings.length > 0) { code += `const ${bindings.join(', ')}; `; } - code += `return ${compile(c.result, useStore, newBound)}; }`; + code += `return ${compile(c.result, useStore, newBound, topLevel)}; }`; } code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`; @@ -245,11 +239,11 @@ export function compileAndRun(defs: Definition[]) { } for (const def of defs) { - const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false)};`; + const compiled = `const ${sanitizeName(def.name)} = ${compile(def.body, false, new Set(), topLevel)};`; compiledDefs.push(compiled); try { - new Function('_rt', compiled); + new Function('store', compiled); } catch (e) { console.error(`=== BROKEN: ${def.name} ===`); console.error(compiled); @@ -263,8 +257,8 @@ export function compileAndRun(defs: Definition[]) { const code = `${compiledDefs.join('\n')} return { ${defNames}, __result: ${sanitizeName(lastName)} };`; - const fn = new Function('_rt', 'store', code); - const allDefs = fn(_rt, store); + const fn = new Function('store', code); + const allDefs = fn(store); // Populate store for (const [name, value] of Object.entries(allDefs)) { @@ -397,8 +391,8 @@ export function recompile(name: string, newAst: AST) { const ast = definitions.get(defName)!; const compiled = compile(ast); - const fn = new Function('_rt', 'store', `return ${compiled}`); - store[defName] = fn(_rt, store); + const fn = new Function('store', `return ${compiled}`); + store[defName] = fn(store); } } diff --git a/src/main.ts b/src/main.ts index 1c77c51..d01c35f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import { compileAndRun } from './compiler' import { tokenize } from './lexer' import { Parser } from './parser' import { runAppCompiled } from './runtime-compiled' -import { _rt, loadDefinitions, saveDefinitions } from './runtime-js' +import { _rt, store, loadDefinitions, saveDefinitions } from './runtime-js' const modules = import.meta.glob('./cg/*.cg', { query: 'raw', import: 'default', eager: true }); const cgCode = Object.keys(modules) @@ -13,20 +13,22 @@ const cgCode = Object.keys(modules) const canvas = document.createElement('canvas') as HTMLCanvasElement; document.body.appendChild(canvas); +// Populate store with natives +for (const [name, value] of Object.entries(_rt)) { + store[name] = value; +} + +store.viewport = { width: window.innerWidth, height: window.innerHeight }; + try { const tokens = tokenize(cgCode); const parser = new Parser(tokens, cgCode); const defs = parser.parse(); loadDefinitions(); - const os = compileAndRun(defs); + compileAndRun(defs); saveDefinitions(); - runAppCompiled( - { init: os.init, update: os.update, view: os.view }, - canvas, - _rt - ); - + runAppCompiled(canvas, store); } catch(error) { console.error(error); } diff --git a/src/runtime-compiled.ts b/src/runtime-compiled.ts index 0761bfb..7491402 100644 --- a/src/runtime-compiled.ts +++ b/src/runtime-compiled.ts @@ -2,20 +2,13 @@ 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; +export function runAppCompiled(canvas: HTMLCanvasElement, store: any) { const componentInstances = new Map(); let focusedComponentKey: string | null = null; @@ -132,13 +125,10 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { } function rerender() { - const viewport = { width: window.innerWidth, height: window.innerHeight }; const renderedKeys = new Set(); try { - const uiValue = app.view(state)(viewport); - const ui = uiValue; - const expandedUI = expandStateful(ui, [], renderedKeys); + const expandedUI = expandStateful(store.os, [], renderedKeys); // clean up unrendered instances for (const key of componentInstances.keys()) { @@ -148,7 +138,16 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { } if (focusedComponentKey && !renderedKeys.has(focusedComponentKey)) { + const parts = focusedComponentKey.split('.'); focusedComponentKey = null; + while (parts.length > 0) { + parts.pop(); + const ancestor = parts.join('.'); + if (ancestor && renderedKeys.has(ancestor)) { + focusedComponentKey = ancestor; + break; + } + } } render(expandedUI, canvas); @@ -182,7 +181,7 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { } if (event._tag === 'Rebind') { - rt.rebind(event._0, event._1, event._2); + store.rebind(event._0, event._1, event._2); return; } @@ -193,14 +192,6 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { if (event._tag === 'NoOp') return; - - const result = app.update(state)(event); - state = result.state; - if (result.emit && Array.isArray(result.emit)) { - for (const e of result.emit) { - handleEvent(e); - } - } } canvas.addEventListener('click', (e) => { @@ -251,9 +242,6 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { } } - // OS root - handleEvent(event); - e.preventDefault(); rerender(); }); @@ -263,6 +251,7 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { cancelAnimationFrame(resizeRAF); resizeRAF = requestAnimationFrame(() => { setupCanvas(); + store.viewport = { width: window.innerWidth, height: window.innerHeight }; rerender(); }); }); diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 614e723..925d5b9 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -1,6 +1,6 @@ import { tokenize } from './lexer' import { Parser } from './parser' -import { compile, recompile, definitions, freeVars, dependencies, dependents } from './compiler' +import { compile, recompile, definitions, freeVars, dependencies, dependents, astRegistry } from './compiler' import { prettyPrint } from './ast' import type { AST } from './ast' import { measure } from './ui'; @@ -61,7 +61,21 @@ export const _rt = { ? { _tag: 'Some', _0: xs[i] } : { _tag: 'None' }, len: (xs: any[] | string) => xs.length, - str: (x: any) => String(x), + // str: (x: any) => String(x), + show: (value: any): string => { + if (value === null || value === undefined) return "None"; + if (typeof value === 'string') return value; + if (typeof value === 'number') return String(value); + if (typeof value === 'boolean') return value ? "True" : "False"; + if (value._tag) return value._0 !== undefined ? `${value._tag} ${_rt.show(value._0)}` : value._tag; + if (Array.isArray(value)) return `[${value.map(_rt.show).join(", ")}]`; + if (typeof value === 'function') return ""; + if (typeof value === 'object') { + const entries = Object.entries(value).map(([k, v]) => `${k} = ${_rt.show(v)}`); + return `{ ${entries.join(", ")} }`; + } + return String(value); + }, chars: (s: string) => s.split(''), join: (delim: string) => (xs: string[]) => xs.join(delim), split: (delim: string) => (xs: string) => xs.split(delim), @@ -183,7 +197,7 @@ export function loadDefinitions() { try { const saved = JSON.parse(data); - for (const [name, source] of Object.entries(saved)) { + for (const [_, source] of Object.entries(saved)) { const tokens = tokenize(source as string); const parser = new Parser(tokens, source as string); const defs = parser.parse(); @@ -245,11 +259,11 @@ function valueToAst(value: any): AST { // Functions if (typeof value === 'function') { - if (value._ast) { - return value._ast; + if (value._astId !== undefined) { + return astRegistry.get(value._astId)!; } - throw new Error('Cannot serialize function without _ast'); + throw new Error('Cannot serialize function without _astId'); } throw new Error(`Cannot convert to AST: ${typeof value}`);