Skip to content

feat: add file-scoped recursive for loops - #547

Merged
Mohamed Mansour (mohamedmansour) merged 4 commits into
mainfrom
mohamedmansour-recursive-for-loops
Sep 18, 2026
Merged

Mohamed Mansour (mohamedmansour) merged 4 commits into
mainfrom
mohamedmansour-recursive-for-loops

Conversation

@mohamedmansour

Copy link
Copy Markdown
Contributor

Why

Render tree-shaped state by reusing one loop body instead of duplicating markup or introducing recursive component definitions.

Closes #518

Approach

<for id="tree-item" each="item in items">
  <h1>{{item.name}}</h1>
  <for id="tree-item" each="item in item.children" />
</for>
  • Compile each named body once as <id>-<owner index>. References within that file reuse its protocol fragment and native client block index; other components can use the same ID independently.
  • Evaluate the reference's collection in its parent scope, then shadow and restore the same item variable for each child. Forward references are supported. A second body produces duplicate-for-id; missing definitions and mismatched item names also fail at build time.
  • Make CSS-token traversal cycle-safe without losing inherited scope information. Preserve component-asset/style discovery, keyed reconciliation, hydration, events, and client-created rendering.
  • Reuse existing protocol fields and runtime behavior: no protocol schema changes or additional browser runtime JavaScript.

Compatibility

id replaces the legacy template attribute, which now produces a migration diagnostic. each accepts only unbraced syntax such as each="item in items". FAST component builds reject named references with an actionable diagnostic; SSR and the native WebUI client support them.

Validation

  • cargo xtask check passed, including workspace tests, linting, dependency audit, protocol drift, native/WASM builds, examples, benchmark validation, and docs.
  • Parser and handler unit regressions cover file isolation, fragment reuse, scope restoration, forward/mutual references, invalid syntax, duplicate definitions, and cyclic build traversal. Unit tests remain inline in their implementation files.
  • All six recursive browser scenarios passed against a freshly rebuilt native addon, covering SSR, hydration, keyed updates, events, and client creation.
  • Release directive benchmarks showed no ordinary-loop regression; representative client-template compilation measured 87.4 us before and 78.3 us after.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

It contains at least one confirmed correctness/UX issue (nondeterministic/incorrect client-template diagnostic locations) plus a FAST validation bypass due to case-sensitive tag matching.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR introduces file-scoped named <for> bodies to enable recursive (and mutually recursive) loops by reusing a single compiled loop body within the same entry/component file, while keeping protocol shape stable and avoiding schema/runtime JS additions.

Changes:

  • Add id="..." support for <for> to define/refer to a reusable loop body (including forward/mutual references) with build-time validation and new diagnostic codes.
  • Enforce unbraced each="item in collection" parsing across SSR/native compilation paths, and reject legacy template naming with a migration diagnostic.
  • Add cycle-safe traversal for CSS-token analysis with shared loop records; add extensive Rust + Playwright fixture coverage and documentation updates.
File summaries
File Description
packages/webui-framework/tests/fixtures/recursive-repeat/state.json Adds state fixture data for recursive/forward/independent/mutual loop scenarios.
packages/webui-framework/tests/fixtures/recursive-repeat/src/test-recursive-tree/test-recursive-tree.html Defines a recursive named-loop tree rendering template with events and keys.
packages/webui-framework/tests/fixtures/recursive-repeat/src/test-recursive-mutual/test-recursive-mutual.html Adds mutual named-loop reference fixture to validate finite traversal and scoping.
packages/webui-framework/tests/fixtures/recursive-repeat/src/test-recursive-independent/test-recursive-independent.html Adds same-id independence fixture scoped per component/file.
packages/webui-framework/tests/fixtures/recursive-repeat/src/test-recursive-forward/test-recursive-forward.html Adds forward-reference fixture (reference before definition).
packages/webui-framework/tests/fixtures/recursive-repeat/src/index.html Fixture page wiring for Playwright coverage.
packages/webui-framework/tests/fixtures/recursive-repeat/recursive-repeat.spec.ts Playwright coverage for SSR/no-JS render, hydration, keyed updates, events, and client-created recursion.
packages/webui-framework/tests/fixtures/recursive-repeat/element.ts Defines observable element classes used by the recursive fixture tests.
docs/guide/concepts/directives/for.md Documents recursive named loops, id rules, key inheritance, and each formatting constraints.
docs/ai.md Updates AI guidance with named recursion rules and links to the canonical docs section.
DESIGN.md Updates specification for <for> parsing/diagnostics and adds cycle-safe CSS token walk behavior notes.
crates/webui/src/lib.rs Adds an end-to-end Rust build test to ensure recursive named loops don’t break component asset/style discovery.
crates/webui-parser/src/plugin/webui/named_repeats.rs Introduces compile-time named-repeat block reuse + key inheritance + cycle-safe root analysis helper.
crates/webui-parser/src/plugin/webui.rs Integrates named repeat compilation into WebUI template metadata emission; normalizes <if> condition braces.
crates/webui-parser/src/plugin/fast/v3.rs Rejects named <for> references in FAST builds; normalizes <if> condition braces.
crates/webui-parser/src/plugin/fast/v2.rs Same as v3: rejects named references; normalizes <if> condition braces.
crates/webui-parser/src/plugin/fast/shared.rs Adds shared FAST validation to detect unsupported named <for> references.
crates/webui-parser/src/plugin/fast/diagnostic.rs Adds fast-named-for-unsupported diagnostic with actionable guidance.
crates/webui-parser/src/lib.rs Implements named loop scoping, ID collision checks, each parser reuse, brace-stripping for <if>, and cycle-safe token traversal.
crates/webui-parser/src/diagnostic.rs Adds new diagnostic code constants for named-loop authoring errors.
crates/webui-handler/src/lib.rs Adds SSR rendering regression tests covering recursion, scoping restore, missing children termination, and error behavior.
Review details
  • Files reviewed: 21/21 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/webui-parser/src/plugin/fast/shared.rs Outdated
Comment thread crates/webui-parser/src/plugin/webui/named_repeats.rs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mddinbox

Copy link
Copy Markdown
Contributor

ISSUE-001: Deduplicate equivalent shared-block analysis paths

Severity: 🟡 Performance — P2

Files: crates\webui-parser\src\plugin\webui.rs:1132–1136; crates\webui-parser\src\plugin\webui\named_repeats.rs:109–123.

The new ancestor-only guard prevents cycles but still visits every acyclic path through shared bodies, retaining a scope record per visit. Equivalent scopes therefore cause exponential build-time CPU and memory growth.

Reproduction: 22 named bodies, each referencing the next twice, produce just 2.8 KB of source. A release-mode real build took approximately 884 ms and 328 MiB peak working set, producing only 22 blocks and 2.5 KB of client metadata. Rendering with empty collections produced 25 bytes of HTML. The expensive work is redundant analysis, not required output.

Suggested fix: Memoize equivalent block/scope visits or compute free-root sets iteratively. Preserve distinctions between genuinely different scopes; block-only global deduplication would be incorrect.

Evidence: 1,451 affected Rust tests, 363 framework unit tests, E2E typechecking, all six recursive browser scenarios, and an additional 250-level client-created tree probe passed. All 35 current-head CI checks are green.

Existing recursive runtime calls are pre-existing, not another PR-introduced finding. Full local quality-gate/platform coverage and ordinary-loop performance claims were not independently verified.

Memoize block visits by their canonical bound-name sets in client root and CSS token analysis, retaining distinct scopes and the ordinary-template fast path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mohamedmansour

Copy link
Copy Markdown
Contributor Author

Accepted ISSUE-001 and fixed it in 1b6e2ff by memoizing equivalent block/bound-name-set visits in client-root and CSS-token analysis, preserving distinct lexical scopes, traversal order, cycle guards, and the ordinary-template fast path. Across seven fresh release-build processes, the 22-body reproduction improved from 875.33 ms/328.39 MiB to 0.87 ms/7.53 MiB (medians), with identical 22 blocks, 2,517-byte metadata, canonical protocol content, and 25-byte HTML; the new inline regressions fail without memoization, and 1,458 affected Rust tests, 363 framework unit tests, 37 repeat browser tests, E2E typechecking, and the full cargo xtask check gate pass.

mddinbox
mddinbox previously approved these changes Sep 18, 2026

@mddinbox mddinbox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the latest head and the addressed feedback. The FAST case-insensitive validation, deterministic diagnostic locations, and shared-block scope memoization fixes are satisfactory. No remaining code-review blockers found.

Validate scoped overrides separately from unconditional defaults while retaining recursive component asset and style coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@mddinbox mddinbox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the latest merge commit and its conflict resolutions. The recursive CSS-token fixtures now correctly distinguish unconditional :root defaults from selector-scoped overrides, while retaining recursive asset/style and fallback-chain coverage. No new correctness, performance, memory, security, reliability, or dependency-impact blockers found.

@mohamedmansour
Mohamed Mansour (mohamedmansour) merged commit d17c9f3 into main Sep 18, 2026
35 checks passed
@mohamedmansour
Mohamed Mansour (mohamedmansour) deleted the mohamedmansour-recursive-for-loops branch September 18, 2026 20:29
Mohamed Mansour (mohamedmansour) added a commit that referenced this pull request Sep 18, 2026
## Release

Bumps WebUI to `0.0.29`.

Previous release tag: `v0.0.28`

## Changes since `v0.0.28`

Features:

- Ship version-matched AI authoring guidance with `@microsoft/webui`,
using a lightweight skill that reads the installed package's reference
(feat: bundle versioned AI guidance with WebUI #522 by @mohamedmansour).
- Add plugin-aware component discovery, with filename-based HTML
components and manifest-driven FAST components (feat: add plugin-aware
component discovery #524 by @mohamedmansour).
- Add WebUI Press `--show=all|content` for shell-free documentation
views that preserve SSR, hydration, authored examples, and accessible
theme behavior (feat: add WebUI Press content mode #525 by
@mohamedmansour).
- Automatically support Trusted Types for compiled templates and
generated CSS import maps without promoting arbitrary state HTML to
trusted content (feat: use Trusted Types automatically in the framework
#536 by @mohamedmansour).
- Keep explicit streaming imports lightweight so applications can load
streaming support early and application code later (feat: keep explicit
streaming imports lightweight #539 by @mohamedmansour).
- Render tree-shaped state with file-scoped named `<for id="...">`
bodies, including forward references and recursive reuse without extra
client runtime code. Use unbraced `each="item in items"`; the legacy
`template` spelling and FAST component references produce actionable
diagnostics (feat: add file-scoped recursive for loops #547 by
@mohamedmansour).

Fixes:

- Preserve table-rooted hydration ownership and correctly compile
whitespace-bearing, nested directives, preventing duplicate controls
after reactive updates. Rebuild templates to receive the compiler fix
(fix: preserve table-rooted hydration paths #512 by @mohamedmansour;
fix: align client directive parsing with the HTML scanner #531 by
@mohamedmansour).
- Emit reachable stylesheets and templates in deterministic traversal
order. External plugin implementations must adapt the affected
`HandlerPlugin` and bootstrap component collection interfaces from hash
sets to slices (fix: render reachable-component ordering
deterministically #519 by @janechu).
- Resolve bare CEM module specifiers through their owning packages,
respecting export maps, package boundaries, and cache invalidation when
nearer dependencies appear (fix: resolve bare CEM module specifiers #528
by @janechu).
- Prevent spurious dev-server rebuilds from read-only filesystem
activity and recursive traversal of linked workspace dependencies
(Improve benchmark telemetry and dev-server watching #521 by
@mohamedmansour).
- Update rustls to address TLS 1.3 handshake encryption-level validation
advisory RUSTSEC-2026-0285 (fix: update rustls to address
RUSTSEC-2026-0285 #533 by @mohamedmansour).
- Forward bare boolean and ARIA component inputs into SSR state while
preserving empty literal attributes and existing conditional boolean
bindings (fix: forward HTML component inputs during SSR #540 by
@mohamedmansour).
- Preserve sibling route declaration order across SSR and client routing
so equally specific routes select the correct pending and error
boundaries (fix: preserve route order for pending and error boundaries
#541 by @mohamedmansour).
- Handle skipped or superseded router view transitions without unhandled
animation rejections, while preserving route-commit errors and
responsive subsequent navigation (fix: handle router view transition
rejections #543 by @mohamedmansour).
- Wait for active dev-server rebuilds and their bundler subprocesses
before graceful shutdown, preventing output writes after the server
exits (fix: wait for dev-server rebuilds during shutdown #546 by
@mohamedmansour).
- Preserve global theme defaults when custom properties are overridden
only by scoped selectors or conditional rules, so unmatched component
instances retain their intended colors (fix: preserve theme defaults
under scoped CSS overrides #548 by @mohamedmansour).

Docs:

- Add a data-driven Benchmark Explorer, centralize performance guidance,
distinguish independent SSR/browser metrics, and publish resource
telemetry with accessible methodology details (docs: centralize
performance guidance and benchmark data #486 by @mohamedmansour; Improve
benchmark telemetry and dev-server watching #521 by @mohamedmansour).
- Update AI reference installation and migration guidance while
retaining `/ai`, and clarify plugin-specific discovery and FAST
converted-template requirements (feat: bundle versioned AI guidance with
WebUI #522 by @mohamedmansour; feat: add plugin-aware component
discovery #524 by @mohamedmansour).
- Document owned Rust partial rendering, streaming state ownership, and
watcher hashing benchmarks, including measurement scope and tradeoffs
(perf: reduce partial response allocations #510 by @mohamedmansour;
perf: move streaming command processing off async workers #529 by
@mohamedmansour; perf: bound watcher hashing memory with reusable
scratch #530 by @mohamedmansour).
- Document automatic Trusted Types support, CSP enforcement, and
first-declared precedence for equally specific routes and their
pending/error boundaries (feat: use Trusted Types automatically in the
framework #536 by @mohamedmansour; fix: preserve route order for pending
and error boundaries #541 by @mohamedmansour).
- Explain named recursive loops and their compatibility limits, scoped
CSS token defaults, and router view-transition error handling (feat: add
file-scoped recursive for loops #547 by @mohamedmansour; fix: preserve
theme defaults under scoped CSS overrides #548 by @mohamedmansour; fix:
handle router view transition rejections #543 by @mohamedmansour).

Maintenance:

- Reduce partial-response and per-render allocations through owned state
projection, borrowed graph traversal, scalar formatting, and borrowed
nested route trees. Rust callers use ownership-taking
`Protocol::render_partial(Value, ...)`; JSON boundaries retain
`render_partial_json` (perf: reduce partial response allocations #510 by
@mohamedmansour; perf: cut per-render handler allocations #511 by
@mohamedmansour; perf: avoid cloning nested route trees #513 by
@mohamedmansour).
- Move streaming command deserialization, validation, and default
preparation onto the existing blocking renderer, transferring owned
records and state rather than cloning retained projections (perf: move
streaming command processing off async workers #529 by @mohamedmansour).
- Bound watcher hashing content storage to one reusable 8 KiB buffer
instead of repeated file-sized allocations while preserving invalidation
behavior (perf: bound watcher hashing memory with reusable scratch #530
by @mohamedmansour).
- Parallelize release WASM builds and artifact staging, and consolidate
dependency scanning before release jobs (chore: parallelize release
pipeline work #509 by @mohamedmansour).
- Refresh compatible Rust and JavaScript dependencies and migrate
repository tooling and CI to pnpm 12.3.4 while preserving existing audit
constraints (chore: update dependencies to latest compatible versions
#534 by @mohamedmansour).

## Validation

- `cargo xtask check`
- `pnpm --dir crates/webui-press test` (16 passed).
- `pnpm --dir packages/webui-framework exec playwright test
recursive-repeat --workers=2 --reporter=line` (6 passed).
- `pnpm --dir packages/webui-router exec playwright test --grep "view
transition rejection ownership" --workers=2 --reporter=line` (4 passed).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Recursive For Loops

3 participants