Skip to content

perry/ui-ios: helper-built widgets clobbered when called inside a for-loop and pushed to HStack/VStack #634

Closed

Description

@proggeramlug

Summary

When a helper function that builds a perry/ui widget is called repeatedly inside a for / for…of loop and the result is pushed into an array passed to HStack / VStack, the resulting children are not rendered (zero-size / invisible). The same widget tree built via literal sibling calls in the array argument renders correctly.

Tested on perry 0.5.727, --target ios-simulator (iPhone 16 Pro running iOS 26 sim).

Minimal repro

import {
    App, VStack, HStack, Text, Button,
    setPadding, setCornerRadius,
    widgetSetBackgroundColor,
    textSetColor, textSetFontSize,
} from "perry/ui";

function chip(label: string): Widget {
    const b = Button(label, () => {});
    textSetColor(b, 0.8, 0.07, 0.26, 1.0);
    textSetFontSize(b, 14);
    const wrap = VStack(0, [b]);
    widgetSetBackgroundColor(wrap, 1.0, 1.0, 1.0, 1.0);
    setCornerRadius(wrap, 5);
    setPadding(wrap, 8, 14, 8, 14);
    return wrap;
}

// ── Bug path: build via a for-loop that pushes to an array ──────────
const labels = ["A", "B", "C"];
const looped: Widget[] = [];
for (const label of labels) {
    looped.push(chip(label));
}
const loopedRow = HStack(8, looped);

// ── Working path: literal sibling calls inside HStack ──────────────
const literalRow = HStack(8, [chip("A"), chip("B"), chip("C")]);

const heading1 = Text("Loop-built (broken):");
textSetFontSize(heading1, 16);
const heading2 = Text("Literal-sibling calls (works):");
textSetFontSize(heading2, 16);

const body = VStack(16, [heading1, loopedRow, heading2, literalRow]);
setPadding(body, 24, 16, 16, 16);
widgetSetBackgroundColor(body, 0.94, 0.94, 0.96, 1.0);

App({
    title: "Loop bug repro",
    width: 390, height: 300,
    body,
});

Compile and run:

mkdir -p dist
perry compile src/main.ts --target ios-simulator -o dist/repro
xcrun simctl install booted dist/repro.app
xcrun simctl launch booted com.perry.repro

Expected vs actual

Expected: two rows of three chips (A B C), top row labeled "Loop-built", bottom row labeled "Literal-sibling".

Actual: the loop-built row renders nothing at all — large empty space below the heading. The literal-sibling row renders correctly. Same chip() helper, same number of children, same parent HStack — only difference is loop-pushed vs literal array.

Workaround

Unroll the loop into a literal array of helper calls. Verified working in sharity-perry-app's Filter screen — 5 radius chips and 10 category chips rendered correctly only after replacing for (const r of RADII) row.push(chip(r)) with [chip("5 km", …), chip("10 km", …), …].

Notes

  • TypeScript type-checks both forms; the bug is purely a runtime / codegen concern.
  • Does not depend on the helper's specific widget shape — the symptom appeared in this codebase first with chip-shaped Buttons-in-VStacks; the minimal repro above uses the same shape but the issue is likely about how the codegen handles the iterated widget-handle creation when a helper produces multiple handles per call.
  • I haven't tested this on other targets (macos, android, web). If it's iOS-specific, the bug likely lives in perry-ui-ios's widget-handle bookkeeping rather than the codegen itself.

Activity

  1. added a commit that references this issue on May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions