@barefootjs/xslate 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 (50) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +30 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +94 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +73 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +96 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +25 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1412 -1323
  12. package/dist/adapter/lib/constants.d.ts +22 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +29 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/kolon-naming.d.ts +22 -0
  17. package/dist/adapter/lib/kolon-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +27 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +38 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/props/prop-classes.d.ts +32 -0
  23. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  24. package/dist/adapter/spread/spread-codegen.d.ts +69 -0
  25. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  26. package/dist/adapter/value/parsed-literal.d.ts +32 -0
  27. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  28. package/dist/adapter/xslate-adapter.d.ts +38 -92
  29. package/dist/adapter/xslate-adapter.d.ts.map +1 -1
  30. package/dist/build.js +1412 -1323
  31. package/dist/index.js +1412 -1323
  32. package/lib/BarefootJS/Backend/Xslate.pm +1 -1
  33. package/package.json +3 -3
  34. package/src/__tests__/query-href.test.ts +96 -0
  35. package/src/__tests__/xslate-adapter.test.ts +200 -8
  36. package/src/adapter/analysis/component-tree.ts +123 -0
  37. package/src/adapter/emit-context.ts +104 -0
  38. package/src/adapter/expr/array-method.ts +332 -0
  39. package/src/adapter/expr/emitters.ts +548 -0
  40. package/src/adapter/expr/operand.ts +35 -0
  41. package/src/adapter/lib/constants.ts +34 -0
  42. package/src/adapter/lib/ir-scope.ts +53 -0
  43. package/src/adapter/lib/kolon-naming.ts +27 -0
  44. package/src/adapter/lib/types.ts +30 -0
  45. package/src/adapter/memo/seed.ts +78 -0
  46. package/src/adapter/props/prop-classes.ts +64 -0
  47. package/src/adapter/spread/spread-codegen.ts +179 -0
  48. package/src/adapter/value/parsed-literal.ts +80 -0
  49. package/src/adapter/xslate-adapter.ts +190 -1236
  50. package/src/test-render.ts +3 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * ParsedExpr → Kolon emitters for the Text::Xslate template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Two `ParsedExprEmitter` implementations:
6
+ *
7
+ * - `XslateFilterEmitter` — filter/predicate context (loop param + local
8
+ * aliases + bare `$name` signal fallback); self-contained, reads no
9
+ * adapter state.
10
+ * - `XslateTopLevelEmitter` — top-level / per-render-var context; depends on
11
+ * the adapter only through the narrow `XslateEmitContext` seam.
12
+ */
13
+
14
+ import {
15
+ type ParsedExprEmitter,
16
+ type HigherOrderMethod,
17
+ type ArrayMethod,
18
+ type LiteralType,
19
+ type ParsedExpr,
20
+ type ObjectLiteralProperty,
21
+ type FlatDepth,
22
+ type TemplatePart,
23
+ identifierPath,
24
+ matchSearchParamsMethodCall,
25
+ sortComparatorFromArrow,
26
+ } from '@barefootjs/jsx'
27
+
28
+ import type { XslateEmitContext } from '../emit-context.ts'
29
+ import { XSLATE_TEMPLATE_PRIMITIVES } from '../lib/constants.ts'
30
+ import {
31
+ renderArrayMethod,
32
+ renderSortMethod,
33
+ renderSortEval,
34
+ renderReduceEval,
35
+ renderPredicateEval,
36
+ renderFlatMethod,
37
+ renderFlatMapEval,
38
+ renderMapEval,
39
+ } from './array-method.ts'
40
+
41
+ /**
42
+ * Local shape for the predicate-lowering helpers (`buildPredicateEval` and the
43
+ * Kolon-lambda fallback). Previously these read a `higher-order` ParsedExpr
44
+ * node directly; after the #2018 P5 collapse the callback arrives as a generic
45
+ * `call` and is destructured by `asCallbackMethodCall` / `callbackMethod`, so
46
+ * the helpers take this narrow record instead.
47
+ */
48
+ type PredicateCall = {
49
+ method: HigherOrderMethod
50
+ object: ParsedExpr
51
+ param: string
52
+ predicate: ParsedExpr
53
+ }
54
+
55
+ // Methods whose callback is a boolean predicate (`<recv>.<m>(x => …)`).
56
+ const PREDICATE_METHODS = new Set<HigherOrderMethod>([
57
+ 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'every', 'some',
58
+ ])
59
+
60
+ /**
61
+ * Lowering for the predicate body of a filter / every / some / find, plus the
62
+ * same shape used by `renderBlockBodyCondition` for complex block-body
63
+ * filters. Higher-order predicates are emitted using Kolon's own scalar
64
+ * comparison operators (which delegate to Perl semantics).
65
+ *
66
+ * NOTE: Kolon has no `grep { } @{...}` form, so a nested higher-order call
67
+ * (`x.tags.filter(...)`, `other.some(...)`) inside a predicate has no faithful
68
+ * scalar lowering here. Such predicates surface BF101 via `onUnsupported`
69
+ * (#2038) instead of silently degrading to the callback's receiver.
70
+ */
71
+ export class XslateFilterEmitter implements ParsedExprEmitter {
72
+ constructor(
73
+ private readonly param: string,
74
+ private readonly localVarMap: Map<string, string>,
75
+ private readonly isStringName: (n: string) => boolean = () => false,
76
+ // Records a BF101 for predicate shapes this emitter can only degrade
77
+ // (#2038). Optional so emitter construction stays possible without an
78
+ // adapter; a missing hook keeps the old silent-degrade emit.
79
+ private readonly onUnsupported?: (message: string, reason?: string) => void,
80
+ ) {}
81
+
82
+ identifier(name: string): string {
83
+ if (name === this.param) return `$${this.param}`
84
+ const signal = this.localVarMap.get(name)
85
+ if (signal) return `$${signal}`
86
+ return `$${name}`
87
+ }
88
+
89
+ literal(value: string | number | boolean | null, literalType: LiteralType): string {
90
+ if (literalType === 'string') return `'${value}'`
91
+ if (literalType === 'boolean') return value ? '1' : '0'
92
+ if (literalType === 'null') return 'nil'
93
+ return String(value)
94
+ }
95
+
96
+ member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
97
+ // `.length` — route through `$bf.length` (handles both array element
98
+ // count and string char count, JS-compatibly). Kolon's builtin `.size()`
99
+ // is array-only and faults on a string.
100
+ if (property === 'length') {
101
+ return `$bf.length(${emit(object)})`
102
+ }
103
+ // Hash field access — Kolon dot works on hash refs.
104
+ return `${emit(object)}.${property}`
105
+ }
106
+
107
+ indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
108
+ // Kolon's `[]` postfix is polymorphic (array index or hash key),
109
+ // mirroring JS — no array/hash split is needed (unlike Perl's
110
+ // `->[]` vs `->{}`). #1897 (data-table's `selected()[index]`).
111
+ return `${emit(object)}[${emit(index)}]`
112
+ }
113
+
114
+ call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
115
+ // Signal getter calls: filter() → $filter
116
+ if (callee.kind === 'identifier' && args.length === 0) {
117
+ return `$${callee.name}`
118
+ }
119
+ return emit(callee)
120
+ }
121
+
122
+ unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
123
+ const arg = emit(argument)
124
+ if (op === '!') {
125
+ const needsParens = argument.kind === 'binary' || argument.kind === 'logical'
126
+ return needsParens ? `!(${arg})` : `!${arg}`
127
+ }
128
+ if (op === '-') return `-${arg}`
129
+ return arg
130
+ }
131
+
132
+ binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
133
+ const l = emit(left)
134
+ const r = emit(right)
135
+ // Kolon's `==` / `!=` are value-equality operators that compare strings
136
+ // and numbers correctly — unlike Perl's numeric `==` (which the Mojo
137
+ // adapter must steer around with `eq`/`ne`). Kolon has no `eq`/`ne`
138
+ // operator at all, so string comparisons stay on `==` / `!=` here.
139
+ const opMap: Record<string, string> = {
140
+ '===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
141
+ '+': '+', '-': '-', '*': '*', '/': '/',
142
+ }
143
+ return `${l} ${opMap[op] ?? op} ${r}`
144
+ }
145
+
146
+ logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
147
+ const l = emit(left)
148
+ const r = emit(right)
149
+ if (op === '&&') return `(${l} && ${r})`
150
+ if (op === '||') return `(${l} || ${r})`
151
+ return `(${l} // ${r})`
152
+ }
153
+
154
+ callbackMethod(
155
+ method: string,
156
+ object: ParsedExpr,
157
+ _arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
158
+ _restArgs: ParsedExpr[],
159
+ emit: (e: ParsedExpr) => string,
160
+ ): string {
161
+ // A nested callback method inside a filter predicate has no Kolon scalar
162
+ // form. The pre-#2038 behavior degraded it to its receiver, which silently
163
+ // changes predicate semantics (`!other.some(r => …)` collapses to
164
+ // `!other`), so surface BF101 instead. The receiver emit is kept only so
165
+ // the template stays syntactically valid while the build fails.
166
+ this.onUnsupported?.(
167
+ `Filter predicate contains a nested '.${method}(...)' callback, which has no Kolon scalar form`,
168
+ `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
169
+ )
170
+ return emit(object)
171
+ }
172
+
173
+ arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
174
+ return `[${elements.map(emit).join(', ')}]`
175
+ }
176
+
177
+ arrayMethod(
178
+ method: ArrayMethod,
179
+ object: ParsedExpr,
180
+ args: ParsedExpr[],
181
+ emit: (e: ParsedExpr) => string,
182
+ ): string {
183
+ return renderArrayMethod(method, object, args, emit)
184
+ }
185
+
186
+ flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
187
+ return renderFlatMethod(emit(object), depth)
188
+ }
189
+
190
+ conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
191
+ return '1'
192
+ }
193
+
194
+ templateLiteral(_parts: TemplatePart[]): string {
195
+ return '1'
196
+ }
197
+
198
+ arrow(_params: string[], _body: ParsedExpr): string {
199
+ return '1'
200
+ }
201
+
202
+ regex(_raw: string): string {
203
+ return '1'
204
+ }
205
+
206
+ unsupported(_raw: string, _reason: string): string {
207
+ return '1'
208
+ }
209
+
210
+ objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
211
+ // Filter-predicate context: emit the truthy sentinel exactly as
212
+ // `unsupported` does, byte-identical with the pre-`object-literal`
213
+ // fallback (Roadmap A-1). Object values lower to Kolon hashrefs in the
214
+ // conditional/attr paths, not through this dispatcher.
215
+ return '1'
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Lowering for top-level expressions whose identifiers resolve against the
221
+ * Kolon template's per-render vars (signals, props, locals introduced by `:
222
+ * my $x = ...` lines). Differs from the filter emitter mainly in
223
+ * - `.length` → `.size()` (Kolon array length),
224
+ * - `conditional` is supported (filter predicates can't return ternaries),
225
+ * - higher-order methods route through `$bf` array helpers.
226
+ */
227
+ export class XslateTopLevelEmitter implements ParsedExprEmitter {
228
+ constructor(private readonly ctx: XslateEmitContext) {}
229
+
230
+ identifier(name: string): string {
231
+ // `undefined` / `null` nested inside a larger expression tree —
232
+ // Kolon `nil` (#1897).
233
+ if (name === 'undefined' || name === 'null') return 'nil'
234
+ // Inline a module-scope pure-string const (`const x = 'literal'`) — it
235
+ // never reaches the per-render stash, so a bare `$x` would render empty.
236
+ const inlined = this.ctx._resolveModuleStringConst(name)
237
+ if (inlined !== null) return inlined
238
+ // Same for a literal const of any scope (`const totalPages = 5`,
239
+ // #1897 pagination's `Page {currentPage()} of {totalPages}`).
240
+ const literalConst = this.ctx._resolveLiteralConst(name)
241
+ if (literalConst !== null) return literalConst
242
+ return `$${name}`
243
+ }
244
+
245
+ literal(value: string | number | boolean | null, literalType: LiteralType): string {
246
+ if (literalType === 'string') return `'${value}'`
247
+ if (literalType === 'boolean') return value ? '1' : '0'
248
+ if (literalType === 'null') return 'nil'
249
+ return String(value)
250
+ }
251
+
252
+ member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
253
+ // `props.x` flattens to the bare `$x` the SSR caller binds each prop to
254
+ // (props arrive as individual top-level vars, not a `$props` hashref).
255
+ if (object.kind === 'identifier' && object.name === 'props') {
256
+ return `$${property}`
257
+ }
258
+ // Static property access on a module object-literal const
259
+ // (`variantClasses.ghost`, #1897) resolves at compile time — the
260
+ // generic dot lowering below would reference a Kolon var that
261
+ // doesn't exist server-side and silently render ''.
262
+ if (object.kind === 'identifier') {
263
+ const staticValue = this.ctx._resolveStaticRecordLiteral(object.name, property)
264
+ if (staticValue !== null) return staticValue
265
+ }
266
+ const obj = emit(object)
267
+ // `.length` → `$bf.length` (array count or string char count, JS-compat);
268
+ // Kolon's builtin `.size()` is array-only and faults on a string.
269
+ if (property === 'length') return `$bf.length(${obj})`
270
+ // Kolon dot access works for hash refs.
271
+ return `${obj}.${property}`
272
+ }
273
+
274
+ indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
275
+ // Kolon's `[]` postfix is polymorphic (array index or hash key),
276
+ // mirroring JS. #1897 (data-table's `selected()[index]`).
277
+ return `${emit(object)}[${emit(index)}]`
278
+ }
279
+
280
+ call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
281
+ // Signal getter: count() → $count
282
+ if (callee.kind === 'identifier' && args.length === 0) {
283
+ return `$${callee.name}`
284
+ }
285
+ // Env-signal method call (#1922): `searchParams().get('sort')` is a real
286
+ // method call on the per-request `$searchParams` reader object, not the
287
+ // generic dot deref `member` would emit (`$searchParams.get`, which drops
288
+ // the arg). Matches the local import binding (incl. an alias).
289
+ if (this.ctx._searchParamsLocals.size > 0) {
290
+ const sp = matchSearchParamsMethodCall(callee, args, this.ctx._searchParamsLocals)
291
+ if (sp) {
292
+ return `$searchParams.${sp.method}(${sp.args.map(emit).join(', ')})`
293
+ }
294
+ }
295
+ // Identifier-path templatePrimitive: `JSON.stringify(x)` / `Math.floor(x)`
296
+ // → `$bf.json($x)` / `$bf.floor($x)`. Args render recursively through this
297
+ // same emitter. A wrong-arity call records BF101 and returns `''`.
298
+ const path = identifierPath(callee)
299
+ const spec = path ? XSLATE_TEMPLATE_PRIMITIVES[path] : undefined
300
+ if (path && spec) {
301
+ if (args.length === spec.arity) {
302
+ return spec.emit(args.map(emit))
303
+ }
304
+ this.ctx._recordExprBF101(
305
+ `templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`,
306
+ `Call '${path}' with exactly ${spec.arity} argument(s).`,
307
+ )
308
+ return "''"
309
+ }
310
+ return emit(callee)
311
+ }
312
+
313
+ unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
314
+ const arg = emit(argument)
315
+ if (op === '!') return `!${arg}`
316
+ if (op === '-') return `-${arg}`
317
+ return arg
318
+ }
319
+
320
+ binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
321
+ const l = emit(left)
322
+ const r = emit(right)
323
+ // Kolon's `==` / `!=` are value-equality operators handling both strings
324
+ // and numbers (unlike Perl's numeric `==`, which the Mojo adapter must
325
+ // route around with `eq`/`ne`). Kolon has no `eq`/`ne` operator, so all
326
+ // equality comparisons — string or numeric — stay on `==` / `!=`.
327
+ const opMap: Record<string, string> = {
328
+ '===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
329
+ '+': '+', '-': '-', '*': '*',
330
+ }
331
+ return `${l} ${opMap[op] ?? op} ${r}`
332
+ }
333
+
334
+ logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
335
+ const l = emit(left)
336
+ const r = emit(right)
337
+ if (op === '&&') return `(${l} && ${r})`
338
+ if (op === '||') return `(${l} || ${r})`
339
+ return `(${l} // ${r})`
340
+ }
341
+
342
+ callbackMethod(
343
+ method: string,
344
+ object: ParsedExpr,
345
+ arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
346
+ restArgs: ParsedExpr[],
347
+ emit: (e: ParsedExpr) => string,
348
+ ): string {
349
+ const recv = emit(object)
350
+ const body = arrow.body
351
+ const params = arrow.params
352
+
353
+ // Predicate family (#2018 P2/P5): `filter` / `find*` / `every` / `some`.
354
+ if (PREDICATE_METHODS.has(method as HigherOrderMethod)) {
355
+ return this._emitPredicateCallback(
356
+ { method: method as HigherOrderMethod, object, param: params[0], predicate: body },
357
+ recv,
358
+ emit,
359
+ )
360
+ }
361
+
362
+ // `.sort(cmp)` / `.toSorted(cmp)` (#2018): serialize the comparator body +
363
+ // emit `$bf.sort_eval`; fall back to the structured `$bf.sort` when the
364
+ // body is outside the evaluator surface (e.g. `localeCompare`). A
365
+ // comparator that neither serializes nor classifies → BF101.
366
+ if (method === 'sort' || method === 'toSorted') {
367
+ const evalForm = renderSortEval(recv, body, params, emit)
368
+ if (evalForm !== null) return evalForm
369
+ const structured = sortComparatorFromArrow(arrow)
370
+ if (structured !== null) return renderSortMethod(recv, structured)
371
+ this.ctx._recordExprBF101(
372
+ `'.${method}(...)' comparator is outside the Xslate adapter's evaluable / structured surface`,
373
+ `Pre-compute the sorted array, or move this position to a '/* @client */' boundary.`,
374
+ )
375
+ return "''"
376
+ }
377
+
378
+ // `.reduce(fn, init)` / `.reduceRight(fn, init)` (#2018): serialize the
379
+ // reducer body + emit `$bf.reduce_eval`. The init is the trailing arg.
380
+ if (method === 'reduce' || method === 'reduceRight') {
381
+ const direction = method === 'reduceRight' ? 'right' : 'left'
382
+ const init = restArgs[0]
383
+ const evalForm =
384
+ init !== undefined
385
+ ? renderReduceEval(recv, body, params, init, direction, emit)
386
+ : null
387
+ if (evalForm !== null) return evalForm
388
+ this.ctx._recordExprBF101(
389
+ `'.${method}(...)' is outside the Xslate adapter's evaluable surface (needs a literal initial value and an evaluable reducer body)`,
390
+ `Pre-compute the reduced value, or move this position to a '/* @client */' boundary.`,
391
+ )
392
+ return "''"
393
+ }
394
+
395
+ // `.flatMap(proj)` (#2018 P3): serialize the projection body + emit
396
+ // `$bf.flat_map_eval`.
397
+ if (method === 'flatMap') {
398
+ const evalForm = renderFlatMapEval(recv, body, params[0], emit)
399
+ if (evalForm !== null) return evalForm
400
+ this.ctx._recordExprBF101(
401
+ `'.flatMap(...)' projection is outside the Xslate adapter's evaluable surface`,
402
+ `Pre-compute the projected array, or move this position to a '/* @client */' boundary.`,
403
+ )
404
+ return "''"
405
+ }
406
+
407
+ // Value-producing `.map(cb)` (#2073): serialize the projection body +
408
+ // emit `$bf.map_eval`. (The JSX-returning `.map` is an IRLoop upstream.)
409
+ if (method === 'map') {
410
+ const evalForm = renderMapEval(recv, body, params[0], emit)
411
+ if (evalForm !== null) return evalForm
412
+ this.ctx._recordExprBF101(
413
+ `'.map(...)' projection is outside the Xslate adapter's evaluable surface`,
414
+ `Pre-compute the projected array, or move this position to a '/* @client */' boundary.`,
415
+ )
416
+ return "''"
417
+ }
418
+
419
+ // Unknown callback method (should not arrive — CALLBACK_METHODS is closed).
420
+ void object
421
+ return recv
422
+ }
423
+
424
+ /**
425
+ * Lower a boolean-predicate callback (`filter` / `find*` / `every` / `some`),
426
+ * extracted from the pre-#2018-P5 `higherOrder` arm. Higher-order array
427
+ * methods all take a JS arrow predicate, lowered to a Kolon lambda
428
+ * `-> $param { PRED }` (callable from Perl as a code ref), and go through the
429
+ * runtime object — consistent with the other array helpers ($bf.includes /
430
+ * $bf.slice / ...). `.find*` map to snake_case runtime methods. The
431
+ * `.filter(...).map(...)` *loop* form is handled separately by renderLoop's
432
+ * inline predicate.
433
+ */
434
+ private _emitPredicateCallback(
435
+ call: PredicateCall,
436
+ arrayExpr: string,
437
+ emit: (e: ParsedExpr) => string,
438
+ ): string {
439
+ const { method, object, param, predicate } = call
440
+
441
+ // Evaluator path (#2018 P2): serialize the predicate body + emit the
442
+ // matching `$bf.*_eval` helper (isomorphic with the Go adapter). Falls
443
+ // back to the Kolon-lambda runtime call below for a predicate the
444
+ // evaluator can't model (e.g. a method-call predicate).
445
+ const evalFn: Record<string, [string, boolean?]> = {
446
+ filter: ['filter_eval'], every: ['every_eval'], some: ['some_eval'],
447
+ find: ['find_eval', true], findLast: ['find_eval', false],
448
+ findIndex: ['find_index_eval', true], findLastIndex: ['find_index_eval', false],
449
+ }
450
+ // `.filter(Boolean)` (identity predicate `_t => _t`) keeps the lambda
451
+ // form — it composes through the array-method chain and renders
452
+ // identically to a truthiness filter.
453
+ const isIdentity =
454
+ method === 'filter' && predicate.kind === 'identifier' && predicate.name === param
455
+ const spec = evalFn[method]
456
+ if (spec && !isIdentity) {
457
+ const evalForm = renderPredicateEval(spec[0], arrayExpr, predicate, param, emit, spec[1])
458
+ if (evalForm !== null) return evalForm
459
+ }
460
+
461
+ const predBody = this.ctx._renderKolonFilterExprPublic(predicate, param)
462
+ const lambda = `-> $${param} { ${predBody} }`
463
+ const fn: Record<string, string> = {
464
+ filter: 'filter',
465
+ every: 'every',
466
+ some: 'some',
467
+ find: 'find',
468
+ findIndex: 'find_index',
469
+ findLast: 'find_last',
470
+ findLastIndex: 'find_last_index',
471
+ }
472
+ if (fn[method]) return `$bf.${fn[method]}(${arrayExpr}, ${lambda})`
473
+ return emit(object)
474
+ }
475
+
476
+ arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
477
+ return `[${elements.map(emit).join(', ')}]`
478
+ }
479
+
480
+ arrayMethod(
481
+ method: ArrayMethod,
482
+ object: ParsedExpr,
483
+ args: ParsedExpr[],
484
+ emit: (e: ParsedExpr) => string,
485
+ ): string {
486
+ return renderArrayMethod(method, object, args, emit)
487
+ }
488
+
489
+ flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
490
+ return renderFlatMethod(emit(object), depth)
491
+ }
492
+
493
+ conditional(
494
+ test: ParsedExpr,
495
+ consequent: ParsedExpr,
496
+ alternate: ParsedExpr,
497
+ emit: (e: ParsedExpr) => string,
498
+ ): string {
499
+ return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`
500
+ }
501
+
502
+ templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string {
503
+ // `` `n=${count() + 1}` `` → Kolon string concatenation (`~`):
504
+ // `'n=' ~ ($count + 1)`. Kolon's `~` is the explicit concat operator.
505
+ const terms: string[] = []
506
+ for (const part of parts) {
507
+ if (part.type === 'string') {
508
+ if (part.value !== '') {
509
+ terms.push(`'${part.value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`)
510
+ }
511
+ } else {
512
+ const rendered = emit(part.expr)
513
+ const needsParens =
514
+ part.expr.kind === 'binary' ||
515
+ part.expr.kind === 'logical' ||
516
+ part.expr.kind === 'conditional'
517
+ terms.push(needsParens ? `(${rendered})` : rendered)
518
+ }
519
+ }
520
+ if (terms.length === 0) return `''`
521
+ return terms.join(' ~ ')
522
+ }
523
+
524
+ arrow(_params: string[], _body: ParsedExpr): string {
525
+ // A bare arrow never stands alone at a render position (it's only
526
+ // meaningful as a callback, handled by `callbackMethod`). Emit the safe
527
+ // empty-string literal so a stray emit can't produce a Kolon syntax error.
528
+ return "''"
529
+ }
530
+
531
+ regex(_raw: string): string {
532
+ // A bare regex literal has no template-render form — mirror `unsupported`.
533
+ return "''"
534
+ }
535
+
536
+ unsupported(_raw: string, _reason: string): string {
537
+ return "''"
538
+ }
539
+
540
+ objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
541
+ // Mirror `unsupported`: a bare object literal reaching the dispatcher
542
+ // lowers to the safe empty-string literal, exactly as before the
543
+ // `object-literal` kind existed (byte-identical; Roadmap A-1). Object
544
+ // values that round-trip to a Kolon hashref go through the dedicated
545
+ // `objectLiteralToKolonHashref` lowering in the conditional/attr paths.
546
+ return "''"
547
+ }
548
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Operand-type classification for the Text::Xslate (Kolon) template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure function over `ParsedExpr` taking an `isStringName`
6
+ * predicate rather than reading adapter instance state.
7
+ *
8
+ * NOTE: unlike the Mojo adapter, Kolon's `==`/`!=` are value-equality
9
+ * operators that compare strings and numbers correctly, so the Kolon
10
+ * emitters never need to steer string comparisons onto `eq`/`ne`. This
11
+ * helper is therefore not consumed by the Kolon lowering today — it is kept
12
+ * as the parallel of the Mojo adapter's `expr/operand.ts` (groundwork for a
13
+ * future shared Perl-evaluator surface, issue #2018 track D).
14
+ */
15
+
16
+ import type { ParsedExpr } from '@barefootjs/jsx'
17
+
18
+ /**
19
+ * Whether a comparison operand is string-typed. In the Mojo adapter this
20
+ * selects Perl `eq`/`ne` over numeric `==`/`!=` for a `===`/`!==` against a
21
+ * string operand. The Kolon emitters do NOT consume it — Kolon's `==`/`!=`
22
+ * are value-equality operators that compare strings and numbers correctly, so
23
+ * `===`/`!==` always map to `==`/`!=` here (see the file header). Kept only as
24
+ * the parallel of the Mojo helper.
25
+ */
26
+ export function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string) => boolean): boolean {
27
+ if (expr.kind === 'literal' && expr.literalType === 'string') return true
28
+ if (expr.kind === 'call' && expr.callee.kind === 'identifier' && expr.args.length === 0) {
29
+ return isStringName(expr.callee.name)
30
+ }
31
+ if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
32
+ return isStringName(expr.property)
33
+ }
34
+ return false
35
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Compile-time constant tables for the Text::Xslate (Kolon) template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D).
6
+ */
7
+
8
+ import type { PrimitiveSpec } from './types.ts'
9
+
10
+ /**
11
+ * Single source of truth for the Xslate adapter's template-primitive
12
+ * surface. Each entry pairs the expected arity with the emit function.
13
+ *
14
+ * The emit fn returns a Kolon expression (no surrounding `<: :>`) suitable
15
+ * for embedding inside an interpolation — `$bf.json($val)`,
16
+ * `$bf.floor($val)`, etc. The same primitive names as the Mojo adapter, but
17
+ * invoked as `$bf.NAME(args)` on the runtime instance instead of `bf->NAME`.
18
+ */
19
+ export const XSLATE_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
20
+ 'JSON.stringify': { arity: 1, emit: (args) => `$bf.json(${args[0]})` },
21
+ 'String': { arity: 1, emit: (args) => `$bf.string(${args[0]})` },
22
+ 'Number': { arity: 1, emit: (args) => `$bf.number(${args[0]})` },
23
+ 'Math.floor': { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
24
+ 'Math.ceil': { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
25
+ 'Math.round': { arity: 1, emit: (args) => `$bf.round(${args[0]})` },
26
+ }
27
+
28
+ /**
29
+ * Module-scope `templatePrimitives` map derived once from the spec record.
30
+ */
31
+ export const XSLATE_PRIMITIVE_EMIT_MAP: Record<string, (args: string[]) => string> =
32
+ Object.fromEntries(
33
+ Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit])
34
+ )
@@ -0,0 +1,53 @@
1
+ /**
2
+ * IR traversal helpers for the Text::Xslate (Kolon) template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure functions over the IR tree — no adapter instance state.
6
+ *
7
+ * SHARED CANDIDATE: the bodies here are byte-identical to the Mojo
8
+ * adapter's `lib/ir-scope.ts`. They are adapter-agnostic (no Perl/Kolon
9
+ * specifics), so they are the obvious first extraction into a shared
10
+ * Perl-family codegen module once one exists — the groundwork for the
11
+ * future Perl evaluator integration (issue #2018 track D). Kept per-adapter
12
+ * for now, matching the repo convention (the Go adapter keeps its own copy).
13
+ */
14
+
15
+ import type { IRNode, IRProp, IRIfStatement, IRFragment } from '@barefootjs/jsx'
16
+
17
+ /**
18
+ * Find the `children` prop's `jsx-children` payload. Narrowed via the
19
+ * AttrValue `kind` discriminator so adapter code stays type-safe if the IR
20
+ * shape evolves.
21
+ */
22
+ export function resolveJsxChildrenProp(props: readonly IRProp[]): IRNode[] {
23
+ const prop = props.find(p => p.name === 'children')
24
+ if (!prop) return []
25
+ if (prop.value.kind !== 'jsx-children') return []
26
+ return prop.value.children
27
+ }
28
+
29
+ /**
30
+ * Collect the component's root scope element node(s) — the elements that
31
+ * become the rendered root and so carry `data-key` for a keyed loop item. A
32
+ * plain element root is itself; an `if-statement` (early-return) root
33
+ * contributes the top element of each branch, since exactly one renders at
34
+ * runtime. (#1297)
35
+ */
36
+ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
37
+ const out = new Set<IRNode>()
38
+ const visit = (n: IRNode | null): void => {
39
+ if (!n) return
40
+ if (n.type === 'element') { out.add(n); return }
41
+ if (n.type === 'if-statement') {
42
+ const s = n as IRIfStatement
43
+ visit(s.consequent)
44
+ visit(s.alternate)
45
+ return
46
+ }
47
+ if (n.type === 'fragment') {
48
+ for (const c of (n as IRFragment).children) visit(c)
49
+ }
50
+ }
51
+ visit(node)
52
+ return out
53
+ }