Records now hoist their fields if they reference other fields so we can have circular

This commit is contained in:
Dustin Swan 2026-04-01 19:43:45 -06:00
parent a76f4cc332
commit d3248be4d9
No known key found for this signature in database
GPG key ID: 30D46587E2100467
2 changed files with 75 additions and 48 deletions

View file

@ -390,58 +390,60 @@ LayoutChild = Fixed Int (Int \ Int \ UI) | Flex Int (Int \ Int \ UI);
Alignment = Start | Center | End; Alignment = Start | Center | End;
Direction = Col | Row; Direction = Col | Row;
layoutDir layout = {
: Direction \ { w : Int, h : Int } \ List LayoutChild \ { ui : UI, w : Int, h : Int } layoutDir
= dir constraints children \ # : Direction \ { w : Int, h : Int } \ List LayoutChild \ { ui : UI, w : Int, h : Int }
mainSize = dir | Col \ constraints.h | Row \ constraints.w; = dir constraints children \
crossSize = dir | Col \ constraints.w | Row \ constraints.h; mainSize = dir | Col \ constraints.h | Row \ constraints.w;
crossSize = dir | Col \ constraints.w | Row \ constraints.h;
# Pass 1: sum fixed heights, total flex weight # Pass 1: sum fixed heights, total flex weight
info = fold (acc child \ child info = fold (acc child \ child
| Fixed s _ \ acc.{ fixedMain = acc.fixedMain + s } | Fixed s _ \ acc.{ fixedMain = acc.fixedMain + s }
| Flex n _ \ acc.{ totalFlex = acc.totalFlex + n } | Flex n _ \ acc.{ totalFlex = acc.totalFlex + n }
) { fixedMain = 0, totalFlex = 0 } children; ) { fixedMain = 0, totalFlex = 0 } children;
remaining = mainSize - info.fixedMain; remaining = mainSize - info.fixedMain;
# Pass 2 # Pass 2
result = fold (acc child \ result = fold (acc child \
allocated = (child allocated = (child
| Fixed s view \ { s = s, view = view } | Fixed s view \ { s = s, view = view }
| Flex n view \ { s = remaining * n / info.totalFlex, view = view }); | Flex n view \ { s = remaining * n / info.totalFlex, view = view });
childW = dir | Col \ crossSize | Row \ allocated.s; childW = dir | Col \ crossSize | Row \ allocated.s;
childH = dir | Col \ allocated.s | Row \ crossSize; childH = dir | Col \ allocated.s | Row \ crossSize;
posX = dir | Col \ 0 | Row \ acc.pos; posX = dir | Col \ 0 | Row \ acc.pos;
posY = dir | Col \ acc.pos | Row \ 0; posY = dir | Col \ acc.pos | Row \ 0;
rendered = ui.positioned { rendered = ui.positioned {
x = posX, y = posY, x = posX, y = posY,
child = allocated.view childW childH child = allocated.view childW childH
}; };
acc.{ pos = acc.pos + allocated.s, children = [...acc.children, rendered] } acc.{ pos = acc.pos + allocated.s, children = [...acc.children, rendered] }
) { pos = 0, children = [] } children; ) { pos = 0, children = [] } children;
{ ui = ui.stack { children = result.children }, w = constraints.w, h = constraints.h }; { ui = ui.stack { children = result.children }, w = constraints.w, h = constraints.h },
col = layoutDir Col; col = layoutDir Col,
row = layoutDir Row; row = layoutDir Row,
align = hAlign vAlign w h child \ align = hAlign vAlign w h child \
x = hAlign x = hAlign
| Start \ 0 | Start \ 0
| Center \ (w - child.w) / 2 | Center \ (w - child.w) / 2
| End \ w - child.w; | End \ w - child.w;
y = vAlign y = vAlign
| Start \ 0 | Start \ 0
| Center \ (h - child.h) / 2 | Center \ (h - child.h) / 2
| End \ h - child.h; | End \ h - child.h;
{ ui = ui.positioned { x = x, y = y, child = child.ui }, w = w, h = h }; { ui = ui.positioned { x = x, y = y, child = child.ui }, w = w, h = h },
};
testApp = name \ testApp = name \
{ view = ctx \ { view = ctx \
col { w = ctx.w, h = ctx.h } [ layout.col { w = ctx.w, h = ctx.h } [
Fixed 40 (w h \ box { w = w, h = h, color = "red", child = text "Header" }), Fixed 40 (w h \ box { w = w, h = h, color = "red", child = text "Header" }),
Flex 1 (w h \ box { w = w, h = h, color = "#1a1a2e", child = text "Main content" }), Flex 1 (w h \ box { w = w, h = h, color = "#1a1a2e", child = text "Main content" }),
Fixed 30 (w h \ box { w = w, h = h, color = "blue", child = text "Footer" }), Fixed 30 (w h \ box { w = w, h = h, color = "blue", child = text "Footer" }),
@ -451,13 +453,13 @@ testApp = name \
testApp2 = _ \ testApp2 = _ \
{ view = ctx \ { view = ctx \
col { w = ctx.w, h = ctx.h } [ layout.col { w = ctx.w, h = ctx.h } [
Fixed 40 (w h \ (align Center Center w h (sizedText "Header")).ui), Fixed 40 (w h \ (layout.align Center Center w h (sizedText "Header")).ui),
Flex 1 (w h \ (row { w = w, h = h } [ Flex 1 (w h \ (layout.row { w = w, h = h } [
Fixed 200 (w h \ box { w = w, h = h, color = "#2a2a3e", child = text "Sidebar" }), Fixed 200 (w h \ box { w = w, h = h, color = "#2a2a3e", child = text "Sidebar" }),
Flex 1 (w h \ box { w = w, h = h, color = "#1a1a2e", child = text "Main" }), Flex 1 (w h \ box { w = w, h = h, color = "#1a1a2e", child = text "Main" }),
]).ui), ]).ui),
Fixed 30 (w h \ (align Center Center w h (sizedText "Footer")).ui), Fixed 30 (w h \ (layout.align Center Center w h (sizedText "Footer")).ui),
] ]
~ (e \ e.ui) ~ (e \ e.ui)
}; };

View file

@ -69,11 +69,36 @@ export function compile(ast: AST, ctx: CompileCtx = defaultCtx): string {
return `${compile(ast.func, ctx)}(${args})`; return `${compile(ast.func, ctx)}(${args})`;
case 'record': { case 'record': {
const fieldNames = new Set(
ast.entries.filter(e => e.kind === 'field').map(e => e.key)
);
// Check if any field references another
const needsHoist = ast.entries.some(entry =>
entry.kind === 'field' &&
[...freeVars(entry.value)].some(v =>
fieldNames.has(v) && v !== entry.key && !ctx.bound.has(v) && !ctx.topLevel.has(v)
)
);
if (needsHoist) {
const bindings: string[] = [];
const names: string[] = [];
for (const entry of ast.entries) {
if (entry.kind === 'spread') continue;
const safe = sanitizeName(entry.key);
bindings.push(`const ${safe} = ${compile(entry.value, { ...ctx, bound: new Set([...ctx.bound, ...fieldNames]) })};`);
names.push(`${JSON.stringify(entry.key)}: ${safe}`);
}
return `(() => { ${bindings.join(' ')} return {${names.join(', ')}}; })()`;
}
// Simple record, no hoisting
const parts = ast.entries.map(entry => const parts = ast.entries.map(entry =>
entry.kind === 'spread' entry.kind === 'spread'
? `...${compile(entry.expr, ctx)}` ? `...${compile(entry.expr, ctx)}`
: `${JSON.stringify(entry.key)}: ${compile(entry.value, ctx)}` : `${JSON.stringify(entry.key)}: ${compile(entry.value, ctx)}`
) )
return `({${parts.join(', ')}})`; return `({${parts.join(', ')}})`;
} }