@barefootjs/jsx 0.17.0 → 0.18.0
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/adapters/env-signal.d.ts +42 -7
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/interface.d.ts +20 -4
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +19 -0
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +48 -2
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +24 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1728 -969
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +41 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +22 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +55 -18
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +13 -0
- package/dist/lowering-registry.d.ts.map +1 -1
- package/dist/relocate.d.ts +28 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
- package/src/__tests__/augment-inherited-props.test.ts +96 -0
- package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
- package/src/__tests__/compiler-stress-1244.test.ts +13 -4
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
- package/src/__tests__/destructured-map-params.test.ts +11 -1
- package/src/__tests__/expression-parser.test.ts +74 -3
- package/src/__tests__/free-identifiers.test.ts +55 -0
- package/src/__tests__/ir-sort-comparator.test.ts +261 -0
- package/src/__tests__/loop-destructure.test.ts +313 -0
- package/src/__tests__/loop-hoisted-template.test.ts +235 -0
- package/src/__tests__/materialize-getter-calls.test.ts +58 -0
- package/src/__tests__/props-destructuring.test.ts +110 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
- package/src/__tests__/ssr-defaults.test.ts +20 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
- package/src/__tests__/tagged-template-interleave.test.ts +268 -0
- package/src/__tests__/unsupported-expression.test.ts +194 -7
- package/src/adapters/env-signal.ts +57 -9
- package/src/adapters/interface.ts +20 -4
- package/src/adapters/parsed-expr-emitter.ts +19 -2
- package/src/analyzer-context.ts +20 -0
- package/src/analyzer.ts +74 -1
- package/src/augment-inherited-props.ts +139 -9
- package/src/compiler.ts +10 -1
- package/src/expression-parser.ts +421 -50
- package/src/index.ts +30 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -1
- package/src/ir-to-client-js/compute-inlinability.ts +6 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
- package/src/ir-to-client-js/emit-registration.ts +4 -2
- package/src/ir-to-client-js/html-template.ts +198 -1
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/types.ts +22 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/loop-destructure.ts +89 -36
- package/src/lowering-registry.ts +16 -0
- package/src/relocate.ts +201 -14
- package/src/ssr-defaults.ts +34 -32
- package/src/ssr-seed-plan.ts +146 -0
- package/src/types.ts +76 -0
package/src/expression-parser.ts
CHANGED
|
@@ -99,18 +99,40 @@ export type ParsedExpr =
|
|
|
99
99
|
object: ParsedExpr
|
|
100
100
|
args: ParsedExpr[]
|
|
101
101
|
}
|
|
102
|
-
// `.flat(depth?)` (#1448 Tier C). The flatten depth
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
102
|
+
// `.flat(depth?)` (#1448 Tier C, dynamic depth #2094). The flatten depth
|
|
103
|
+
// is validated at parse time — a literal integer / `Infinity` /
|
|
104
|
+
// unary-minus-literal normalises into a structured `FlatDepth` and the
|
|
105
|
+
// literal never reaches `args`, so adapters fold via a runtime helper
|
|
106
|
+
// instead of re-inspecting the depth argument. A non-literal depth that
|
|
107
|
+
// itself parses to a SUPPORTED `ParsedExpr` (a numeric prop, signal read,
|
|
108
|
+
// arithmetic, …) becomes a DYNAMIC depth: `depthExpr` carries that
|
|
109
|
+
// expression and `flatDepth` is a meaningless placeholder (`1`) — every
|
|
110
|
+
// consumer must check `depthExpr` first. A depth expression that itself
|
|
111
|
+
// doesn't resolve (an `unsupported` shape) still refuses with BF101 (the
|
|
112
|
+
// depth must be *expressible*, even if not known until render time). See
|
|
113
|
+
// the `.flat` arm in `convertNode`.
|
|
114
|
+
//
|
|
115
|
+
// Runtime coercion contract for a dynamic depth (JS `ToIntegerOrInfinity`,
|
|
116
|
+
// pinned by the `flat_dynamic` golden-vector cases): truncate toward
|
|
117
|
+
// zero; a NaN-producing value coerces to `0`; negative coerces to `0`
|
|
118
|
+
// (JS: negative depth never recurses, same as `0` — a shallow copy);
|
|
119
|
+
// `Infinity` / a huge finite value flattens fully. This is intentionally
|
|
120
|
+
// NOT the same runtime helper as the literal-depth path: the literal path
|
|
121
|
+
// pre-normalises `Infinity` to a `-1` *sentinel* meaning "flatten fully"
|
|
122
|
+
// (see `FlatDepth`'s doc), and a genuinely dynamic value of `-1` means
|
|
123
|
+
// the OPPOSITE (JS: no flatten) — reusing one helper for both would
|
|
124
|
+
// silently invert that case. Each adapter therefore routes a dynamic
|
|
125
|
+
// depth through a SEPARATE runtime helper (Go: `bf_flat_dynamic` /
|
|
126
|
+
// `FlatDynamicDepth`, distinct from the literal path's `bf_flat` / `Flat`)
|
|
127
|
+
// that performs the coercion above from scratch, never reusing the
|
|
128
|
+
// literal path's `-1`-means-infinite convention.
|
|
108
129
|
| {
|
|
109
130
|
kind: 'array-method'
|
|
110
131
|
method: 'flat'
|
|
111
132
|
object: ParsedExpr
|
|
112
133
|
args: []
|
|
113
134
|
flatDepth: FlatDepth
|
|
135
|
+
depthExpr?: ParsedExpr
|
|
114
136
|
}
|
|
115
137
|
| { kind: 'unsupported'; raw: string; reason: string }
|
|
116
138
|
|
|
@@ -247,8 +269,12 @@ export interface SupportResult {
|
|
|
247
269
|
const UNSUPPORTED_METHODS = new Set([
|
|
248
270
|
// Higher-order array methods. Seven of these (`filter`, `every`,
|
|
249
271
|
// `some`, `find`, `findIndex`, `findLast`, `findLastIndex`) are
|
|
250
|
-
// intercepted as `higher-order` IR before reaching this gate
|
|
251
|
-
// `map` is intercepted as an IRLoop
|
|
272
|
+
// intercepted as `higher-order` IR before reaching this gate.
|
|
273
|
+
// `map` is intercepted as an IRLoop when its callback returns JSX,
|
|
274
|
+
// and as a `CALLBACK_METHODS` evaluator lowering (`map_eval`, #2073)
|
|
275
|
+
// when it returns a value — it stays listed here so the fall-throughs
|
|
276
|
+
// (a bare `arr.map` reference, a function-reference callback) still
|
|
277
|
+
// refuse loudly. `reduce` / `reduceRight` stay
|
|
252
278
|
// listed here so the shapes the Tier C catalogue can't lower still
|
|
253
279
|
// refuse loudly: the `convertNode` call branch intercepts a matching
|
|
254
280
|
// `.reduce(fn, init)` / `.reduceRight(fn, init)` into the structured
|
|
@@ -603,11 +629,12 @@ export function tsNodeToParsedExpr(node: ts.Node): ParsedExpr {
|
|
|
603
629
|
* Higher-order array methods whose callback body the runtime evaluator drives
|
|
604
630
|
* (#2018). Recognised generically as a `call` whose callee is `<recv>.<method>`
|
|
605
631
|
* and whose first argument is an `arrow`; the adapter serializes the arrow body
|
|
606
|
-
* to the evaluator.
|
|
607
|
-
*
|
|
632
|
+
* to the evaluator. A JSX-returning `.map` / `.flatMap` is an IRLoop upstream
|
|
633
|
+
* and never reaches this recognition; the value-returning `.map(cb)` form
|
|
634
|
+
* (e.g. `tags.map(t => \`#${t}\`).join(' ')`) lowers via `map_eval` (#2073).
|
|
608
635
|
*/
|
|
609
636
|
export const CALLBACK_METHODS: ReadonlySet<string> = new Set([
|
|
610
|
-
'filter', 'every', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex',
|
|
637
|
+
'filter', 'map', 'every', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex',
|
|
611
638
|
'sort', 'toSorted', 'reduce', 'reduceRight', 'flatMap',
|
|
612
639
|
])
|
|
613
640
|
|
|
@@ -790,6 +817,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
790
817
|
if (callee.property === 'flat') {
|
|
791
818
|
const depthNode = node.arguments[0]
|
|
792
819
|
let flatDepth: FlatDepth
|
|
820
|
+
let depthExpr: ParsedExpr | undefined
|
|
793
821
|
if (depthNode === undefined) {
|
|
794
822
|
flatDepth = 1
|
|
795
823
|
} else if (ts.isIdentifier(depthNode) && depthNode.text === 'Infinity') {
|
|
@@ -806,16 +834,34 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
806
834
|
n = -Number(depthNode.operand.text)
|
|
807
835
|
}
|
|
808
836
|
if (n === undefined || Number.isNaN(n)) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
837
|
+
// Not a literal — try a DYNAMIC depth: an expression that
|
|
838
|
+
// itself resolves to a supported `ParsedExpr` (a numeric prop,
|
|
839
|
+
// signal read, arithmetic, …) can be lowered through a runtime
|
|
840
|
+
// helper that coerces it at render time (JS
|
|
841
|
+
// `ToIntegerOrInfinity`; see the `depthExpr` doc on the
|
|
842
|
+
// `array-method`/`flat` type). Still refuse with BF101 when
|
|
843
|
+
// the depth expression itself is outside the supported
|
|
844
|
+
// surface — the depth must be *expressible*, even if not
|
|
845
|
+
// known until render time.
|
|
846
|
+
const parsedDepth = convertNode(depthNode, raw)
|
|
847
|
+
if (checkSupport(parsedDepth).supported) {
|
|
848
|
+
depthExpr = parsedDepth
|
|
849
|
+
flatDepth = 1 // unused placeholder — consumers must check `depthExpr` first
|
|
850
|
+
} else {
|
|
851
|
+
return {
|
|
852
|
+
kind: 'unsupported',
|
|
853
|
+
raw,
|
|
854
|
+
reason: `\`.flat(depth)\` needs a literal integer, \`Infinity\`, or a supported dynamic depth expression — this depth can't be resolved. Use a literal depth, a supported expression (prop/signal/arithmetic), or pre-compute the value before the template.`,
|
|
855
|
+
}
|
|
813
856
|
}
|
|
857
|
+
} else {
|
|
858
|
+
const truncated = Math.trunc(n)
|
|
859
|
+
flatDepth = truncated < 0 ? 0 : truncated
|
|
814
860
|
}
|
|
815
|
-
const truncated = Math.trunc(n)
|
|
816
|
-
flatDepth = truncated < 0 ? 0 : truncated
|
|
817
861
|
}
|
|
818
|
-
return
|
|
862
|
+
return depthExpr !== undefined
|
|
863
|
+
? { kind: 'array-method', method: 'flat', object: callee.object, args: [], flatDepth, depthExpr }
|
|
864
|
+
: { kind: 'array-method', method: 'flat', object: callee.object, args: [], flatDepth }
|
|
819
865
|
}
|
|
820
866
|
// `.toLowerCase()` — string-only (the IR carries a value-builtin
|
|
821
867
|
// tag, not a receiver-type discriminator, so the `array-method`
|
|
@@ -1864,6 +1910,7 @@ function validateRestUsage(
|
|
|
1864
1910
|
case 'array-method':
|
|
1865
1911
|
walk(e.object)
|
|
1866
1912
|
for (const a of e.args) walk(a)
|
|
1913
|
+
if (e.method === 'flat' && e.depthExpr) walk(e.depthExpr)
|
|
1867
1914
|
return
|
|
1868
1915
|
case 'literal':
|
|
1869
1916
|
case 'unsupported':
|
|
@@ -1976,6 +2023,7 @@ function collectIdentifiers(expr: ParsedExpr, out: Set<string>): void {
|
|
|
1976
2023
|
case 'array-method':
|
|
1977
2024
|
collectIdentifiers(expr.object, out)
|
|
1978
2025
|
expr.args.forEach(e => collectIdentifiers(e, out))
|
|
2026
|
+
if (expr.method === 'flat' && expr.depthExpr) collectIdentifiers(expr.depthExpr, out)
|
|
1979
2027
|
return
|
|
1980
2028
|
case 'literal':
|
|
1981
2029
|
case 'regex':
|
|
@@ -2073,8 +2121,16 @@ function substituteDestructuredFields(
|
|
|
2073
2121
|
case 'array-method':
|
|
2074
2122
|
if (e.method === 'flat') {
|
|
2075
2123
|
// `flatDepth` is a normalised literal — no destructure refs to
|
|
2076
|
-
// substitute.
|
|
2077
|
-
|
|
2124
|
+
// substitute. `depthExpr` (a dynamic depth) DOES need the same
|
|
2125
|
+
// substitution as any other value position.
|
|
2126
|
+
return {
|
|
2127
|
+
kind: 'array-method',
|
|
2128
|
+
method: 'flat',
|
|
2129
|
+
object: walk(e.object),
|
|
2130
|
+
args: [],
|
|
2131
|
+
flatDepth: e.flatDepth,
|
|
2132
|
+
...(e.depthExpr ? { depthExpr: walk(e.depthExpr) } : {}),
|
|
2133
|
+
}
|
|
2078
2134
|
}
|
|
2079
2135
|
return { kind: 'array-method', method: e.method, object: walk(e.object), args: e.args.map(walk) }
|
|
2080
2136
|
case 'literal':
|
|
@@ -2197,6 +2253,16 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2197
2253
|
const argSupport = checkSupport(arg)
|
|
2198
2254
|
if (!argSupport.supported) return argSupport
|
|
2199
2255
|
}
|
|
2256
|
+
// A dynamic `.flat(depth)` carries its depth expression outside
|
|
2257
|
+
// `args` (see the `depthExpr` doc) — check it too. The parser
|
|
2258
|
+
// already gated this at parse time (only a SUPPORTED depth
|
|
2259
|
+
// expression becomes `depthExpr`), so this re-check is defensive:
|
|
2260
|
+
// it keeps `isSupported` total for any `array-method`/`flat` node
|
|
2261
|
+
// regardless of how it was constructed (e.g. by a rewrite walker).
|
|
2262
|
+
if (expr.method === 'flat' && expr.depthExpr) {
|
|
2263
|
+
const depthSupport = checkSupport(expr.depthExpr)
|
|
2264
|
+
if (!depthSupport.supported) return depthSupport
|
|
2265
|
+
}
|
|
2200
2266
|
return { supported: true, level: 'L2' }
|
|
2201
2267
|
}
|
|
2202
2268
|
|
|
@@ -2207,8 +2273,12 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2207
2273
|
// the receiver and the callback BODY are supported. Recognised before the
|
|
2208
2274
|
// `UNSUPPORTED_METHODS` gate so the eval-lowered shapes aren't refused
|
|
2209
2275
|
// (a BARE method reference — `arr.filter` uncalled, no arrow arg — still
|
|
2210
|
-
// falls through to the gate). A nested callback inside the body
|
|
2211
|
-
//
|
|
2276
|
+
// falls through to the gate). A nested callback inside the body is NOT
|
|
2277
|
+
// refused here: the evaluator refuses it (`serializeParsedExpr` → null)
|
|
2278
|
+
// and each adapter then either lowers it faithfully (Mojo's inline
|
|
2279
|
+
// `grep`, Go's `len (bf_filter_eval …)`) or surfaces BF101 at its
|
|
2280
|
+
// predicate fallback's exact degrade points (#2038) — a blanket refusal
|
|
2281
|
+
// here would break the faithful shapes (#1443 PR4).
|
|
2212
2282
|
const cb = asCallbackMethodCall(expr)
|
|
2213
2283
|
if (cb) {
|
|
2214
2284
|
const objSupport = checkSupport(cb.object)
|
|
@@ -2329,6 +2399,31 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2329
2399
|
case 'logical': {
|
|
2330
2400
|
const leftSupport = checkSupport(expr.left)
|
|
2331
2401
|
if (!leftSupport.supported) return leftSupport
|
|
2402
|
+
|
|
2403
|
+
// `x ?? {}` — admit an EMPTY object-literal fallback as the right
|
|
2404
|
+
// operand of `??` only. A standalone object literal stays refused
|
|
2405
|
+
// (the `object-literal` case above), and a non-empty one here still
|
|
2406
|
+
// falls through to the general `checkSupport(expr.right)` refusal
|
|
2407
|
+
// below. This is deliberately narrow, scoped to the one shape that
|
|
2408
|
+
// needs it (chart's `<Ctx.Provider value={{ config: props.config ??
|
|
2409
|
+
// {} }}>`):
|
|
2410
|
+
// - `??` only, not `&&` / `||` — a `{}` operand there plays a
|
|
2411
|
+
// different (always-truthy) role that each adapter's
|
|
2412
|
+
// boolean/filter dispatcher already handles by treating an
|
|
2413
|
+
// object-literal leaf as an unconditional-truthy sentinel; that
|
|
2414
|
+
// path doesn't need a real emitted value the way `??`'s value
|
|
2415
|
+
// position does.
|
|
2416
|
+
// - EMPTY object literal only (`properties.length === 0`) — the
|
|
2417
|
+
// one caller of this shape never needs populated fallback keys,
|
|
2418
|
+
// and restricting to empty means every adapter's `objectLiteral`
|
|
2419
|
+
// value-emitter only has to special-case the zero-property case
|
|
2420
|
+
// (emit its language's empty dict/hashref literal) rather than
|
|
2421
|
+
// also lowering arbitrary member values through the general
|
|
2422
|
+
// expression pipeline for SSR/CSR parity.
|
|
2423
|
+
if (expr.op === '??' && expr.right.kind === 'object-literal' && expr.right.properties.length === 0) {
|
|
2424
|
+
return { supported: true, level: 'L4' }
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2332
2427
|
const rightSupport = checkSupport(expr.right)
|
|
2333
2428
|
if (!rightSupport.supported) return rightSupport
|
|
2334
2429
|
|
|
@@ -2388,7 +2483,11 @@ export function containsHigherOrder(expr: ParsedExpr): boolean {
|
|
|
2388
2483
|
case 'array-literal':
|
|
2389
2484
|
return expr.elements.some(containsHigherOrder)
|
|
2390
2485
|
case 'array-method':
|
|
2391
|
-
return
|
|
2486
|
+
return (
|
|
2487
|
+
containsHigherOrder(expr.object) ||
|
|
2488
|
+
expr.args.some(containsHigherOrder) ||
|
|
2489
|
+
(expr.method === 'flat' && expr.depthExpr !== undefined && containsHigherOrder(expr.depthExpr))
|
|
2490
|
+
)
|
|
2392
2491
|
default:
|
|
2393
2492
|
return false
|
|
2394
2493
|
}
|
|
@@ -2741,7 +2840,10 @@ function usesPerPath(name: string, expr: ParsedExpr): { min: number; max: number
|
|
|
2741
2840
|
case 'array-literal':
|
|
2742
2841
|
return sum(e.elements)
|
|
2743
2842
|
case 'array-method':
|
|
2744
|
-
|
|
2843
|
+
if (e.method === 'flat') {
|
|
2844
|
+
return add(walk(e.object), e.depthExpr ? walk(e.depthExpr) : { min: 0, max: 0 })
|
|
2845
|
+
}
|
|
2846
|
+
return add(walk(e.object), sum(e.args))
|
|
2745
2847
|
case 'object-literal':
|
|
2746
2848
|
return sum(e.properties.map(p => p.value))
|
|
2747
2849
|
case 'arrow':
|
|
@@ -2817,7 +2919,14 @@ function inlineBinding(
|
|
|
2817
2919
|
return { kind: 'array-literal', elements: e.elements.map(el => walk(el, enclosing)) }
|
|
2818
2920
|
case 'array-method':
|
|
2819
2921
|
if (e.method === 'flat') {
|
|
2820
|
-
return {
|
|
2922
|
+
return {
|
|
2923
|
+
kind: 'array-method',
|
|
2924
|
+
method: 'flat',
|
|
2925
|
+
object: walk(e.object, enclosing),
|
|
2926
|
+
args: [],
|
|
2927
|
+
flatDepth: e.flatDepth,
|
|
2928
|
+
...(e.depthExpr ? { depthExpr: walk(e.depthExpr, enclosing) } : {}),
|
|
2929
|
+
}
|
|
2821
2930
|
}
|
|
2822
2931
|
return { kind: 'array-method', method: e.method, object: walk(e.object, enclosing), args: e.args.map(a => walk(a, enclosing)) }
|
|
2823
2932
|
case 'object-literal':
|
|
@@ -3033,8 +3142,12 @@ export function exprToString(expr: ParsedExpr): string {
|
|
|
3033
3142
|
return `[${expr.elements.map(exprToString).join(', ')}]`
|
|
3034
3143
|
case 'array-method':
|
|
3035
3144
|
if (expr.method === 'flat') {
|
|
3036
|
-
// Preserve the normalised depth so diagnostics don't
|
|
3037
|
-
// print `.flat()` for a `.flat(2)` / `.flat(Infinity)`
|
|
3145
|
+
// Preserve the normalised / dynamic depth so diagnostics don't
|
|
3146
|
+
// misleadingly print `.flat()` for a `.flat(2)` / `.flat(Infinity)`
|
|
3147
|
+
// / `.flat(depthProp)` source.
|
|
3148
|
+
if (expr.depthExpr) {
|
|
3149
|
+
return `${exprToString(expr.object)}.flat(${exprToString(expr.depthExpr)})`
|
|
3150
|
+
}
|
|
3038
3151
|
const d = expr.flatDepth
|
|
3039
3152
|
const depthSrc = d === 'infinity' ? 'Infinity' : String(d)
|
|
3040
3153
|
return `${exprToString(expr.object)}.flat(${d === 1 ? '' : depthSrc})`
|
|
@@ -3106,8 +3219,12 @@ export function stringifyParsedExpr(expr: ParsedExpr): string {
|
|
|
3106
3219
|
return `[${expr.elements.map(stringifyParsedExpr).join(', ')}]`
|
|
3107
3220
|
case 'array-method':
|
|
3108
3221
|
if (expr.method === 'flat') {
|
|
3109
|
-
// Round-trip the normalised depth back to JS for the
|
|
3110
|
-
// path: `'infinity'` → `Infinity`, `1` is left implicit
|
|
3222
|
+
// Round-trip the normalised / dynamic depth back to JS for the
|
|
3223
|
+
// CSR / Hono path: `'infinity'` → `Infinity`, `1` is left implicit
|
|
3224
|
+
// (`.flat()`), a dynamic depth round-trips its own expression.
|
|
3225
|
+
if (expr.depthExpr) {
|
|
3226
|
+
return `${stringifyParsedExpr(expr.object)}.flat(${stringifyParsedExpr(expr.depthExpr)})`
|
|
3227
|
+
}
|
|
3111
3228
|
const d = expr.flatDepth
|
|
3112
3229
|
const depthSrc = d === 'infinity' ? 'Infinity' : String(d)
|
|
3113
3230
|
return `${stringifyParsedExpr(expr.object)}.flat(${d === 1 ? '' : depthSrc})`
|
|
@@ -3121,6 +3238,85 @@ export function stringifyParsedExpr(expr: ParsedExpr): string {
|
|
|
3121
3238
|
}
|
|
3122
3239
|
}
|
|
3123
3240
|
|
|
3241
|
+
/**
|
|
3242
|
+
* Rewrite every zero-arg `call` node whose callee is a bare identifier in
|
|
3243
|
+
* `names` into that identifier — `tag()` → `tag` — leaving everything else
|
|
3244
|
+
* untouched. Returns a new tree; the input is never mutated.
|
|
3245
|
+
*
|
|
3246
|
+
* Rationale: in an SSR seed/constructor context a signal/memo getter call
|
|
3247
|
+
* reads the already-computed SEEDED value, so `tag()` reduces to "the value
|
|
3248
|
+
* bound to `tag`". Materialising the call lets the runtime evaluator (which
|
|
3249
|
+
* refuses any non-builtin call, {@link toEvalNode}'s `evalBuiltinCalleeName`
|
|
3250
|
+
* gate) evaluate a predicate that reads sibling memos — e.g. a `.filter`
|
|
3251
|
+
* predicate `(p) => !tag() || p.tags.includes(tag())` — with the getter's
|
|
3252
|
+
* value supplied through the evaluator's `base_env` instead of an
|
|
3253
|
+
* unsupported call node. `names` is caller-supplied (typically the sibling
|
|
3254
|
+
* signals/memos seeded alongside the derived memo being lowered), so a call
|
|
3255
|
+
* to an unrelated function is left as a `call` node and still refused by the
|
|
3256
|
+
* evaluator's builtin gate if it reaches `serializeParsedExpr`.
|
|
3257
|
+
*/
|
|
3258
|
+
export function materializeGetterCalls(expr: ParsedExpr, names: ReadonlySet<string>): ParsedExpr {
|
|
3259
|
+
const rw = (e: ParsedExpr): ParsedExpr => materializeGetterCalls(e, names)
|
|
3260
|
+
switch (expr.kind) {
|
|
3261
|
+
case 'call':
|
|
3262
|
+
if (
|
|
3263
|
+
expr.args.length === 0 &&
|
|
3264
|
+
expr.callee.kind === 'identifier' &&
|
|
3265
|
+
names.has(expr.callee.name)
|
|
3266
|
+
) {
|
|
3267
|
+
return { kind: 'identifier', name: expr.callee.name }
|
|
3268
|
+
}
|
|
3269
|
+
return { kind: 'call', callee: rw(expr.callee), args: expr.args.map(rw) }
|
|
3270
|
+
case 'binary':
|
|
3271
|
+
return { kind: 'binary', op: expr.op, left: rw(expr.left), right: rw(expr.right) }
|
|
3272
|
+
case 'logical':
|
|
3273
|
+
return { kind: 'logical', op: expr.op, left: rw(expr.left), right: rw(expr.right) }
|
|
3274
|
+
case 'unary':
|
|
3275
|
+
return { kind: 'unary', op: expr.op, argument: rw(expr.argument) }
|
|
3276
|
+
case 'conditional':
|
|
3277
|
+
return {
|
|
3278
|
+
kind: 'conditional',
|
|
3279
|
+
test: rw(expr.test),
|
|
3280
|
+
consequent: rw(expr.consequent),
|
|
3281
|
+
alternate: rw(expr.alternate),
|
|
3282
|
+
}
|
|
3283
|
+
case 'member':
|
|
3284
|
+
return { kind: 'member', object: rw(expr.object), property: expr.property, computed: expr.computed }
|
|
3285
|
+
case 'index-access':
|
|
3286
|
+
return { kind: 'index-access', object: rw(expr.object), index: rw(expr.index) }
|
|
3287
|
+
case 'template-literal':
|
|
3288
|
+
return {
|
|
3289
|
+
kind: 'template-literal',
|
|
3290
|
+
parts: expr.parts.map(p => (p.type === 'string' ? p : { type: 'expression', expr: rw(p.expr) })),
|
|
3291
|
+
}
|
|
3292
|
+
case 'array-literal':
|
|
3293
|
+
return { kind: 'array-literal', elements: expr.elements.map(rw) }
|
|
3294
|
+
case 'array-method':
|
|
3295
|
+
// `flat`'s `args` is always `[]` (the depth is carried structurally in
|
|
3296
|
+
// `flatDepth` / `depthExpr`, not `args`) — still rewrite `object` (and
|
|
3297
|
+
// `depthExpr`, when dynamic), just skip the `args.map` that every
|
|
3298
|
+
// other method needs.
|
|
3299
|
+
if (expr.method === 'flat') {
|
|
3300
|
+
return { ...expr, object: rw(expr.object), ...(expr.depthExpr ? { depthExpr: rw(expr.depthExpr) } : {}) }
|
|
3301
|
+
}
|
|
3302
|
+
return { ...expr, object: rw(expr.object), args: expr.args.map(rw) }
|
|
3303
|
+
case 'object-literal':
|
|
3304
|
+
return {
|
|
3305
|
+
kind: 'object-literal',
|
|
3306
|
+
raw: expr.raw,
|
|
3307
|
+
properties: expr.properties.map(p => ({ ...p, value: rw(p.value) })),
|
|
3308
|
+
}
|
|
3309
|
+
case 'arrow':
|
|
3310
|
+
return { kind: 'arrow', params: expr.params, body: rw(expr.body) }
|
|
3311
|
+
// Leaves / opaque shapes — nothing to rewrite.
|
|
3312
|
+
case 'identifier':
|
|
3313
|
+
case 'literal':
|
|
3314
|
+
case 'regex':
|
|
3315
|
+
case 'unsupported':
|
|
3316
|
+
return expr
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3124
3320
|
/**
|
|
3125
3321
|
* Serialize a pure-expression `ParsedExpr` (a higher-order callback body) into
|
|
3126
3322
|
* the minimal JSON the runtime evaluator consumes — the format pinned by the
|
|
@@ -3155,30 +3351,36 @@ export function serializeParsedExpr(expr: ParsedExpr): string | null {
|
|
|
3155
3351
|
*/
|
|
3156
3352
|
export function freeVarsInBody(body: ParsedExpr, params: ReadonlySet<string>): string[] {
|
|
3157
3353
|
const found = new Set<string>()
|
|
3158
|
-
|
|
3354
|
+
// `bound` starts as the outer callback's own `params` and grows when the
|
|
3355
|
+
// walk descends into a nested `map`/`filter` callback arrow (#2094) — a
|
|
3356
|
+
// NESTED arrow's own params shadow the outer ones for its body only,
|
|
3357
|
+
// mirroring {@link freeIdentifiers}'s lexical scoping. Before #2094 a
|
|
3358
|
+
// serializable body never contained a nested arrow, so this parameter
|
|
3359
|
+
// didn't need to change per call; it does now.
|
|
3360
|
+
const visit = (e: ParsedExpr, bound: ReadonlySet<string>): void => {
|
|
3159
3361
|
switch (e.kind) {
|
|
3160
3362
|
case 'identifier':
|
|
3161
|
-
if (!
|
|
3363
|
+
if (!bound.has(e.name)) found.add(e.name)
|
|
3162
3364
|
return
|
|
3163
3365
|
case 'binary':
|
|
3164
3366
|
case 'logical':
|
|
3165
|
-
visit(e.left)
|
|
3166
|
-
visit(e.right)
|
|
3367
|
+
visit(e.left, bound)
|
|
3368
|
+
visit(e.right, bound)
|
|
3167
3369
|
return
|
|
3168
3370
|
case 'unary':
|
|
3169
|
-
visit(e.argument)
|
|
3371
|
+
visit(e.argument, bound)
|
|
3170
3372
|
return
|
|
3171
3373
|
case 'conditional':
|
|
3172
|
-
visit(e.test)
|
|
3173
|
-
visit(e.consequent)
|
|
3174
|
-
visit(e.alternate)
|
|
3374
|
+
visit(e.test, bound)
|
|
3375
|
+
visit(e.consequent, bound)
|
|
3376
|
+
visit(e.alternate, bound)
|
|
3175
3377
|
return
|
|
3176
3378
|
case 'member':
|
|
3177
|
-
visit(e.object)
|
|
3379
|
+
visit(e.object, bound)
|
|
3178
3380
|
return
|
|
3179
3381
|
case 'index-access':
|
|
3180
|
-
visit(e.object)
|
|
3181
|
-
visit(e.index)
|
|
3382
|
+
visit(e.object, bound)
|
|
3383
|
+
visit(e.index, bound)
|
|
3182
3384
|
return
|
|
3183
3385
|
case 'call':
|
|
3184
3386
|
// A builtin callee (`String`/`Number`/`Boolean`, or `Math.<fn>`) is
|
|
@@ -3186,35 +3388,137 @@ export function freeVarsInBody(body: ParsedExpr, params: ReadonlySet<string>): s
|
|
|
3186
3388
|
// captured free var. Visiting it would add `Math` / `String` to the
|
|
3187
3389
|
// env, making the adapter emit an undefined `$Math` / `.Math` base_env
|
|
3188
3390
|
// entry (Copilot review #2031). Skip the callee identifier in that
|
|
3189
|
-
// case; the arguments are still real references and are visited
|
|
3190
|
-
|
|
3191
|
-
|
|
3391
|
+
// case; the arguments are still real references and are visited
|
|
3392
|
+
// (a nested `.map(cb)` / `.filter(cb)` callback call's `args[0]` is
|
|
3393
|
+
// the callback `arrow`, visited below with its own params bound).
|
|
3394
|
+
if (evalBuiltinCalleeName(e.callee) === null) visit(e.callee, bound)
|
|
3395
|
+
e.args.forEach((a) => visit(a, bound))
|
|
3192
3396
|
return
|
|
3193
3397
|
case 'template-literal':
|
|
3194
|
-
for (const p of e.parts) if (p.type === 'expression') visit(p.expr)
|
|
3398
|
+
for (const p of e.parts) if (p.type === 'expression') visit(p.expr, bound)
|
|
3195
3399
|
return
|
|
3196
3400
|
case 'array-literal':
|
|
3197
|
-
e.elements.forEach(visit)
|
|
3401
|
+
e.elements.forEach((el) => visit(el, bound))
|
|
3198
3402
|
return
|
|
3199
3403
|
case 'object-literal':
|
|
3200
3404
|
// Object *values* are references; keys are not. (Shorthand `{ x }`
|
|
3201
3405
|
// carries the ref on its `value` identifier, which is visited here.)
|
|
3202
|
-
for (const p of e.properties) visit(p.value)
|
|
3406
|
+
for (const p of e.properties) visit(p.value, bound)
|
|
3407
|
+
return
|
|
3408
|
+
case 'array-method':
|
|
3409
|
+
// `.includes(x)` / `.join(sep?)` are serializable ({@link
|
|
3410
|
+
// toEvalNode}); their `object` (the receiver) and `args` (needle /
|
|
3411
|
+
// separator) are the value positions serialized, so visit both when
|
|
3412
|
+
// the tree reaches here with either method. A nested `.map`/`.filter`
|
|
3413
|
+
// reaches this walk as a `call` (above), not an `array-method` — see
|
|
3414
|
+
// {@link asCallbackMethodCall}. Every other `array-method` is
|
|
3415
|
+
// non-serializable and doesn't occur in a serializable body.
|
|
3416
|
+
if (e.method === 'includes' || e.method === 'join') {
|
|
3417
|
+
visit(e.object, bound)
|
|
3418
|
+
e.args.forEach((a) => visit(a, bound))
|
|
3419
|
+
}
|
|
3203
3420
|
return
|
|
3421
|
+
// A nested callback arrow (the `.map`/`.filter` callback argument,
|
|
3422
|
+
// #2094): its own params shadow the outer bound set for its body only.
|
|
3423
|
+
case 'arrow': {
|
|
3424
|
+
const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
|
|
3425
|
+
visit(e.body, inner)
|
|
3426
|
+
return
|
|
3427
|
+
}
|
|
3204
3428
|
// Non-serializable kinds don't occur in a serializable body
|
|
3205
3429
|
// (serializeParsedExpr returns null for them); nothing to collect.
|
|
3206
3430
|
case 'literal':
|
|
3207
|
-
case 'array-method':
|
|
3208
|
-
case 'arrow':
|
|
3209
3431
|
case 'regex':
|
|
3210
3432
|
case 'unsupported':
|
|
3211
3433
|
return
|
|
3212
3434
|
}
|
|
3213
3435
|
}
|
|
3214
|
-
visit(body)
|
|
3436
|
+
visit(body, params)
|
|
3215
3437
|
return [...found].sort()
|
|
3216
3438
|
}
|
|
3217
3439
|
|
|
3440
|
+
/**
|
|
3441
|
+
* Every value-position identifier in `expr` NOT bound by an enclosing arrow's
|
|
3442
|
+
* own parameters — with proper lexical scoping: an arrow's params bind only
|
|
3443
|
+
* within that arrow's body, and nested arrows accumulate onto the enclosing
|
|
3444
|
+
* bound set. Unlike {@link freeVarsInBody} (which assumes a single flat param
|
|
3445
|
+
* set and never recurses into nested `arrow` nodes, since a serializable
|
|
3446
|
+
* evaluator body never contains one), this walks the full source-level tree —
|
|
3447
|
+
* including arrows — so a caller can ask "is this name free ANYWHERE in the
|
|
3448
|
+
* expression, honoring each arrow's own scope" rather than only within one
|
|
3449
|
+
* callback body.
|
|
3450
|
+
*
|
|
3451
|
+
* Walks the same value positions as {@link serializeParsedExpr} /
|
|
3452
|
+
* {@link freeVarsInBody}: call callee (skipped when it resolves to an
|
|
3453
|
+
* evaluator builtin — see {@link evalBuiltinCalleeName} — so `Math.floor(x)`
|
|
3454
|
+
* doesn't report `Math` as free) + args, binary/logical/unary operands,
|
|
3455
|
+
* conditional branches, a member's OBJECT only (the property name is not a
|
|
3456
|
+
* reference), an index-access's object + index, template-literal expression
|
|
3457
|
+
* parts, array-literal elements, array-method object + args, and an
|
|
3458
|
+
* object-literal's property VALUES (not keys). An `arrow` recurses into its
|
|
3459
|
+
* body with its own params added to the bound set.
|
|
3460
|
+
*
|
|
3461
|
+
* Returns `null` when the tree contains an `unsupported` node (or any other
|
|
3462
|
+
* shape this walk can't analyze) — the caller must fail safe rather than
|
|
3463
|
+
* assume nothing is free.
|
|
3464
|
+
*/
|
|
3465
|
+
export function freeIdentifiers(expr: ParsedExpr): Set<string> | null {
|
|
3466
|
+
const free = new Set<string>()
|
|
3467
|
+
|
|
3468
|
+
function visit(e: ParsedExpr, bound: ReadonlySet<string>): boolean {
|
|
3469
|
+
switch (e.kind) {
|
|
3470
|
+
case 'literal':
|
|
3471
|
+
case 'regex':
|
|
3472
|
+
return true
|
|
3473
|
+
case 'identifier':
|
|
3474
|
+
if (!bound.has(e.name)) free.add(e.name)
|
|
3475
|
+
return true
|
|
3476
|
+
case 'call': {
|
|
3477
|
+
const isBuiltinCallee = evalBuiltinCalleeName(e.callee) !== null
|
|
3478
|
+
if (!isBuiltinCallee && !visit(e.callee, bound)) return false
|
|
3479
|
+
for (const a of e.args) if (!visit(a, bound)) return false
|
|
3480
|
+
return true
|
|
3481
|
+
}
|
|
3482
|
+
case 'member':
|
|
3483
|
+
return visit(e.object, bound)
|
|
3484
|
+
case 'index-access':
|
|
3485
|
+
return visit(e.object, bound) && visit(e.index, bound)
|
|
3486
|
+
case 'binary':
|
|
3487
|
+
case 'logical':
|
|
3488
|
+
return visit(e.left, bound) && visit(e.right, bound)
|
|
3489
|
+
case 'unary':
|
|
3490
|
+
return visit(e.argument, bound)
|
|
3491
|
+
case 'conditional':
|
|
3492
|
+
return visit(e.test, bound) && visit(e.consequent, bound) && visit(e.alternate, bound)
|
|
3493
|
+
case 'template-literal':
|
|
3494
|
+
for (const p of e.parts) {
|
|
3495
|
+
if (p.type === 'expression' && !visit(p.expr, bound)) return false
|
|
3496
|
+
}
|
|
3497
|
+
return true
|
|
3498
|
+
case 'array-literal':
|
|
3499
|
+
for (const el of e.elements) if (!visit(el, bound)) return false
|
|
3500
|
+
return true
|
|
3501
|
+
case 'array-method':
|
|
3502
|
+
if (!visit(e.object, bound)) return false
|
|
3503
|
+
for (const a of e.args) if (!visit(a, bound)) return false
|
|
3504
|
+
if (e.method === 'flat' && e.depthExpr && !visit(e.depthExpr, bound)) return false
|
|
3505
|
+
return true
|
|
3506
|
+
case 'object-literal':
|
|
3507
|
+
for (const p of e.properties) if (!visit(p.value, bound)) return false
|
|
3508
|
+
return true
|
|
3509
|
+
case 'arrow': {
|
|
3510
|
+
const inner = new Set(bound)
|
|
3511
|
+
for (const p of e.params) inner.add(p)
|
|
3512
|
+
return visit(e.body, inner)
|
|
3513
|
+
}
|
|
3514
|
+
case 'unsupported':
|
|
3515
|
+
return false
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
return visit(expr, new Set()) ? free : null
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3218
3522
|
// Operators the evaluator implements (Go `eval.go` evalBinary / evalUnary, Perl
|
|
3219
3523
|
// `Evaluator.pm` _binary / _unary). An op outside these sets — loose `==`,
|
|
3220
3524
|
// `instanceof`, `**`, bitwise/shift, or the parser's `'unknown'` sentinel — is
|
|
@@ -3302,6 +3606,44 @@ function toEvalNode(e: ParsedExpr): Record<string, unknown> | null {
|
|
|
3302
3606
|
return object && index ? { kind: 'index-access', object, index } : null
|
|
3303
3607
|
}
|
|
3304
3608
|
case 'call': {
|
|
3609
|
+
// A nested `.map(cb)` / `.filter(cb)` callback call (#2094) — the
|
|
3610
|
+
// SAME recognition `callbackMethod()` dispatch uses. Only these two
|
|
3611
|
+
// widen into the evaluator's surface: they are order-preserving,
|
|
3612
|
+
// per-element, and produce a bounded result, so the pure gate can
|
|
3613
|
+
// still reason about them structurally. `sort` / `reduce` /
|
|
3614
|
+
// `reduceRight` / `flat` / `flatMap` / `every` / `some` / `find*`
|
|
3615
|
+
// nested INSIDE an eval body stay refused — `asCallbackMethodCall`
|
|
3616
|
+
// recognizes them too, but they fall through to the generic
|
|
3617
|
+
// non-builtin-callee refusal below because `cb.method` doesn't match
|
|
3618
|
+
// either case here.
|
|
3619
|
+
const cb = asCallbackMethodCall(e)
|
|
3620
|
+
if (cb && (cb.method === 'map' || cb.method === 'filter')) {
|
|
3621
|
+
const object = toEvalNode(cb.object)
|
|
3622
|
+
if (!object) return null
|
|
3623
|
+
const body = toEvalNode(cb.arrow.body)
|
|
3624
|
+
if (!body) return null
|
|
3625
|
+
// `cb.arrow.params` is always plain identifier names (the `ParsedExpr`
|
|
3626
|
+
// `arrow` variant only carries destructure-rewritten / plain params —
|
|
3627
|
+
// an array-binding-pattern param stays `unsupported` upstream and
|
|
3628
|
+
// never reaches here). 1- and 2-param arrows are both supported for
|
|
3629
|
+
// `map`/`filter` (element, and index when declared); the Go/eval-cases
|
|
3630
|
+
// reference pins the 2-param contract.
|
|
3631
|
+
//
|
|
3632
|
+
// Re-serialized in the SAME `call` / `member` / `arrow` shape
|
|
3633
|
+
// `asCallbackMethodCall` recognizes — NOT a bespoke wrapper — because
|
|
3634
|
+
// the `eval-vectors.json` golden corpus carries the genuine
|
|
3635
|
+
// `ParsedExpr` `parseExpression` produces (unfiltered by
|
|
3636
|
+
// `toEvalNode`; see `eval-generate.ts`), which is exactly this
|
|
3637
|
+
// shape. Using the same shape here means the compiled-template
|
|
3638
|
+
// embedded body and the golden-vector corpus reach the evaluator in
|
|
3639
|
+
// one encoding, so a single `EvalNode`/`evaluate` recognition path
|
|
3640
|
+
// (Go `eval.go`, `eval-reference.ts`) covers both.
|
|
3641
|
+
return {
|
|
3642
|
+
kind: 'call',
|
|
3643
|
+
callee: { kind: 'member', object, property: cb.method, computed: false },
|
|
3644
|
+
args: [{ kind: 'arrow', params: cb.arrow.params, body }],
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3305
3647
|
// The evaluator executes only the builtin allowlist; a non-builtin callee
|
|
3306
3648
|
// would evaluate to nil at runtime, so refuse it here (the purity gate).
|
|
3307
3649
|
if (evalBuiltinCalleeName(e.callee) === null) return null
|
|
@@ -3346,10 +3688,39 @@ function toEvalNode(e: ParsedExpr): Record<string, unknown> | null {
|
|
|
3346
3688
|
}
|
|
3347
3689
|
return { kind: 'object-literal', properties }
|
|
3348
3690
|
}
|
|
3691
|
+
case 'array-method': {
|
|
3692
|
+
// `.includes(x)` is one `array-method` the evaluator executes
|
|
3693
|
+
// (Go `eval.go` / Perl `Evaluator.pm`, includes support): the
|
|
3694
|
+
// receiver-type dispatch (array SameValueZero membership vs string
|
|
3695
|
+
// substring) happens at evaluator runtime, same as the SSR template
|
|
3696
|
+
// lowering's `bf_includes` / `$bf->includes`.
|
|
3697
|
+
if (e.method === 'includes' && e.args.length === 1) {
|
|
3698
|
+
const object = toEvalNode(e.object)
|
|
3699
|
+
const arg = toEvalNode(e.args[0])
|
|
3700
|
+
return object && arg ? { kind: 'array-method', method: 'includes', object, args: [arg] } : null
|
|
3701
|
+
}
|
|
3702
|
+
// `.join(sep?)` (#2094) — 0 or 1 serializable separator arg. Lets a
|
|
3703
|
+
// nested `.map`/`.filter` chain feed straight into `.join(' ')` inside
|
|
3704
|
+
// an eval body (the #1938 blog-showcase shape:
|
|
3705
|
+
// `p.tags.map(t => '#' + t)` is joined by the OUTER `.join`, which is
|
|
3706
|
+
// already the caller-visible template expression, not inside the eval
|
|
3707
|
+
// body — this arm is for a `.join` that itself appears NESTED inside
|
|
3708
|
+
// an eval body, e.g. a `.map(p => p.tags.join(','))` projection).
|
|
3709
|
+
if (e.method === 'join' && e.args.length <= 1) {
|
|
3710
|
+
const object = toEvalNode(e.object)
|
|
3711
|
+
if (!object) return null
|
|
3712
|
+
if (e.args.length === 0) return { kind: 'array-method', method: 'join', object, args: [] }
|
|
3713
|
+
const sep = toEvalNode(e.args[0])
|
|
3714
|
+
return sep ? { kind: 'array-method', method: 'join', object, args: [sep] } : null
|
|
3715
|
+
}
|
|
3716
|
+
// Every other `array-method` (`slice`, `flat`, `flatMap` [handled as
|
|
3717
|
+
// its own `call` shape when it has an arrow, so not reached here],
|
|
3718
|
+
// …) is outside the evaluator's surface and refuses.
|
|
3719
|
+
return null
|
|
3720
|
+
}
|
|
3349
3721
|
// Outside the evaluator's pure-expression surface — refuse so the caller
|
|
3350
3722
|
// falls back to BF101 / `@client`. A nested `arrow` (a callback inside the
|
|
3351
3723
|
// body) is refused here, keeping the evaluator non-recursive.
|
|
3352
|
-
case 'array-method':
|
|
3353
3724
|
case 'arrow':
|
|
3354
3725
|
case 'regex':
|
|
3355
3726
|
case 'unsupported':
|