@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.
- package/dist/adapter/analysis/component-tree.d.ts +23 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +53 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/helper-inline.d.ts +31 -0
- package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
- package/dist/adapter/expr/url-builder.d.ts +23 -0
- package/dist/adapter/expr/url-builder.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +302 -1071
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +3116 -2692
- package/dist/adapter/lib/compile-state.d.ts +142 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +11 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/go-emit.d.ts +126 -0
- package/dist/adapter/lib/go-emit.d.ts.map +1 -0
- package/dist/adapter/lib/go-naming.d.ts +39 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +13 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +165 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
- package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
- package/dist/adapter/memo/memo-compute.d.ts +173 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
- package/dist/adapter/memo/memo-type.d.ts +48 -0
- package/dist/adapter/memo/memo-type.d.ts.map +1 -0
- package/dist/adapter/memo/memo-value.d.ts +64 -0
- package/dist/adapter/memo/memo-value.d.ts.map +1 -0
- package/dist/adapter/memo/template-interp.d.ts +43 -0
- package/dist/adapter/memo/template-interp.d.ts.map +1 -0
- package/dist/adapter/props/prop-types.d.ts +31 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +40 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/type/type-codegen.d.ts +25 -0
- package/dist/adapter/type/type-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
- package/dist/adapter/value/value-lowering.d.ts +46 -0
- package/dist/adapter/value/value-lowering.d.ts.map +1 -0
- package/dist/build.js +3114 -2690
- package/dist/index.js +3116 -2692
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +155 -84
- package/src/__tests__/go-template-adapter.test.ts +398 -54
- package/src/__tests__/lowering-plugin.test.ts +109 -0
- package/src/__tests__/query-href.test.ts +179 -0
- package/src/adapter/analysis/component-tree.ts +174 -0
- package/src/adapter/emit-context.ts +59 -0
- package/src/adapter/expr/helper-inline.ts +274 -0
- package/src/adapter/expr/url-builder.ts +123 -0
- package/src/adapter/go-template-adapter.ts +2189 -5320
- package/src/adapter/lib/compile-state.ts +181 -0
- package/src/adapter/lib/constants.ts +24 -0
- package/src/adapter/lib/go-emit.ts +309 -0
- package/src/adapter/lib/go-naming.ts +84 -0
- package/src/adapter/lib/ir-scope.ts +31 -0
- package/src/adapter/lib/types.ts +182 -0
- package/src/adapter/memo/ctor-lowering.ts +267 -0
- package/src/adapter/memo/memo-compute.ts +661 -0
- package/src/adapter/memo/memo-type.ts +93 -0
- package/src/adapter/memo/memo-value.ts +197 -0
- package/src/adapter/memo/template-interp.ts +246 -0
- package/src/adapter/props/prop-types.ts +84 -0
- package/src/adapter/spread/spread-codegen.ts +458 -0
- package/src/adapter/type/type-codegen.ts +95 -0
- package/src/adapter/value/parsed-literal-to-go.ts +94 -0
- package/src/adapter/value/value-lowering.ts +162 -0
- 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 {
|
|
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
|
|
241
|
-
// call `sp()` still resolves to the canonical `.SearchParams` field
|
|
242
|
-
// generated struct field name is fixed, not derived from the JS
|
|
243
|
-
test('aliased
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
|
|
1908
|
-
expect(
|
|
1909
|
-
expect(result.template).toContain(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2841
|
-
//
|
|
2842
|
-
//
|
|
2843
|
-
{ fixture: arraySortFieldAscFixture, expect: '
|
|
2844
|
-
{ fixture: arraySortFieldDescFixture, expect: '
|
|
2845
|
-
{ fixture: arraySortPrimitiveFixture, expect: '
|
|
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)
|
|
2848
|
-
//
|
|
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
|
|
2851
|
-
|
|
2852
|
-
{ fixture:
|
|
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 - #
|
|
2898
|
-
//
|
|
2899
|
-
//
|
|
2900
|
-
//
|
|
2901
|
-
//
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
2904
|
-
test('reduce over
|
|
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('
|
|
2914
|
-
|
|
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
|
-
|
|
2956
|
-
|
|
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
|
|
2960
|
-
expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain('
|
|
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
|
|
2964
|
-
expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain('
|
|
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
|
-
|
|
2968
|
-
|
|
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
|
-
//
|
|
3023
|
-
//
|
|
3024
|
-
|
|
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
|