# Hierarchical Params Refactor – Implementation Summary ## Executive Summary - We rewrote the map-closure transformer so generated code keeps the original callback bodies and variable names, while maintaining compatibility with the existing `mapWithPattern` runtime contract. - The change substantially improves readability, removes several brittle AST rewriting passes, and closes correctness gaps (optional chaining, computed keys, identifier collisions). ## Why We Needed This The earlier transformer renamed callback parameters (`item` → `element`), flattened captured state into a single object, and rewrote the callback body to match those new names. That approach worked functionally but left us with: - Generated code that no longer resembled the source, making reviews and debugging harder. - Symbol-resolution issues in TypeScript because we injected synthetic identifiers. - Edge-case bugs around optional chaining, computed property names, and alias collisions (`element`, `_v1`, etc.). Our goal for this refactor was to eliminate the body rewrite altogether and let developers read the transformed file as if it were hand-written. ## What Changed ### 1. Capture hierarchy mirrors the source - We now build a **capture tree** (`groupCapturesByRoot`) that records each captured expression by its root (e.g. `state.pricing.discount`). - The generated schema and runtime params object follow that same shape. Example: `{ state: { pricing: { discount: … } } }` instead of `{ discount: … }`. #### Before/after: single state capture ``` // Source state.items.map((item) => item.price * state.discount); // Old output (simplified) ({ element, params: { discount } }) => element.price * discount { discount: state.discount } // New output ({ element: item, params: { state } }) => item.price * state.discount { state: { discount: state.discount } } ``` ### 2. Destructuring aliases recover original names - Runtime still delivers `{ element, index, array, params }` to the recipe callback. - We destructure to the developer’s names (`({ element: item, params: { state } })`) so the callback body is untouched. - New helpers normalise identifiers across transformers, ensuring shared behaviour when we need fresh names. #### Before/after: optional chaining & nested structure ``` // Source orders.map((order) => order.customer?.address ?? state.fallback); // Old output rewrote the chain and flattened params ({ element, params: { fallback } }) => element.customer.address ?? fallback // New output preserves the body and structure ({ element: order, params: { state } }) => order.customer?.address ?? state.fallback ``` ### 3. Body rewriting is minimal and safer - We no longer rename identifiers or replace capture references. - The only edits we still make are to rebuild destructured element bindings and to cache computed property names once per callback (so expressions like `{ [nextKey()]: value }` run exactly once). - Optional chaining is preserved because params are built from the original AST rather than reconstructed manually. #### Before/after: outer `element` variable collision ``` const element = highlight; items.map(() => {element}); // Old output shadowed the outer variable ({ element }) => {element} // New output aliases the runtime value and keeps the capture intact ({ element: __ct_element, params: { element } }) => {element} ``` ### 4. Supporting utilities were aligned - `capture-tree.ts` now rebuilds access expressions using the original operators (optional chaining vs. plain dot access). - Identifier normalisation is shared across closures, derives, and opaque-ref transforms. - The `derive` transformer was updated alongside closures so its synthesized callbacks use the same hierarchical capture helpers and keep destructuring intact; a focused regression test guards the collision case we fixed there. - Regression fixtures were updated manually to document the new structure and to add coverage for numeric aliases, computed keys, and collision scenarios. #### Before/after: computed property caching ``` // Source items.map(({ [nextKey()]: value }) => value); // Old output re-evaluated nextKey() for every read const __ct_amount_key = nextKey(); ({ element }) => element[__ct_amount_key] // New output caches once and threads the key through derive const __ct_val_key = nextKey(); ({ element }) => derive({ element, __ct_val_key }, ({ element, __ct_val_key: key }) => element[key]) ``` #### Before/after: derive callback collision fix ``` // Source const fallback = _v1(); derive(items, () => _v1()); // Old output reused _v1 inside the lambda, shadowing the capture ({ _v1 }) => _v1() // New output generates a stable alias ({ _v1_1 }) => _v1_1() ``` ## Impact & Benefits - **Readability:** Generated output now mirrors the source; reviewers can reason about behaviour without mentally translating renamed variables. - **Correctness:** Fixes long-standing edge cases (optional chaining, computed aliases, captured `element` collisions) and prevents repeated evaluation of computed keys. - **Maintainability:** We removed entire classes of substitution logic, making the transformer easier to extend (e.g. upcoming handler-closure support can reuse the same helpers). - **Confidence:** Full `deno task test` passes; targeted regression fixtures fail if we revert the business-logic pieces (demonstrated via stash/unstash checks during development). ## Trade-offs & Remaining Questions - **Fixture churn:** Updating fixtures by hand was tedious but gives us a high-confidence baseline that future regressions will surface clearly. - **Runtime contract:** We intentionally kept the existing `{ element, index, array, params }` shape to minimise risk. If we ever adjust the runtime API we can simplify further, but that is out of scope for now. - **Future alignment:** Other transformers (e.g. handler closures, `derive`) can adopt the same capture-tree utilities so we continue converging on a single naming story. ## Recommended Next Steps 1. **Share this summary with stakeholders** so everyone understands the new shape and the reasons behind it. 2. **Apply the shared helpers** to upcoming work (handler closures, additional built-ins) to avoid drifting naming rules. 3. **Monitor for follow-on cleanups:** once handler closures land, reassess whether we can simplify the runtime contract or remove more legacy code paths. With these changes in place the closure transformer is significantly more predictable, and we have a solid foundation for the remaining roadmap items. ## Appendix ### Code & Fixture References - `packages/ts-transformers/src/closures/transformer.ts`: hierarchical capture tree and map callback rewriting logic. - `packages/ts-transformers/src/utils/capture-tree.ts`: rebuilds access expressions (including optional chaining) based on the original AST. - `packages/ts-transformers/src/utils/identifiers.ts`: shared identifier normalisation helpers. - `packages/ts-transformers/src/transformers/builtins/derive.ts`: derives share the same capture/alias strategy as map closures. Representative fixtures documenting the new behaviour: - Map closures: `packages/ts-transformers/test/fixtures/closures/map-outer-element.*`, `map-computed-alias-side-effect.*`, `map-destructured-numeric-alias.*`. - Derive transformer: `packages/ts-transformers/test/fixtures/ast-transform/ternary_derive.*` and unit test `test/derive/create-derive-call.test.ts`. - Optional chaining: `packages/ts-transformers/test/fixtures/jsx-expressions/optional-chain-captures.*`. ### Test Commands - `deno task test` (from `packages/ts-transformers/`) – runs unit and fixture suites covering closures, derive, and opaque-ref transforms. - `deno lint` / `deno fmt` – keep transformer and fixture files consistent (lint excludes expected fixtures by default).