From 8bc05efa1eb0f35ced1110cbb00be1e9986d176f Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Tue, 10 Feb 2026 16:46:31 -0700 Subject: [PATCH] Fixing pretty print ast. adding a few more builtins :( getting further with inspector --- src/ast.ts | 80 +++++++++++++++++++++++++------------- src/cg/01-stdlib.cg | 11 ++++-- src/cg/03-ui-components.cg | 68 +++++++++++++++++++++++--------- src/cg/05-palette.cg | 77 ++++++++++++++++++++++++++++-------- src/cg/06-inspector.cg | 44 +++++++++++++++++++++ src/cg/10-os.cg | 26 ++++++++++++- src/compiler.ts | 19 ++++++--- src/parser.ts | 18 ++++++--- src/runtime-js.ts | 11 +++++- 9 files changed, 272 insertions(+), 82 deletions(-) create mode 100644 src/cg/06-inspector.cg diff --git a/src/ast.ts b/src/ast.ts index 0cecb81..f4c4722 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -109,7 +109,11 @@ export type List = { export type Record = { kind: 'record' - fields: { [key: string]: AST } + entries: Array< + | { kind: 'field', key: string, value: AST } + | { kind: 'spread', expr: AST } + > + // fields: { [key: string]: AST } line?: number column?: number start?: number @@ -190,61 +194,77 @@ export type AST = | Rebind export function prettyPrint(ast: AST, indent = 0): string { - const i = ' '.repeat(indent); + const i = ' '.repeat(indent); switch (ast.kind) { case 'literal': { const val = ast.value; - switch (val.kind) { - case 'int': - case 'float': - return `${i}${val.value}`; - case 'string': - return `${i}"${val.value}"`; + if (val.kind === 'string') { + return `${i}"${val.value}"`; } + return `${i}${val.value}`; } case 'variable': - return `${i}${ast.name}`; - case 'constructor': - return `${i}${ast.name}`; + return ast.name; case 'apply': const func = prettyPrint(ast.func, 0); const args = ast.args.map(a => prettyPrint(a, 0)).join(' '); - return `${i}(${func} ${args})` + return `(${func} ${args})` case 'let': - return `${i}let ${ast.name} = \n${prettyPrint(ast.value, indent + 1)}\n${i}in\n${prettyPrint(ast.body, indent + 1)}` + return `${ast.name} = ${prettyPrint(ast.value, indent + 1)};\n${i}${prettyPrint(ast.body, indent)}` case 'list': const elems = ast.elements.map(e => prettyPrint(e, 0)).join(', '); - return `${i}[${elems}]`; + return `[${elems}]`; case 'record': - const fields = Object.entries(ast.fields) - .map(([k, v]) => `${k} = ${prettyPrint(v, 0)}`) - .join(', '); - return `${i}{${fields}}`; + const parts = ast.entries.map(entry => + entry.kind === 'spread' + ? `...${prettyPrint(entry.expr, )}` + : `${entry.key} = ${prettyPrint(entry.value, 0)}` + ); + return `{ ${parts.join(', ') }`; + + case 'lambda': { + const params = ast.params.join(' '); + const body = prettyPrint(ast.body, indent + 1); + const isComplex = ast.body.kind === 'match' || ast.body.kind === 'let'; + if (isComplex) { + return `${params} \\\n${body}` - case 'lambda': - const params = ast.params.join(', '); - return `${i}(${params}) => ${prettyPrint(ast.body)}` + } + return `${params} \\ ${body}` + } case 'record-access': - return `${i}${prettyPrint(ast.record)}.${ast.field}`; + return `${prettyPrint(ast.record, 0)}.${ast.field}`; - case 'record-update': - const updates = Object.entries(ast.updates).map(([k, v]) => `${k} = ${prettyPrint(v, 0)}`).join(', '); - return `${i}${prettyPrint(ast.record)} { ${updates} }` + case 'record-update': { + const updates = Object.entries(ast.updates) + .map(([k, v]) => `${k} = ${prettyPrint(v, 0)}`) + .join(', '); + return `${prettyPrint(ast.record, 0)}.{ ${updates} }` + } case 'match': const expr = prettyPrint(ast.expr, 0); const cases = ast.cases - .map(c => ` | ${prettyPrintPattern(c.pattern)} -> ${prettyPrint(c.result, 0)}`) + .map(c => `${i}| ${prettyPrintPattern(c.pattern)} \\ ${prettyPrint(c.result, indent + 1)}`) .join('\n'); - return `${i}match ${expr}\n${cases}`; + return `${expr}\n${cases}`; + + case 'rebind': + return `${prettyPrint(ast.target, 0)} := ${prettyPrint(ast.value, 0)}`; + + case 'list-spread': + return `...${prettyPrint(ast.spread, 0)}`; + + case 'definition': + return `${ast.name} = ${prettyPrint(ast.body, indent)}`; default: return `Unknown AST kind: ${i}${(ast as any).kind}` @@ -273,6 +293,12 @@ function prettyPrintPattern(pattern: Pattern): string { const elems = pattern.elements.map(prettyPrintPattern).join(', '); return `[${elems}]`; + case 'list-spread': + const head = pattern.head.map(prettyPrintPattern).join(', '); + return head.length > 0 + ? `[${head}, ...${pattern.spread}]` + : `[...${pattern.spread}]`; + case 'record': const fields = Object.entries(pattern.fields) .map(([k, p]) => `${k} = ${prettyPrintPattern(p)}`) diff --git a/src/cg/01-stdlib.cg b/src/cg/01-stdlib.cg index 2825f8a..978eb6c 100644 --- a/src/cg/01-stdlib.cg +++ b/src/cg/01-stdlib.cg @@ -1,8 +1,9 @@ # nth : Int \ List a \ Maybe a -nth = i list \ [i, list] - | [_, []] \ None - | [0, [x, ...xs]] \ (Some x) - | [n, [x, ...xs]] \ nth (n - 1) xs; +# in host at the moment, until we get typeclasses or something +# nth = i list \ [i, list] +# | [_, []] \ None +# | [0, [x, ...xs]] \ (Some x) +# | [n, [x, ...xs]] \ nth (n - 1) xs; # map : (a \ b) \ List a \ List b map = f list \ list @@ -118,3 +119,5 @@ any = f list \ fold (acc x \ or acc (f x)) False list; # all : (a \ Bool) \ List a \ Bool all = f list \ fold (acc x \ and acc (f x)) True list; + +# split : String \ String \ String diff --git a/src/cg/03-ui-components.cg b/src/cg/03-ui-components.cg index ac83441..017ec1e 100644 --- a/src/cg/03-ui-components.cg +++ b/src/cg/03-ui-components.cg @@ -1,3 +1,5 @@ +empty = ui.rect { w = 0, h = 0 }; + centerH = parentW child \ childW = (ui.measure child).width; ui.positioned { x = (parentW - childW) / 2, y = 0, child = child }; @@ -10,6 +12,17 @@ center = parentW parentH child \ childSize = ui.measure child; ui.positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child }; +scrollable = config \ + ui.clip { + w = config.w, + h = config.h, + child = ui.positioned { + x = 0 - config.scrollX, + y = 0 - config.scrollY, + child = config.child + } + }; + # button : Record -> ui button = config \ ui.clickable { @@ -22,25 +35,42 @@ button = config \ } }; -box = config \ ui.stack { - children = [ - # background - ui.rect { w = config.w, h = config.h, color = config.color }, - # top border - ui.rect { w = config.w, h = config.borderTop, color = config.borderColor }, - # bottom border - ui.positioned { x = 0, y = config.h - config.borderBottom, child = - ui.rect { w = config.w, h = config.borderBottom, color = config.borderColor } - }, - # left border - ui.rect { w = config.borderLeft, h = config.h, color = config.borderColor }, - # right border - ui.positioned { x = config.w - config.borderRight, y = 0, child = - ui.rect { w = config.borderRight, h = config.h, color = config.borderColor } - }, - # content - ui.positioned { x = config.paddingLeft, y = config.paddingTop, child = config.child } - ]}; +box = config \ + defaults = { + w = 100, + h = 100, + color = "white", + borderTop = 0, + borderBottom = 0, + borderLeft = 0, + borderRight = 0, + borderColor = "transparent", + paddingTop = 0, + paddingLeft = 0 + }; + + c = { ...defaults, ...config }; + + ui.stack { + children = [ +# background + ui.rect { w = c.w, h = c.h, color = c.color }, +# top border + ui.rect { w = c.w, h = c.borderTop, color = c.borderColor }, +# bottom border + ui.positioned { x = 0, y = c.h - c.borderBottom, child = + ui.rect { w = c.w, h = c.borderBottom, color = c.borderColor } + }, +# left border + ui.rect { w = c.borderLeft, h = c.h, color = c.borderColor }, +# right border + ui.positioned { x = c.w - c.borderRight, y = 0, child = + ui.rect { w = c.borderRight, h = c.h, color = c.borderColor } + }, +# content + ui.positioned { x = c.paddingLeft, y = c.paddingTop, child = c.child } + ] + }; insertChar = text pos char \ before = slice text 0 pos; diff --git a/src/cg/05-palette.cg b/src/cg/05-palette.cg index 1c25f26..aaaa79d 100644 --- a/src/cg/05-palette.cg +++ b/src/cg/05-palette.cg @@ -1,9 +1,43 @@ +# TODO: Section labels - flat list with Section/Item types, +# skip sections in keyboard nav, when scrolling with keyboard +# itemHeight +# items = [ +# Section { label = "Suggestions" }, +# Item { name = "Calendar", type = "Application", icon = "..." }, +# Item { name = "Weather", type = "Application", icon = "..." }, +# Section { label = "Commands" }, +# Item { name = "DynamoDB", subtitle = "Amazon AWS", type = "Command" }, +# ... +# ]; +# +# itemHeight = item \ item +# | Section _ \ 30 +# | Item _ \ 44; +# +# itemYOffset = items index \ +# sum (map itemHeight (take index items)); +# Then render based on type: +# renderItem = item \ item +# | Section { label = l } \ ui.text { content = l, color = "#888" } +# | Item i \ paletteRow { ... }; +# +# For keyboard nav, you'd skip sections when moving focus: +# nextSelectableIndex = items currentIndex direction \ +# next = currentIndex + direction; +# (nth next items +# | Some (Section _) \ nextSelectableIndex items next direction +# | Some (Item _) \ next +# | None \ currentIndex); + + + palette = config \ focusedIndex = 0; windowHeight = 400; windowWidth = 600; - results = take 10 (config.search config.state.query); + results = take 8 (config.search config.state.query); + # results = config.search config.state.query; # once you get scrolling.. dialogPadding = 0; @@ -20,13 +54,12 @@ palette = config \ child = ui.stack { children = [ ui.rect { w = config.w, h = config.h, color = color }, - centerV config.h ( - ui.positioned { - x = 10, - y = 10, - child = ui.text { content = config.child, color = "white" } - } - ) + + ui.positioned { + x = 6, + y = 12, + child = ui.text { content = config.child, color = "white" } + } ] } }; @@ -54,22 +87,34 @@ palette = config \ onChange = text \ batch [config.state.query := text, config.state.focusedIndex := 0], onKeyDown = key \ key | "ArrowUp" \ config.state.focusedIndex := max 0 (config.state.focusedIndex - 1) - | "ArrowDown" \ config.state.focusedIndex := (config.state.focusedIndex + 1) + | "ArrowDown" \ config.state.focusedIndex := min (len results - 1) (config.state.focusedIndex + 1) | "Enter" \ (\ config.onSelect (unwrapOr "" (nth config.state.focusedIndex results))) | _ \ noOp }, + ui.clip { w = contentWidth, h = listHeight, child = ui.column { gap = 1, - children = mapWithIndex (t i \ paletteRow { - child = t, - w = contentWidth, - h = textInputHeight, - selected = (config.state.focusedIndex == i), - onClick = \ config.onSelect t - }) results + children = [ + box { + w = contentWidth, + h = 30, + color = "transparent", + paddingLeft = 6, + paddingTop = 8, + child = ui.text { content = "Store values", color = "#bbb" }, + }, + + ...(mapWithIndex (t i \ paletteRow { + child = t, + w = contentWidth, + h = textInputHeight, + selected = (config.state.focusedIndex == i), + onClick = \ config.onSelect t + }) results) + ] } } ] diff --git a/src/cg/06-inspector.cg b/src/cg/06-inspector.cg new file mode 100644 index 0000000..70c3d10 --- /dev/null +++ b/src/cg/06-inspector.cg @@ -0,0 +1,44 @@ +inspector = config \ + windowHeight = 400; + windowWidth = 600; + + source = getSource config.name; + sourceLines = split "\n" source; + _ = debug "source" source; + _ = debug "sourceLines" sourceLines; + + dialogPadding = 0; + + textInputHeight = 40; + contentWidth = windowWidth - (dialogPadding * 2); + contentHeight = windowHeight - (dialogPadding * 2); + + ui.positioned { + x = (config.viewport.width - windowWidth) / 2, + y = (config.viewport.height - windowHeight) / 2, + + child = ui.stack { + children = [ + # background + ui.rect { w = windowWidth, h = windowHeight, color = "#063351", radius = 0, strokeWidth = 1, strokeColor = "#1A5F80" }, + ui.column { + gap = 0, + children = mapWithIndex (line i \ + textInput { + key = "palette-query" & (str i), + initialValue = line, + initialFocus = False, + color = "white", + backgroundColor = "rgba(0,0,0,0.0)", + w = contentWidth, + h = textInputHeight, + # onChange = text \ batch [config.state.query := text, config.state.focusedIndex := 0], + onChange = text \ batch [], + onKeyDown = key \ key + | _ \ noOp + } + ) sourceLines + } + ] + } + }; diff --git a/src/cg/10-os.cg b/src/cg/10-os.cg index 214c7ed..f9ce073 100644 --- a/src/cg/10-os.cg +++ b/src/cg/10-os.cg @@ -4,8 +4,20 @@ osState = { query = "", focusedIndex = 0, }, + inspector = { + visible = False, + name = "" + } }; +inspect = item \ + batch [ + osState.palette.visible := False, + osState.palette.query := "", + osState.inspector.visible := True, + osState.inspector.name := item + ]; + init = {}; update = state event \ event @@ -16,14 +28,24 @@ view = state viewport \ ui.stack { children = [ ui.rect { w = viewport.width, h = viewport.height, color = "#012" }, + + osState.inspector.visible + | True \ inspector { + name = osState.inspector.name, + viewport = viewport, + } + | False \ empty, + + # keep palette at the end so it's on top osState.palette.visible | True \ palette { state = osState.palette, search = storeSearch, - onSelect = item \ (debug "selected" item), + onSelect = item \ inspect item, viewport = viewport, } - | False \ ui.text { content = "" } + | False \ empty, + ] }; diff --git a/src/compiler.ts b/src/compiler.ts index 68f20b0..21c2c1e 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -12,7 +12,7 @@ export function compile(ast: AST): 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}`); + throw new Error(`Cannot compile literal`); // of kind ${ast.value.kind}`); case 'variable': return sanitize(ast.name); @@ -35,9 +35,12 @@ export function compile(ast: AST): string { return `${compile(ast.func)}(${args})`; case 'record': { - const fields = Object.entries(ast.fields) - .map(([k, v]) => `${sanitizeName(k)}: ${compile(v)}`); - return `({${fields.join(', ')}})`; + const parts = ast.entries.map(entry => + entry.kind === 'spread' + ? `...${compile(entry.expr)}` + : `${sanitizeName(entry.key)}: ${compile(entry.value)}` + ) + return `({${parts.join(', ')}})`; } case 'list': { @@ -98,7 +101,7 @@ function sanitize(name: string): string { if (ops[name]) return ops[name]; - const natives = ['measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'len', 'slice', 'str', 'redefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui']; + const natives = ['measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui']; if (natives.includes(name)) return `_rt.${name}`; return sanitizeName(name); @@ -284,7 +287,11 @@ function freeVars(ast: AST, bound: Set = new Set()): Set { } case 'record': { - const allVars = Object.values(ast.fields).flatMap(v => [...freeVars(v, bound)]); + const allVars = ast.entries.flatMap(entry => + entry.kind === 'spread' + ? [...freeVars(entry.expr, bound)] + : [...freeVars(entry.value, bound)] + ); return new Set(allVars); } diff --git a/src/parser.ts b/src/parser.ts index af4708a..f4690f5 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -475,7 +475,7 @@ export class Parser { if (token.kind === 'open-brace') { this.advance(); - const fields: { [key: string]: AST } = {}; + const entries: Array<{ kind: 'field', key: string, value: AST } | { kind: 'spread', expr: AST }> = []; let first = true; while (this.current().kind !== 'close-brace') { @@ -485,14 +485,20 @@ export class Parser { } first = false; - const keyToken = this.expect('ident'); - const key = (keyToken as { value: string }).value; - this.expect('equals'); - fields[key] = this.parseExpression(); + 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() }); + } } this.expect('close-brace'); - return { kind: 'record', fields, ...this.getPos(token) }; + return { kind: 'record', entries, ...this.getPos(token) }; } if (token.kind === 'int') { diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 531583e..698b57e 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -51,8 +51,14 @@ export const _rt = { rerender: { _tag: 'Rerender' }, focus: (key: string) => ({ _tag: 'Focus', _0: key }), - len: (list: any[]) => list.length, + nth: (i: number) => (xs: any[] | string) => i >= 0 && i < xs.length + ? { _tag: 'Some', _0: xs[i] } + : { _tag: 'None' }, + len: (xs: any[] | string) => xs.length, str: (x: any) => String(x), + chars: (s: string) => s.split(''), + join: (delim: string) => (xs: string[]) => xs.join(delim), + split: (delim: string) => (xs: string) => xs.split(delim), slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end), debug: (label: string) => (value: any) => { console.log(label, value); return value; }, fuzzyMatch: (query: string) => (target: string) => { @@ -70,7 +76,8 @@ export const _rt = { getSource: (name: string) => { const ast = definitions.get(name); if (!ast) return ""; - return prettyPrint(ast); + const printed = prettyPrint(ast); + return printed; }, rebind: (name: string, pathOrValue: any, maybeValue?: any) => { if (maybeValue === undefined) {