@barefootjs/go-template 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +23 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +53 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/helper-inline.d.ts +31 -0
  6. package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
  7. package/dist/adapter/expr/url-builder.d.ts +23 -0
  8. package/dist/adapter/expr/url-builder.d.ts.map +1 -0
  9. package/dist/adapter/go-template-adapter.d.ts +302 -1071
  10. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  11. package/dist/adapter/index.js +3116 -2692
  12. package/dist/adapter/lib/compile-state.d.ts +142 -0
  13. package/dist/adapter/lib/compile-state.d.ts.map +1 -0
  14. package/dist/adapter/lib/constants.d.ts +11 -0
  15. package/dist/adapter/lib/constants.d.ts.map +1 -0
  16. package/dist/adapter/lib/go-emit.d.ts +126 -0
  17. package/dist/adapter/lib/go-emit.d.ts.map +1 -0
  18. package/dist/adapter/lib/go-naming.d.ts +39 -0
  19. package/dist/adapter/lib/go-naming.d.ts.map +1 -0
  20. package/dist/adapter/lib/ir-scope.d.ts +13 -0
  21. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  22. package/dist/adapter/lib/types.d.ts +165 -0
  23. package/dist/adapter/lib/types.d.ts.map +1 -0
  24. package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
  25. package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
  26. package/dist/adapter/memo/memo-compute.d.ts +173 -0
  27. package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
  28. package/dist/adapter/memo/memo-type.d.ts +48 -0
  29. package/dist/adapter/memo/memo-type.d.ts.map +1 -0
  30. package/dist/adapter/memo/memo-value.d.ts +64 -0
  31. package/dist/adapter/memo/memo-value.d.ts.map +1 -0
  32. package/dist/adapter/memo/template-interp.d.ts +43 -0
  33. package/dist/adapter/memo/template-interp.d.ts.map +1 -0
  34. package/dist/adapter/props/prop-types.d.ts +31 -0
  35. package/dist/adapter/props/prop-types.d.ts.map +1 -0
  36. package/dist/adapter/spread/spread-codegen.d.ts +40 -0
  37. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  38. package/dist/adapter/type/type-codegen.d.ts +25 -0
  39. package/dist/adapter/type/type-codegen.d.ts.map +1 -0
  40. package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
  41. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
  42. package/dist/adapter/value/value-lowering.d.ts +46 -0
  43. package/dist/adapter/value/value-lowering.d.ts.map +1 -0
  44. package/dist/build.js +3114 -2690
  45. package/dist/index.js +3116 -2692
  46. package/dist/test-render.d.ts.map +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/derived-state-memo.test.ts +155 -84
  49. package/src/__tests__/go-template-adapter.test.ts +398 -54
  50. package/src/__tests__/lowering-plugin.test.ts +109 -0
  51. package/src/__tests__/query-href.test.ts +179 -0
  52. package/src/adapter/analysis/component-tree.ts +174 -0
  53. package/src/adapter/emit-context.ts +59 -0
  54. package/src/adapter/expr/helper-inline.ts +274 -0
  55. package/src/adapter/expr/url-builder.ts +123 -0
  56. package/src/adapter/go-template-adapter.ts +2189 -5320
  57. package/src/adapter/lib/compile-state.ts +181 -0
  58. package/src/adapter/lib/constants.ts +24 -0
  59. package/src/adapter/lib/go-emit.ts +309 -0
  60. package/src/adapter/lib/go-naming.ts +84 -0
  61. package/src/adapter/lib/ir-scope.ts +31 -0
  62. package/src/adapter/lib/types.ts +182 -0
  63. package/src/adapter/memo/ctor-lowering.ts +267 -0
  64. package/src/adapter/memo/memo-compute.ts +661 -0
  65. package/src/adapter/memo/memo-type.ts +93 -0
  66. package/src/adapter/memo/memo-value.ts +197 -0
  67. package/src/adapter/memo/template-interp.ts +246 -0
  68. package/src/adapter/props/prop-types.ts +84 -0
  69. package/src/adapter/spread/spread-codegen.ts +458 -0
  70. package/src/adapter/type/type-codegen.ts +95 -0
  71. package/src/adapter/value/parsed-literal-to-go.ts +94 -0
  72. package/src/adapter/value/value-lowering.ts +162 -0
  73. package/src/test-render.ts +48 -22
@@ -58,6 +58,13 @@ runAdapterConformanceTests({
58
58
  // a `SearchParams bf.SearchParams` binding (zero value → empty query → the
59
59
  // author's default). See #1922; Mojo / Xslate stay skipped pending their own
60
60
  // env-signal lowering + per-request Perl reader.
61
+ //
62
+ // `search-params-derived-filter` (#2075) is no longer skipped: a LIST-
63
+ // valued memo derived from the searchParams() env signal (the blog
64
+ // `visible` filter shape) now SSR-computes via `bf.FilterEval` — the
65
+ // memo's field type is `[]any` and its constructor init serializes the
66
+ // `.filter` predicate to the runtime evaluator. See the #2075 constructor
67
+ // pins below.
61
68
  skipJsx: [],
62
69
  // Per-fixture build-time contracts for shapes the Go template
63
70
  // adapter intentionally refuses to lower. Lives here (not on the
@@ -98,6 +105,17 @@ runAdapterConformanceTests({
98
105
  // (`cn\`base \${tone()}\``) likewise can't lower into Go template
99
106
  // syntax — same BF101 refusal.
100
107
  'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
108
+ // #2038: a filter predicate whose body contains a NESTED callback call
109
+ // (`t => !picked().some(p => …)` / `t => picked().find(p => …)`). The
110
+ // evaluator refuses nested arrows and `renderFilterExpr` has no faithful
111
+ // Go form for the inner call (its `call` arm used to silently drop the
112
+ // arrow argument and render only the callee) — the compiler is loud
113
+ // instead of lossy. The `/* @client */` twin
114
+ // (`filter-nested-callback-predicate-client`) has no pin here: it must
115
+ // render clean on every adapter, which asserts the suppression contract.
116
+ // https://github.com/piconic-ai/barefootjs/issues/2038
117
+ 'filter-nested-callback-predicate': [{ code: 'BF101', severity: 'error' }],
118
+ 'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error' }],
101
119
  // #1310: rest destructure in .map() callback. The object-rest shape read
102
120
  // via member access (`rest-destructure-object-in-map`) now lowers — each
103
121
  // binding resolves to a field on a synthetic `$__bf_item0` range var (the
@@ -154,6 +172,11 @@ runAdapterConformanceTests({
154
172
  // `string-trim` no longer pinned — pre-existing `bf_trim`
155
173
  // (wraps `strings.TrimSpace`) handles the strip (#1448 Tier A
156
174
  // ninth PR, closing out Tier A).
175
+ // #2073 follow-up: a function-reference `.map(format)` callback has no
176
+ // arrow body to serialize — not a CALLBACK_METHODS shape — so the
177
+ // UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
178
+ // a broken template.
179
+ 'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
157
180
  },
158
181
  // `JSON_STRINGIFY_VIA_CONST` and `MATH_FLOOR_VIA_CONST` now pass
159
182
  // via `GoTemplateAdapter.templatePrimitives` (#1188). The two
@@ -226,8 +249,9 @@ describe('GoTemplateAdapter - searchParams() env-signal lowering (#1922)', () =>
226
249
  test('lowers searchParams().get(k) to .SearchParams.Get and emits the struct binding', () => {
227
250
  const adapter = new GoTemplateAdapter()
228
251
  const ir = compileToIR(`
229
- import { searchParams } from '@barefootjs/client'
252
+ import { createSearchParams } from '@barefootjs/client'
230
253
  export function SortLabel() {
254
+ const [searchParams] = createSearchParams()
231
255
  return <p>{searchParams().get('sort') ?? 'none'}</p>
232
256
  }
233
257
  `, adapter)
@@ -237,14 +261,15 @@ export function SortLabel() {
237
261
  expect(types).toContain('SearchParams: in.SearchParams')
238
262
  })
239
263
 
240
- // An aliased import binds the env signal to a different local name; the
241
- // call `sp()` still resolves to the canonical `.SearchParams` field (the
242
- // generated struct field name is fixed, not derived from the JS alias).
243
- test('aliased import (`searchParams as sp`) resolves sp() to canonical .SearchParams', () => {
264
+ // An aliased destructured getter binds the env signal to a different local
265
+ // name; the call `sp()` still resolves to the canonical `.SearchParams` field
266
+ // (the generated struct field name is fixed, not derived from the JS name).
267
+ test('aliased env-signal getter (`const [sp] = createSearchParams()`) resolves sp() to canonical .SearchParams', () => {
244
268
  const adapter = new GoTemplateAdapter()
245
269
  const ir = compileToIR(`
246
- import { searchParams as sp } from '@barefootjs/client'
270
+ import { createSearchParams } from '@barefootjs/client'
247
271
  export function SortLabel() {
272
+ const [sp] = createSearchParams()
248
273
  return <p>{sp().get('sort') ?? 'none'}</p>
249
274
  }
250
275
  `, adapter)
@@ -267,8 +292,9 @@ describe('GoTemplateAdapter - template-literal text lowering (#1933)', () => {
267
292
  const adapter = new GoTemplateAdapter()
268
293
  const ir = compileToIR(`
269
294
  'use client'
270
- import { createMemo, searchParams } from '@barefootjs/client'
295
+ import { createMemo, createSearchParams } from '@barefootjs/client'
271
296
  export function StatusLine() {
297
+ const [searchParams] = createSearchParams()
272
298
  const tag = createMemo(() => searchParams().get('tag') ?? '')
273
299
  return (
274
300
  <div className="status">
@@ -735,6 +761,28 @@ export function Tags() {
735
761
  )
736
762
  })
737
763
 
764
+ test('bakes a numeric scalar array from the carried tree (Roadmap A-3)', () => {
765
+ // The analyzer carries `SignalInfo.parsed`, so the scalar-array bake
766
+ // reads the structured tree (`parsedLiteralToGo`) instead of re-parsing
767
+ // the value string with `ts.createSourceFile`. Each numeric element uses
768
+ // the carried raw token (= `NumericLiteral.text`), so the bake matches
769
+ // the fallback byte-for-byte — including TS's `.text` normalisation of
770
+ // `1e3` → `1000` and `0x10` → `16`.
771
+ const adapter = new GoTemplateAdapter()
772
+ const ir = compileToIR(`
773
+ "use client"
774
+ import { createSignal } from "@barefootjs/client"
775
+
776
+ export function Nums() {
777
+ const [ns] = createSignal<number[]>([1, 2, 1e3, 0x10])
778
+ return <ul>{ns().map((n) => <li key={n}>{n}</li>)}</ul>
779
+ }
780
+ `)
781
+ expect(adapter.generate(ir).types!).toContain(
782
+ 'Ns: []int{1, 2, 1000, 16},',
783
+ )
784
+ })
785
+
738
786
  test('synthesises a struct for an untyped object array and bakes it (#1680)', () => {
739
787
  // An untyped object array has no element type to bake against. Rather
740
788
  // than leave it nil (empty SSR loop), infer a struct from the literal's
@@ -1715,7 +1763,9 @@ export function TodoStatus() {
1715
1763
  return <div>{todos().every(t => t.done)}</div>
1716
1764
  }
1717
1765
  `)
1718
- expect(result.template).toContain('bf_every .Todos "Done"')
1766
+ // #2018 P2: `.every(t => t.done)` now lowers through the evaluator —
1767
+ // the predicate body travels as serialized ParsedExpr JSON.
1768
+ expect(result.template).toContain('bf_every_eval .Todos')
1719
1769
  })
1720
1770
 
1721
1771
  test('nested .filter(...).length in filter predicate lowers via len (bf_filter ...) (#1443 PR4)', () => {
@@ -1745,7 +1795,7 @@ export function TodoList() {
1745
1795
  }
1746
1796
  `, adapter)
1747
1797
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
1748
- expect(result.template).toContain('gt (len (bf_filter .Tags "Active" true)) 0')
1798
+ expect(result.template).toContain('gt (len (bf_filter_eval .Tags')
1749
1799
  // No degenerate fallbacks
1750
1800
  expect(result.template).not.toContain('{{if false}}')
1751
1801
  expect(result.template).not.toContain('[UNSUPPORTED-FILTER-EXPR]')
@@ -1776,7 +1826,7 @@ export function TodoList() {
1776
1826
  )
1777
1827
  }
1778
1828
  `, adapter)
1779
- expect(result.template).toContain('gt (len (bf_filter .Tags "Active" true)) 0')
1829
+ expect(result.template).toContain('gt (len (bf_filter_eval .Tags')
1780
1830
  expect(result.template).toContain('{{if not .Done}}')
1781
1831
  })
1782
1832
 
@@ -1803,6 +1853,31 @@ export function TodoList() {
1803
1853
  const bf101 = adapter.errors.filter(e => e.code === 'BF101')
1804
1854
  expect(bf101).toEqual([])
1805
1855
  })
1856
+
1857
+ test('nested-callback refusal keeps the emitted template syntactically valid (#2038)', () => {
1858
+ // The BF101 contract itself is pinned at the shared conformance layer
1859
+ // (`filter-nested-callback-predicate` / `filter-nested-find-predicate`
1860
+ // expectedDiagnostics; the `/* @client */` suppression twin renders
1861
+ // clean). This test pins the GO-SPECIFIC half: the refusal must emit
1862
+ // the `false` sentinel — not a half-rendered predicate like `.Some` —
1863
+ // so `text/template` parsing doesn't cascade into secondary errors.
1864
+ const adapter = new GoTemplateAdapter()
1865
+ const result = compileAndGenerate(`
1866
+ "use client"
1867
+ import { createSignal } from "@barefootjs/client"
1868
+
1869
+ type Item = { id: number }
1870
+
1871
+ export function Picker() {
1872
+ const [items, setItems] = createSignal<Item[]>([])
1873
+ const [picked, setPicked] = createSignal<Item[]>([])
1874
+ return <ul>{items().filter(t => !picked().some(p => p.id === t.id)).map(t => <li key={t.id}>{t.id}</li>)}</ul>
1875
+ }
1876
+ `, adapter)
1877
+ expect(adapter.errors.some(e => e.code === 'BF101')).toBe(true)
1878
+ expect(result.template).not.toContain('.Some')
1879
+ expect(result.template).toContain('{{if false}}')
1880
+ })
1806
1881
  })
1807
1882
 
1808
1883
  describe('find/findIndex - adapter specific', () => {
@@ -1837,7 +1912,10 @@ export function ItemChecker() {
1837
1912
  return <div>{items().find(t => t.done) ? 'Found' : 'Not found'}</div>
1838
1913
  }
1839
1914
  `)
1840
- expect(result.template).toContain('bf_find .Items "Done" true')
1915
+ // #2018 P2: `.find(t => t.done)` lowers through the evaluator; the
1916
+ // trailing `true` is the forward-search flag (find / findIndex).
1917
+ expect(result.template).toContain('bf_find_eval .Items')
1918
+ expect(result.template).toContain('true bf_env')
1841
1919
  expect(result.template).toContain('Found')
1842
1920
  })
1843
1921
  })
@@ -1855,7 +1933,10 @@ export function ItemChecker() {
1855
1933
  return <div>{items().findLast(t => t.done) ? 'Found' : 'Not found'}</div>
1856
1934
  }
1857
1935
  `)
1858
- expect(result.template).toContain('bf_find_last .Items "Done" true')
1936
+ // #2018 P2: `.findLast(t => t.done)` shares `bf_find_eval` with `.find`,
1937
+ // distinguished by the `false` (backward) forward-flag.
1938
+ expect(result.template).toContain('bf_find_eval .Items')
1939
+ expect(result.template).toContain('false bf_env')
1859
1940
  expect(result.template).toContain('Found')
1860
1941
  })
1861
1942
 
@@ -1889,10 +1970,20 @@ export function ItemChecker() {
1889
1970
  return <div>idx: {items().findLastIndex(t => t.done)}</div>
1890
1971
  }
1891
1972
  `)
1892
- expect(result.template).toContain('bf_find_last_index .Items "Done" true')
1893
- })
1894
-
1895
- test('renders findLastIndex() with complex predicate via range', () => {
1973
+ // #2018 P2: `.findLastIndex` lowers via `bf_find_index_eval` with the
1974
+ // `false` (backward) forward-flag.
1975
+ expect(result.template).toContain('bf_find_index_eval .Items')
1976
+ expect(result.template).toContain('false bf_env')
1977
+ })
1978
+
1979
+ test('renders findLastIndex() with a pure complex predicate via the evaluator', () => {
1980
+ // #2018 P2: a pure (call-free) complex predicate like
1981
+ // `t.price > 50 && t.active` is no longer confined to the
1982
+ // field-equality catalogue — it serializes to a ParsedExpr and lowers
1983
+ // through `bf_find_index_eval` (backward `false` flag), replacing the
1984
+ // old `{{range}}` $bf_r accumulator. The range fallback is now reserved
1985
+ // for predicates the evaluator can't model (e.g. a signal-getter call,
1986
+ // covered by the findLast test above).
1896
1987
  const result = compileAndGenerate(`
1897
1988
  "use client"
1898
1989
  import { createSignal } from "@barefootjs/client"
@@ -1904,10 +1995,9 @@ export function ItemFinder() {
1904
1995
  return <div>{items().findLastIndex(t => t.price > 50 && t.active)}</div>
1905
1996
  }
1906
1997
  `)
1907
- const varMatch = result.template.match(/(\$bf_r\d+) := -1/)
1908
- expect(varMatch).not.toBeNull()
1909
- expect(result.template).toContain(`${varMatch![1]} = $i`)
1910
- expect(result.template).not.toContain('{{break}}')
1998
+ expect(result.template).toContain('bf_find_index_eval .Items')
1999
+ expect(result.template).toContain('false bf_env')
2000
+ expect(result.template).not.toContain('$bf_r')
1911
2001
  })
1912
2002
 
1913
2003
  test('findLast() complex predicate in IR-level ternary works via preamble splitting', () => {
@@ -2428,7 +2518,9 @@ export function C() {
2428
2518
  return <ul>{items().filter(({done}) => done).map(t => <li key={t.id}>{t.name}</li>)}</ul>
2429
2519
  }`)
2430
2520
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
2431
- expect(result.template).toContain('bf_filter .Items "Done" true')
2521
+ // #2018 P2: the normalised `_t => _t.done` predicate lowers through the
2522
+ // evaluator (`bf_filter_eval`).
2523
+ expect(result.template).toContain('bf_filter_eval .Items')
2432
2524
  })
2433
2525
 
2434
2526
  test('.filter(function (x) { return x.done }).map(...) lowers cleanly', () => {
@@ -2441,7 +2533,9 @@ export function C() {
2441
2533
  return <ul>{items().filter(function (x) { return x.done }).map(t => <li key={t.id}>{t.name}</li>)}</ul>
2442
2534
  }`)
2443
2535
  expect(result.errors?.filter(e => e.code === 'BF101') ?? []).toEqual([])
2444
- expect(result.template).toContain('bf_filter .Items "Done" true')
2536
+ // #2018 P2: the normalised `_t => _t.done` predicate lowers through the
2537
+ // evaluator (`bf_filter_eval`).
2538
+ expect(result.template).toContain('bf_filter_eval .Items')
2445
2539
  })
2446
2540
  })
2447
2541
 
@@ -2837,19 +2931,23 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
2837
2931
  // #1448 Tier B — string → string, padded to a target width.
2838
2932
  { fixture: stringPadStartFixture, expect: 'bf_pad_start .Value 5 "0"' },
2839
2933
  { fixture: stringPadEndFixture, expect: 'bf_pad_end .Value 5 "."' },
2840
- // #1448 Tier B — sort / toSorted. Loop-chained shapes wrap the
2841
- // iterable in `bf_sort .Items <kind> <key> <type> <dir>`;
2842
- // standalone shapes inline the helper at the call site.
2843
- { fixture: arraySortFieldAscFixture, expect: 'bf_sort .Items "field" "Price" "numeric" "asc"' },
2844
- { fixture: arraySortFieldDescFixture, expect: 'bf_sort .Items "field" "Price" "numeric" "desc"' },
2845
- { fixture: arraySortPrimitiveFixture, expect: 'bf_sort .Nums "self" "" "numeric" "asc"' },
2934
+ // #1448 Tier B — sort / toSorted. Both the standalone shapes and the
2935
+ // `.sort().map()` loop-hoist now lower through the evaluator (#2018 P1/P3):
2936
+ // the comparator body travels as serialized ParsedExpr to `bf_sort_eval`.
2937
+ { fixture: arraySortFieldAscFixture, expect: 'bf_sort_eval .Items' },
2938
+ { fixture: arraySortFieldDescFixture, expect: 'bf_sort_eval .Items' },
2939
+ { fixture: arraySortPrimitiveFixture, expect: 'bf_sort_eval .Nums' },
2940
+ // localeCompare can't be evaluated, so it keeps the legacy `bf_sort` path
2941
+ // (both standalone and loop-hoist fall back).
2846
2942
  { fixture: arraySortLocaleFixture, expect: 'bf_sort .Names "self" "" "string" "asc"' },
2847
- // Multi-key (`||`-chain): one 4-string group per comparison key,
2848
- // applied in priority order as tie-breakers.
2943
+ // Multi-key (`||`-chain) here ends in a `localeCompare`, so the whole
2944
+ // comparator falls back to the structured `bf_sort` (one 4-string group
2945
+ // per comparison key, applied in priority order as tie-breakers).
2849
2946
  { fixture: arraySortMultiKeyFixture, expect: 'bf_sort .Items "field" "Price" "numeric" "asc" "field" "Name" "string" "asc"' },
2850
- // Relational-ternary comparator lowers to a single `auto` key.
2851
- { fixture: arraySortTernaryFixture, expect: 'bf_sort .Items "field" "Rank" "auto" "asc"' },
2852
- { fixture: arrayToSortedFixture, expect: 'bf_sort .Nums "self" "" "numeric" "asc"' },
2947
+ // Relational-ternary comparator a pure body, so it lowers via the
2948
+ // evaluator like the other field sorts.
2949
+ { fixture: arraySortTernaryFixture, expect: 'bf_sort_eval .Items' },
2950
+ { fixture: arrayToSortedFixture, expect: 'bf_sort_eval .Nums' },
2853
2951
  // #1448 Tier B — iteration shapes. These are loop-level
2854
2952
  // patterns (range binding order), not helper function calls.
2855
2953
  { fixture: arrayEntriesFixture, expect: '{{range $i, $v := .Items}}' },
@@ -2894,14 +2992,14 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
2894
2992
  // now listed in `UNSUPPORTED_METHODS`, so `isSupported` refuses them
2895
2993
  // and `convertExpressionToGo` records BF101 — the same treatment the
2896
2994
  // unsupported array methods already got. These tests pin that parity.
2897
- describe('GoTemplateAdapter - #1448 Tier C reduce field capitalisation', () => {
2898
- // #1728 review: `bf_reduce`'s projected field name must use the same
2899
- // initialism-aware capitalisation the adapter applies when generating
2900
- // struct fields (`capitalizeFieldName`), or the runtime reflect lookup
2901
- // misses the exported field (`id` struct `ID`, not `Id`) and folds a
2902
- // zero value. Pin the emitted key so a regression to plain
2903
- // first-letter capitalisation fails here.
2904
- test('reduce over an initialism field emits the Go-initialism key (ID, not Id)', () => {
2995
+ describe('GoTemplateAdapter - #2018 reduce via the evaluator', () => {
2996
+ // A standalone `.reduce(fn, init)` now lowers through the evaluator: the
2997
+ // reducer body is serialized to ParsedExpr JSON and folded by `bf_reduce_eval`.
2998
+ // The body carries the JS field name (`x.id`); the runtime field reader
2999
+ // (`getFieldValue`) resolves it case-variantly against the Go struct field
3000
+ // (`ID`) at render time, so no compile-time capitalisation is needed (the
3001
+ // former #1728 initialism gotcha is moot on this path).
3002
+ test('reduce over a field lowers to bf_reduce_eval with the serialized body', () => {
2905
3003
  const adapter = new GoTemplateAdapter()
2906
3004
  const ir = compileToIR(`
2907
3005
  function C({ items }: { items: { id: number }[] }) {
@@ -2910,8 +3008,9 @@ function C({ items }: { items: { id: number }[] }) {
2910
3008
  export { C }
2911
3009
  `, adapter)
2912
3010
  const template = adapter.generate(ir).template ?? ''
2913
- expect(template).toContain('bf_reduce .Items "+" "field" "ID" "numeric" "0"')
2914
- expect(template).not.toContain('"Id"')
3011
+ expect(template).toContain('bf_reduce_eval .Items')
3012
+ // The seed is threaded through bf_number; the body is the serialized tree.
3013
+ expect(template).toContain('(bf_number "0")')
2915
3014
  })
2916
3015
  })
2917
3016
 
@@ -2940,6 +3039,238 @@ export { C }
2940
3039
  })
2941
3040
  })
2942
3041
 
3042
+ describe('GoTemplateAdapter - #2075 searchParams()-derived memo constructor', () => {
3043
+ // A memo derived from the createSearchParams() env signal SSR-computes in
3044
+ // the generated constructor from the canonical `in.SearchParams` reader —
3045
+ // including under a local alias. `Get` returns "" for an absent key, so
3046
+ // the `??` default fires on "" — the same documented `?? → or` divergence
3047
+ // as the template-position lowering.
3048
+ test('computes an aliased `?? default` derived memo from in.SearchParams', () => {
3049
+ const adapter = new GoTemplateAdapter()
3050
+ const ir = compileToIR(`
3051
+ 'use client'
3052
+ import { createMemo, createSearchParams } from '@barefootjs/client'
3053
+ export function SortStatus() {
3054
+ const [sp] = createSearchParams()
3055
+ const sort = createMemo(() => sp().get('sort') ?? 'date')
3056
+ return <p>sort: {sort()}</p>
3057
+ }
3058
+ `, adapter)
3059
+ const { types } = adapter.generate(ir)
3060
+ expect(types).toContain(
3061
+ 'Sort: func() string { if v := in.SearchParams.Get("sort"); v != "" { return v }; return "date" }(),',
3062
+ )
3063
+ })
3064
+
3065
+ test('computes a bare env read memo from in.SearchParams', () => {
3066
+ const adapter = new GoTemplateAdapter()
3067
+ const ir = compileToIR(`
3068
+ 'use client'
3069
+ import { createMemo, createSearchParams } from '@barefootjs/client'
3070
+ export function QueryEcho() {
3071
+ const [searchParams] = createSearchParams()
3072
+ const q = createMemo(() => searchParams().get('q'))
3073
+ return <p>{q()}</p>
3074
+ }
3075
+ `, adapter)
3076
+ const { types } = adapter.generate(ir)
3077
+ expect(types).toContain('Q: in.SearchParams.Get("q"),')
3078
+ })
3079
+
3080
+ // #2075 residual: a LIST-valued memo (`.filter(arrow)` over a props array)
3081
+ // chained off the env-derived `tag` memo — the `search-params-derived-filter`
3082
+ // shared fixture's shape. The field is `[]any` (not the misclassified `bool`
3083
+ // the `/=>\s*!/` heuristic used to produce from the predicate's `!tag()`),
3084
+ // and the constructor seeds it via `bf.FilterEval`, whose serialized
3085
+ // predicate JSON is embedded (escaped) in the Go string literal.
3086
+ test('computes a list-filter memo chained off a searchParams()-derived memo via bf.FilterEval', () => {
3087
+ const adapter = new GoTemplateAdapter()
3088
+ const ir = compileToIR(`
3089
+ 'use client'
3090
+ import { createMemo, createSearchParams } from '@barefootjs/client'
3091
+ export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
3092
+ const [searchParams] = createSearchParams()
3093
+ const tag = createMemo(() => searchParams().get('tag') ?? '')
3094
+ const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
3095
+ return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
3096
+ }
3097
+ `, adapter)
3098
+ const { types } = adapter.generate(ir)
3099
+ expect(types).toContain('Visible []any')
3100
+ expect(types).toContain('Visible: bf.FilterEval(in.Items,')
3101
+ // The predicate JSON is escaped inside a Go double-quoted string literal,
3102
+ // so assert on the substring that survives escaping (backslash-escaped
3103
+ // quotes around the JSON keys/values).
3104
+ expect(types).toContain('\\"method\\":\\"includes\\"')
3105
+ })
3106
+
3107
+ // #2077 review finding 3: `tag` (an earlier sibling the `visible` filter's
3108
+ // predicate closes over) used to be recomputed verbatim a second time
3109
+ // inline in `visible`'s `bf.FilterEval` env map — the same
3110
+ // `in.SearchParams.Get("tag")` expression emitted twice in the constructor.
3111
+ // It's now hoisted to a shared local before the `return`, referenced by
3112
+ // BOTH the `Tag:` field and the env-map entry.
3113
+ test('hoists the earlier sibling memo instead of duplicating its expression', () => {
3114
+ const adapter = new GoTemplateAdapter()
3115
+ const ir = compileToIR(`
3116
+ 'use client'
3117
+ import { createMemo, createSearchParams } from '@barefootjs/client'
3118
+ export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
3119
+ const [searchParams] = createSearchParams()
3120
+ const tag = createMemo(() => searchParams().get('tag') ?? '')
3121
+ const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
3122
+ return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
3123
+ }
3124
+ `, adapter)
3125
+ const { types } = adapter.generate(ir)
3126
+ expect(types).toContain('var memoTag string =')
3127
+ expect(types).toContain('Tag: memoTag,')
3128
+ expect(types).toContain('"tag": memoTag')
3129
+ // The multi-token `Get("tag")` expression appears exactly once in the
3130
+ // whole constructor — the hoisted local, not a second inline copy.
3131
+ expect(types.split('in.SearchParams.Get("tag")').length).toBe(2)
3132
+ })
3133
+ })
3134
+
3135
+ describe('GoTemplateAdapter - env-derived memo sourced from the SSR seed plan (Package I)', () => {
3136
+ // `CompileState.ssrSeedPlan` (and `envSignalReadersByLocal` derived from it)
3137
+ // is now the single authority for env-reader recognition. `compileToIR`
3138
+ // already JSON-round-trips the IR (mirrors the adapter conformance
3139
+ // harness), so each `env-reader` step's `reader.methods` — a ReadonlySet —
3140
+ // arrives serialized to `{}`; `primeCompileState` re-resolves a live reader
3141
+ // via `envSignalReaderFor(step.reader.key)` before trusting `.methods`.
3142
+ // Pins that the env-derived memo constructor still SSR-computes through
3143
+ // that re-resolution instead of silently falling back to a zero value.
3144
+ test('computes an env-derived memo constructor through the JSON-round-tripped ssrSeedPlan', () => {
3145
+ const adapter = new GoTemplateAdapter()
3146
+ const ir = compileToIR(`
3147
+ 'use client'
3148
+ import { createMemo, createSearchParams } from '@barefootjs/client'
3149
+ export function QueryEcho() {
3150
+ const [searchParams] = createSearchParams()
3151
+ const q = createMemo(() => searchParams().get('q'))
3152
+ return <p>{q()}</p>
3153
+ }
3154
+ `, adapter)
3155
+ const { types } = adapter.generate(ir)
3156
+ expect(types).toContain('Q: in.SearchParams.Get("q"),')
3157
+ })
3158
+ })
3159
+
3160
+ describe('GoTemplateAdapter - #2077 review sibling-memo recursion guard', () => {
3161
+ // Finding 1: mutually-referencing memos (each resolving the other as a
3162
+ // filter-arm free var, or as a bare-getter ternary condition) used to
3163
+ // recurse without bound and blow the compiler's call stack
3164
+ // (`RangeError: Maximum call stack size exceeded`, verified by probe before
3165
+ // the fix). The cycle/declaration-order guard makes both directions fall
3166
+ // back to the field's Go zero value instead of throwing.
3167
+ test('mutual filter-arm memos compile without throwing and fall back to zero values', () => {
3168
+ const adapter = new GoTemplateAdapter()
3169
+ const ir = compileToIR(`
3170
+ 'use client'
3171
+ import { createMemo } from '@barefootjs/client'
3172
+ export function C(props: { items: { title: string }[] }) {
3173
+ const a = createMemo(() => props.items.filter((x) => Boolean(b())))
3174
+ const b = createMemo(() => props.items.filter((x) => Boolean(a())))
3175
+ return <ul>{a().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
3176
+ }
3177
+ `, adapter)
3178
+ let types = ''
3179
+ expect(() => { types = adapter.generate(ir).types }).not.toThrow()
3180
+ expect(types).toContain('A []any')
3181
+ expect(types).toContain('B []any')
3182
+ // Neither memo can resolve the OTHER as a free var (order rules out both
3183
+ // directions at once — `a` can't see `b` because `b` is declared later,
3184
+ // and vice versa), so `a`'s own computation falls back to the `[]any`
3185
+ // zero value (hoisted since `b` references it), and `b`'s free-var
3186
+ // resolution of `a` reuses that hoisted zero-valued local.
3187
+ expect(types).toContain('var memoA []any = nil')
3188
+ expect(types).toContain('A: memoA,')
3189
+ expect(types).toContain('B: bf.FilterEval(in.Items,')
3190
+ expect(types).toContain('map[string]any{"a": memoA}')
3191
+ })
3192
+
3193
+ test('mutual ternary memos (bare-getter condition) compile without throwing and fall back to zero values', () => {
3194
+ const adapter = new GoTemplateAdapter()
3195
+ const ir = compileToIR(`
3196
+ 'use client'
3197
+ import { createMemo } from '@barefootjs/client'
3198
+ export function C() {
3199
+ const a = createMemo(() => (b() ? 'A' : 'Z'))
3200
+ const b = createMemo(() => (a() ? 'B' : 'Y'))
3201
+ return <p>{a()}{b()}</p>
3202
+ }
3203
+ `, adapter)
3204
+ let types = ''
3205
+ expect(() => { types = adapter.generate(ir).types }).not.toThrow()
3206
+ expect(types).toContain('A: "",')
3207
+ expect(types).toContain('B: "",')
3208
+ })
3209
+
3210
+ test('self-referencing filter-arm memo compiles without throwing', () => {
3211
+ const adapter = new GoTemplateAdapter()
3212
+ const ir = compileToIR(`
3213
+ 'use client'
3214
+ import { createMemo } from '@barefootjs/client'
3215
+ export function C(props: { items: { title: string }[] }) {
3216
+ const a = createMemo(() => props.items.filter((x) => Boolean(a())))
3217
+ return <ul>{a().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
3218
+ }
3219
+ `, adapter)
3220
+ let types = ''
3221
+ expect(() => { types = adapter.generate(ir).types }).not.toThrow()
3222
+ expect(types).toContain('A: nil,')
3223
+ })
3224
+
3225
+ test('self-referencing ternary memo compiles without throwing', () => {
3226
+ const adapter = new GoTemplateAdapter()
3227
+ const ir = compileToIR(`
3228
+ 'use client'
3229
+ import { createMemo } from '@barefootjs/client'
3230
+ export function C() {
3231
+ const a = createMemo(() => (a() ? 'A' : 'Z'))
3232
+ return <p>{a()}</p>
3233
+ }
3234
+ `, adapter)
3235
+ let types = ''
3236
+ expect(() => { types = adapter.generate(ir).types }).not.toThrow()
3237
+ expect(types).toContain('A: "",')
3238
+ })
3239
+ })
3240
+
3241
+ describe('GoTemplateAdapter - #2073 value-producing .map(cb)', () => {
3242
+ function emitMap(expr: string): string {
3243
+ const adapter = new GoTemplateAdapter()
3244
+ const ir = compileToIR(`
3245
+ function C({ tags, users }: { tags: string[]; users: { name: string }[] }) {
3246
+ return <div>{${expr}}</div>
3247
+ }
3248
+ export { C }
3249
+ `, adapter)
3250
+ return adapter.generate(ir).template ?? ''
3251
+ }
3252
+
3253
+ // The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
3254
+ // projection, not JSX) lowers through the evaluator — `bf_map_eval` projects
3255
+ // each element (no flatten) and composes through `bf_join`. (Like the
3256
+ // flatMap pins below, the projection JSON's quotes are backslash-escaped in
3257
+ // the Go-template string; the JSON itself is verified by the render
3258
+ // conformance fixtures + the runtime TestMapEval.)
3259
+ test('.map(t => `#${t}`).join(" ") emits bf_map_eval composed into bf_join', () => {
3260
+ const t = emitMap("tags.map(t => `#${t}`).join(' ')")
3261
+ expect(t).toContain('bf_join (bf_map_eval .Tags')
3262
+ })
3263
+
3264
+ test('.map(u => u.name) emits bf_map_eval with the receiver', () => {
3265
+ const t = emitMap("users.map(u => u.name).join(', ')")
3266
+ expect(t).toContain('bf_map_eval .Users')
3267
+ })
3268
+
3269
+ // The function-reference `.map(format)` BF101 refusal is now covered
3270
+ // cross-adapter by the `array-map-function-reference` shared fixture's
3271
+ // `expectedDiagnostics` entry above.
3272
+ })
3273
+
2943
3274
  describe('GoTemplateAdapter - #1448 Tier C .flatMap(field projection)', () => {
2944
3275
  function emitFlatMap(expr: string): string {
2945
3276
  const adapter = new GoTemplateAdapter()
@@ -2952,20 +3283,32 @@ export { C }
2952
3283
  return adapter.generate(ir).template ?? ''
2953
3284
  }
2954
3285
 
2955
- test('.flatMap(i => i.field) emits bf_flat_map with the Go-capitalised field', () => {
2956
- expect(emitFlatMap('rows.flatMap(i => i.tags).join(" ")')).toContain('bf_flat_map .Rows "field" "Tags"')
3286
+ // #2018 P3: `.flatMap(proj)` lowers through the evaluator the projection
3287
+ // body serializes to ParsedExpr JSON and `bf_flat_map_eval` flattens the
3288
+ // results one level. The raw (lowercase) field name travels in the JSON; the
3289
+ // runtime field reader resolves it case-insensitively against Go data.
3290
+ // (The serialized projection JSON is embedded in a Go-template double-quoted
3291
+ // string — its quotes are backslash-escaped — so these pins assert the helper
3292
+ // + receiver; the projection JSON itself is verified by the render
3293
+ // conformance fixtures + the runtime TestFlatMapEval.)
3294
+ test('.flatMap(i => i.field) emits bf_flat_map_eval', () => {
3295
+ expect(emitFlatMap('rows.flatMap(i => i.tags).join(" ")')).toContain('bf_flat_map_eval .Rows')
2957
3296
  })
2958
3297
 
2959
- test('.flatMap(i => i) emits the self projection', () => {
2960
- expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain('bf_flat_map .Rows "self" ""')
3298
+ test('.flatMap(i => i) emits bf_flat_map_eval (self projection)', () => {
3299
+ expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain('bf_flat_map_eval .Rows')
2961
3300
  })
2962
3301
 
2963
- test('.flatMap(i => [i.a, i.b]) emits bf_flat_map_tuple with capitalised leaves', () => {
2964
- expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain('bf_flat_map_tuple .Rows "field" "A" "field" "B"')
3302
+ test('.flatMap(i => [i.a, i.b]) emits bf_flat_map_eval (array-literal projection)', () => {
3303
+ expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain('bf_flat_map_eval .Rows')
2965
3304
  })
2966
3305
 
2967
- test('tuple self + field leaves', () => {
2968
- expect(emitFlatMap('rows.flatMap(i => [i, i.a]).join(" ")')).toContain('bf_flat_map_tuple .Rows "self" "" "field" "A"')
3306
+ // (#2018 P5) A tuple with a string-literal element lowers the same way: the
3307
+ // whole array-literal body serializes and `bf_flat_map_eval` evaluates it per
3308
+ // item, so a non-member element no longer refuses (it did under the structured
3309
+ // `bf_flat_map_tuple` catalogue).
3310
+ test('.flatMap(i => [i.name, "x"]) emits bf_flat_map_eval (literal element)', () => {
3311
+ expect(emitFlatMap('rows.flatMap(i => [i.name, "x"]).join(" ")')).toContain('bf_flat_map_eval .Rows')
2969
3312
  })
2970
3313
  })
2971
3314
 
@@ -3019,9 +3362,10 @@ export function C() {
3019
3362
  // the no-initial-value form stays refused — JS throws on an empty
3020
3363
  // array there, which a template can't mirror.
3021
3364
  { name: 'reduce (no init)', expr: `items().reduce((a, b) => a + b.n)`, badEmit: '.Reduce' },
3022
- // Self / field / field-tuple `.flatMap` now lowers (#1448 Tier C); a
3023
- // tuple with a non-leaf element (here a string literal) stays refused.
3024
- { name: 'flatMap (literal element)', expr: `items().flatMap(i => [i.name, "x"])`, badEmit: '.FlatMap' },
3365
+ // (#2018 P5) The literal-element tuple `.flatMap(i => [i.name, "x"])` now
3366
+ // lowers through the evaluator (`bf_flat_map_eval` serializes the array-
3367
+ // literal body and flattens), so it is no longer refused pinned positively
3368
+ // in the `.flatMap` describe block alongside the `[i.a, i.b]` form.
3025
3369
  // Lowered methods whose MEANINGFUL extra argument isn't lowered yet
3026
3370
  // (#1448): the `fromIndex` of `.includes`/`.indexOf`/`.lastIndexOf`
3027
3371
  // and the variadic `.concat`. The parser refuses these (silently