@barefootjs/mojolicious 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 (53) 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 +96 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +85 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +78 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +34 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1482 -1339
  12. package/dist/adapter/lib/constants.d.ts +27 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +33 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/perl-naming.d.ts +29 -0
  17. package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +28 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +34 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/mojo-adapter.d.ts +46 -104
  23. package/dist/adapter/mojo-adapter.d.ts.map +1 -1
  24. package/dist/adapter/props/prop-classes.d.ts +48 -0
  25. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  26. package/dist/adapter/spread/spread-codegen.d.ts +64 -0
  27. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  28. package/dist/adapter/value/parsed-literal.d.ts +26 -0
  29. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  30. package/dist/build.js +1482 -1339
  31. package/dist/index.js +1482 -1339
  32. package/dist/test-render.d.ts.map +1 -1
  33. package/lib/BarefootJS/Backend/Mojo.pm +1 -1
  34. package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
  35. package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
  36. package/package.json +3 -3
  37. package/src/__tests__/mojo-adapter.test.ts +286 -69
  38. package/src/__tests__/query-href.test.ts +94 -0
  39. package/src/adapter/analysis/component-tree.ts +128 -0
  40. package/src/adapter/emit-context.ts +107 -0
  41. package/src/adapter/expr/array-method.ts +429 -0
  42. package/src/adapter/expr/emitters.ts +639 -0
  43. package/src/adapter/expr/operand.ts +55 -0
  44. package/src/adapter/lib/constants.ts +39 -0
  45. package/src/adapter/lib/ir-scope.ts +57 -0
  46. package/src/adapter/lib/perl-naming.ts +36 -0
  47. package/src/adapter/lib/types.ts +31 -0
  48. package/src/adapter/memo/seed.ts +73 -0
  49. package/src/adapter/mojo-adapter.ts +248 -1485
  50. package/src/adapter/props/prop-classes.ts +87 -0
  51. package/src/adapter/spread/spread-codegen.ts +181 -0
  52. package/src/adapter/value/parsed-literal.ts +34 -0
  53. package/src/test-render.ts +2 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Array / string method lowering for the Mojolicious EP template adapter.
3
+ *
4
+ * Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure free functions shared by both the filter-context emitter
6
+ * and the top-level emitter — they take an `emit` callback for receiver /
7
+ * argument recursion and read no adapter instance state.
8
+ */
9
+
10
+ import {
11
+ serializeParsedExpr,
12
+ freeVarsInBody,
13
+ } from '@barefootjs/jsx'
14
+ import type {
15
+ ParsedExpr,
16
+ ArrayMethod,
17
+ SortComparator,
18
+ FlatDepth,
19
+ } from '@barefootjs/jsx'
20
+
21
+ /**
22
+ * Lower an `arr.<method>(...)` / `str.<method>(...)` value-builtin call to
23
+ * its Perl form. The IR lifts these into the dedicated `array-method` kind at
24
+ * parse time (see the `arrayMethod` emitter arms), so this is the single
25
+ * place every adapter-supported array/string method is mapped. An unhandled
26
+ * `ArrayMethod` variant throws rather than emitting a silent no-op — the
27
+ * drift defence we already apply to `ParsedExpr.kind` extended to its
28
+ * sub-discriminator.
29
+ */
30
+ export function renderArrayMethod(
31
+ method: ArrayMethod,
32
+ object: ParsedExpr,
33
+ args: ParsedExpr[],
34
+ emit: (e: ParsedExpr) => string,
35
+ ): string {
36
+ switch (method) {
37
+ case 'join': {
38
+ // arr.join(sep) → join(sep, @{arr}). The default `${obj}->{join}`
39
+ // hash-lookup fallback would emit invalid Perl, which is why the
40
+ // IR carves out a dedicated method node instead of routing
41
+ // through the generic call dispatcher. `.join()` defaults the
42
+ // separator to `,` (JS) and ignores any extra argument.
43
+ const obj = emit(object)
44
+ const sep = args.length >= 1 ? emit(args[0]) : `','`
45
+ return `join(${sep}, @{${obj}})`
46
+ }
47
+ case 'includes': {
48
+ // Both `arr.includes(x)` and `str.includes(sub)` route here —
49
+ // the parser can't disambiguate the receiver type. The Mojo
50
+ // runtime's `bf->includes($recv, $elem)` inspects `ref($recv)`
51
+ // and dispatches: ARRAY ref scans the list with `eq`, scalar
52
+ // falls back to `index(..., ...) != -1`. Helper lives in
53
+ // packages/adapter-perl/lib/BarefootJS.pm.
54
+ //
55
+ // The `bf->` (no `$`) form matches every other helper emit —
56
+ // in real Mojolicious `bf` is a controller helper; the
57
+ // standalone test-render in test-render.ts rewrites the bare
58
+ // `bf->` to `$bf->` so both render paths stay consistent.
59
+ const obj = emit(object)
60
+ const needle = emit(args[0])
61
+ return `bf->includes(${obj}, ${needle})`
62
+ }
63
+ case 'indexOf':
64
+ case 'lastIndexOf': {
65
+ // Array `.indexOf(x)` / `.lastIndexOf(x)` value-equality
66
+ // search. The Perl helpers (`bf->index_of`, `bf->last_index_of`)
67
+ // walk the array forward / backward and compare with `eq`
68
+ // (with defined/undef parity). The existing `.find` lowering
69
+ // uses Perl `grep` for struct-field find — disjoint surface,
70
+ // disjoint helpers.
71
+ const fn = method === 'indexOf' ? 'index_of' : 'last_index_of'
72
+ const obj = emit(object)
73
+ const needle = emit(args[0])
74
+ return `bf->${fn}(${obj}, ${needle})`
75
+ }
76
+ case 'at': {
77
+ // `.at(i)` with negative-index support — `.at(-1)` is the
78
+ // last element. The Mojo helper wraps the same `length + i`
79
+ // arithmetic the Go `bf_at` does so the lowering stays
80
+ // symmetric across adapters. `.at()` with no argument is `.at(0)`
81
+ // (the first element); extra arguments are ignored.
82
+ const obj = emit(object)
83
+ const idx = args.length >= 1 ? emit(args[0]) : '0'
84
+ return `bf->at(${obj}, ${idx})`
85
+ }
86
+ case 'concat': {
87
+ // `.concat(other)` merges two arrays. Returns a new ARRAY
88
+ // ref so the result composes with `.join(...)` / other
89
+ // array-shape methods downstream (the canonical Tier A
90
+ // conformance fixture chains `.concat(...).join(' ')`).
91
+ // `.concat()` with no argument is a shallow copy — indistinguishable
92
+ // from the receiver in an SSR snapshot, so it lowers to the receiver.
93
+ if (args.length === 0) {
94
+ return emit(object)
95
+ }
96
+ const a = emit(object)
97
+ const b = emit(args[0])
98
+ return `bf->concat(${a}, ${b})`
99
+ }
100
+ case 'slice': {
101
+ // `.slice()` / `.slice(start)` / `.slice(start, end)`. The Mojo
102
+ // helper mirrors the Go arithmetic (negative-index normalisation,
103
+ // out-of-bounds clamping, empty result on start >= end). A
104
+ // missing `start` defaults to 0 (full copy); an absent `end`
105
+ // lowers as `undef`, which the helper treats as "to length". JS
106
+ // ignores a third+ argument. Returns a new ARRAY ref so the
107
+ // result composes with `.join(...)` downstream.
108
+ const recv = emit(object)
109
+ const start = args.length >= 1 ? emit(args[0]) : '0'
110
+ const end = args.length >= 2 ? emit(args[1]) : 'undef'
111
+ return `bf->slice(${recv}, ${start}, ${end})`
112
+ }
113
+ case 'reverse':
114
+ case 'toReversed': {
115
+ // Both shapes share a lowering — see the parser arm + Go
116
+ // emit for the SSR-mutation-rationale. Returns a new ARRAY
117
+ // ref so the result composes with `.join(...)` downstream.
118
+ const recv = emit(object)
119
+ return `bf->reverse(${recv})`
120
+ }
121
+ case 'toLowerCase': {
122
+ // Perl's native `lc` is the obvious lowering — no helper
123
+ // method needed. The receiver flows through `emit` so any
124
+ // upstream coercion (`$value`, `$bf->string(...)`, etc.)
125
+ // composes naturally.
126
+ const recv = emit(object)
127
+ return `lc(${recv})`
128
+ }
129
+ case 'toUpperCase': {
130
+ // Perl's native `uc` — mirrors `toLowerCase` exactly.
131
+ const recv = emit(object)
132
+ return `uc(${recv})`
133
+ }
134
+ case 'trim': {
135
+ // No Perl native `trim`; route through the `bf->trim`
136
+ // helper so the regex stays in one place (and so an undef
137
+ // receiver doesn't trigger a warning about applying `s///`
138
+ // to undef).
139
+ const recv = emit(object)
140
+ return `bf->trim(${recv})`
141
+ }
142
+ case 'toFixed': {
143
+ // `.toFixed(digits?)` — Number → fixed-decimal string. `bf->to_fixed`
144
+ // mirrors JS rounding + zero-padding (default 0 digits). #1897.
145
+ const recv = emit(object)
146
+ const digits = args.length >= 1 ? emit(args[0]) : '0'
147
+ return `bf->to_fixed(${recv}, ${digits})`
148
+ }
149
+ case 'split': {
150
+ // `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
151
+ // ARRAY ref via `bf->split`. With no separator the helper returns
152
+ // the whole string as a single element; otherwise it quotemetas
153
+ // the separator (literal match, not regex) and keeps trailing
154
+ // empties (`-1`), staying byte-equal with Go's `bf_split`. The
155
+ // optional `limit` caps the pieces; JS ignores a third+ argument.
156
+ // See #1448 Tier B.
157
+ const recv = emit(object)
158
+ if (args.length === 0) {
159
+ return `bf->split(${recv})`
160
+ }
161
+ const sep = emit(args[0])
162
+ if (args.length === 1) {
163
+ return `bf->split(${recv}, ${sep})`
164
+ }
165
+ const limit = emit(args[1])
166
+ return `bf->split(${recv}, ${sep}, ${limit})`
167
+ }
168
+ case 'startsWith':
169
+ case 'endsWith': {
170
+ // `.startsWith(prefix, position?)` / `.endsWith(suffix,
171
+ // endPosition?)` — string → boolean. The Perl helpers
172
+ // (`bf->starts_with` / `bf->ends_with`) do a `substr`-anchored
173
+ // comparison so the search string is matched literally (no regex
174
+ // metachar surprises) and undef receivers stay quiet. The optional
175
+ // second argument re-anchors the test; JS ignores a third+
176
+ // argument. See #1448 Tier B.
177
+ const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
178
+ const recv = emit(object)
179
+ const arg = emit(args[0])
180
+ if (args.length >= 2) {
181
+ return `bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`
182
+ }
183
+ return `bf->${fn}(${recv}, ${arg})`
184
+ }
185
+ case 'replace': {
186
+ // `.replace(old, new)` — string-pattern form, first occurrence.
187
+ // The `bf->replace` helper splices via index/substr (not `s///`)
188
+ // so both the pattern and the replacement are literal — no Perl
189
+ // regex metacharacters and no `$1` / `$&` interpolation in the
190
+ // replacement, keeping it byte-equal with Go's `bf_replace`. The
191
+ // regex-pattern form is refused upstream at the parser. See
192
+ // #1448 Tier B.
193
+ const recv = emit(object)
194
+ const oldS = emit(args[0])
195
+ const newS = emit(args[1])
196
+ return `bf->replace(${recv}, ${oldS}, ${newS})`
197
+ }
198
+ case 'repeat': {
199
+ // `.repeat(n)` — string repeated `n` times. The `bf->repeat`
200
+ // helper wraps Perl's `x` operator with the same negative-count
201
+ // → "" clamp and integer truncation Go's `bf_repeat` applies, so
202
+ // the two adapters stay byte-equal. Full JS arity: the no-argument
203
+ // form is `repeat(0)` → ""; a second+ argument is ignored.
204
+ // See #1448 Tier B.
205
+ const recv = emit(object)
206
+ const count = args.length === 0 ? '0' : emit(args[0])
207
+ return `bf->repeat(${recv}, ${count})`
208
+ }
209
+ case 'padStart':
210
+ case 'padEnd': {
211
+ // `.padStart(target, pad?)` / `.padEnd(target, pad?)`. The
212
+ // `bf->pad_*` helpers default the pad to a single space when the
213
+ // arg is omitted and measure length in characters, matching Go's
214
+ // rune-based `bf_pad_*`. Full JS arity: the no-argument form is
215
+ // `padStart(0)` → the receiver unchanged; a third+ argument is
216
+ // ignored. See #1448 Tier B.
217
+ const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
218
+ const recv = emit(object)
219
+ if (args.length === 0) {
220
+ return `bf->${fn}(${recv}, 0)`
221
+ }
222
+ const target = emit(args[0])
223
+ if (args.length === 1) {
224
+ return `bf->${fn}(${recv}, ${target})`
225
+ }
226
+ const pad = emit(args[1])
227
+ return `bf->${fn}(${recv}, ${target}, ${pad})`
228
+ }
229
+ default: {
230
+ // TS-level exhaustiveness guard. If this throws at runtime, the
231
+ // IR was constructed against a newer `ArrayMethod` variant that
232
+ // this adapter hasn't been updated for — loud failure is better
233
+ // than emitting a silent empty string downstream.
234
+ const _exhaustive: never = method
235
+ throw new Error(
236
+ `renderArrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`,
237
+ )
238
+ }
239
+ }
240
+ }
241
+
242
+ /** Escape a string for embedding in a Perl single-quoted literal. */
243
+ function escapePerlSingleQuote(s: string): string {
244
+ // Double every backslash and escape apostrophes: Perl single-quote
245
+ // un-escaping then restores the original bytes (a JSON `\\` must reach
246
+ // `JSON::PP->decode` as `\\`, so it travels as `\\\\` in the literal).
247
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
248
+ }
249
+
250
+ /**
251
+ * Build the `base_env` hashref argument for an evaluator call: the
252
+ * comparator / reducer body's free variables (body idents minus the
253
+ * callback params), each materialised to its SSR value via `emit`. An
254
+ * empty capture set yields `{}` — the runtime's seed-once env.
255
+ */
256
+ function emitEvalEnvArg(
257
+ body: ParsedExpr,
258
+ params: string[],
259
+ emit: (e: ParsedExpr) => string,
260
+ ): string {
261
+ const free = freeVarsInBody(body, new Set(params))
262
+ if (free.length === 0) return '{}'
263
+ const pairs = free.map(
264
+ n => `'${escapePerlSingleQuote(n)}' => ${emit({ kind: 'identifier', name: n })}`,
265
+ )
266
+ return `{ ${pairs.join(', ')} }`
267
+ }
268
+
269
+ /**
270
+ * Emit a `.sort(cmp)` / `.toSorted(cmp)` via the runtime evaluator (#2018):
271
+ * the comparator body travels as serialized-ParsedExpr JSON, evaluated per
272
+ * comparison against `{paramA, paramB, …captured}`. Returns null when the
273
+ * body can't be evaluated (e.g. a `localeCompare` comparator —
274
+ * `serializeParsedExpr` refuses it), so the caller falls back to the
275
+ * structured `bf->sort`. A `||`-chained multi-key comparator needs no
276
+ * special handling — JS `0 || next` is exactly the tie-break semantics.
277
+ */
278
+ export function renderSortEval(
279
+ recv: string,
280
+ body: ParsedExpr,
281
+ params: string[],
282
+ emit: (e: ParsedExpr) => string,
283
+ ): string | null {
284
+ if (params.length < 2) return null
285
+ const [paramA, paramB] = params
286
+ const json = serializeParsedExpr(body)
287
+ if (json === null) return null
288
+ const env = emitEvalEnvArg(body, [paramA, paramB], emit)
289
+ return `bf->sort_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramA}', '${paramB}', ${env})`
290
+ }
291
+
292
+ /**
293
+ * Emit a `.reduce(fn, init)` / `.reduceRight(fn, init)` via the runtime
294
+ * evaluator (#2018): the reducer body travels as serialized-ParsedExpr JSON,
295
+ * folded over the receiver from `init` in `direction` order. Returns null when
296
+ * the body can't be evaluated (→ caller falls back to `bf->reduce`). A numeric
297
+ * seed passes through as a bare Perl number; a concat seed as a single-quoted
298
+ * string.
299
+ */
300
+ export function renderReduceEval(
301
+ recv: string,
302
+ body: ParsedExpr,
303
+ params: string[],
304
+ init: ParsedExpr,
305
+ direction: 'left' | 'right',
306
+ emit: (e: ParsedExpr) => string,
307
+ ): string | null {
308
+ if (params.length < 2) return null
309
+ const [paramAcc, paramItem] = params
310
+ const json = serializeParsedExpr(body)
311
+ if (json === null) return null
312
+ // Only literal seeds round-trip to a Perl scalar here: a string literal as a
313
+ // single-quoted Perl string, a number literal as a bare numeric. Anything
314
+ // else (an expression seed, an omitted seed) can't be materialised → null,
315
+ // and the caller records BF101.
316
+ let initPerl: string
317
+ if (init.kind === 'literal' && init.literalType === 'string') {
318
+ initPerl = `'${escapePerlSingleQuote(String(init.value))}'`
319
+ } else if (init.kind === 'literal' && init.literalType === 'number') {
320
+ initPerl = String(init.value)
321
+ } else {
322
+ return null
323
+ }
324
+ const env = emitEvalEnvArg(body, [paramAcc, paramItem], emit)
325
+ return `bf->reduce_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramAcc}', '${paramItem}', ${initPerl}, '${direction}', ${env})`
326
+ }
327
+
328
+ /**
329
+ * Emit a higher-order predicate call via the runtime evaluator (#2018, P2):
330
+ * `bf->filter_eval` / `bf->every_eval` / `bf->some_eval` / `bf->find_eval` /
331
+ * `bf->find_index_eval`, carrying the serialized predicate body + captured env
332
+ * hashref. Generalizes the inline `grep` / `bf->find` lowering to the same
333
+ * JS-faithful evaluator the Go adapter uses (cross-adapter isomorphism).
334
+ * Returns null when the predicate is outside the evaluator surface (e.g. a
335
+ * method-call predicate — `serializeParsedExpr` refuses it), so the caller
336
+ * falls back to the `grep` / `bf->find` form. `forward` (find / findIndex
337
+ * family only) selects the search direction — `false` = findLast /
338
+ * findLastIndex.
339
+ */
340
+ export function renderPredicateEval(
341
+ funcName: string,
342
+ recv: string,
343
+ predicate: ParsedExpr,
344
+ param: string,
345
+ emit: (e: ParsedExpr) => string,
346
+ forward?: boolean,
347
+ ): string | null {
348
+ const json = serializeParsedExpr(predicate)
349
+ if (json === null) return null
350
+ const env = emitEvalEnvArg(predicate, [param], emit)
351
+ const fwd = forward === undefined ? '' : `, ${forward ? 1 : 0}`
352
+ return `bf->${funcName}(${recv}, '${escapePerlSingleQuote(json)}', '${param}'${fwd}, ${env})`
353
+ }
354
+
355
+ /**
356
+ * Emit a `.flatMap(proj)` via the runtime evaluator (#2018, P3/P5): the
357
+ * projection `body` serializes to JSON and `bf->flat_map_eval` projects +
358
+ * flattens one level. The sole flatMap lowering now (the structured
359
+ * `bf->flat_map` / `bf->flat_map_tuple` fallback was retired with the
360
+ * `ParsedExpr` collapse); returns null when the projection is outside the
361
+ * evaluator surface, and the caller records BF101.
362
+ */
363
+ export function renderFlatMapEval(
364
+ recv: string,
365
+ body: ParsedExpr,
366
+ param: string,
367
+ emit: (e: ParsedExpr) => string,
368
+ ): string | null {
369
+ const json = serializeParsedExpr(body)
370
+ if (json === null) return null
371
+ const env = emitEvalEnvArg(body, [param], emit)
372
+ return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
373
+ }
374
+
375
+ /**
376
+ * Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
377
+ * projection `body` serializes to JSON and `bf->map_eval` projects each
378
+ * element, one result per element (no flatten — the JS `.map` contract).
379
+ * Composes through the array-method chain (`.map(cb).join(' ')`). Returns
380
+ * null when the projection is outside the evaluator surface, and the caller
381
+ * records BF101. The JSX-returning `.map` is an IRLoop upstream and never
382
+ * reaches this emit.
383
+ */
384
+ export function renderMapEval(
385
+ recv: string,
386
+ body: ParsedExpr,
387
+ param: string,
388
+ emit: (e: ParsedExpr) => string,
389
+ ): string | null {
390
+ const json = serializeParsedExpr(body)
391
+ if (json === null) return null
392
+ const env = emitEvalEnvArg(body, [param], emit)
393
+ return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
394
+ }
395
+
396
+ /**
397
+ * Shared Mojo emit for `.sort(cmp)` / `.toSorted(cmp)` (#1448 Tier B).
398
+ * Used by both the filter-context emitter and the top-level emitter,
399
+ * plus the loop-hoist path in `renderLoop` — same emit shape across
400
+ * all three so a regression in any one path surfaces consistently.
401
+ *
402
+ * The Perl helper accepts a hash-ref opts bag whose `keys` entry is
403
+ * an ordered list of per-key hashes (room for a future `nulls` knob
404
+ * without arity churn), and returns a fresh ARRAY ref so downstream
405
+ * composition (`@{bf->sort(...)}` in `join(...)`, etc.) stays
406
+ * straightforward.
407
+ */
408
+ export function renderSortMethod(recv: string, c: SortComparator): string {
409
+ // One hash per comparison key, in priority order, under `keys`. A
410
+ // simple comparator yields a one-element list; a `||`-chained
411
+ // multi-key comparator yields one per operand. `bf->sort` walks them
412
+ // in order, falling through to the next on a tie.
413
+ const keyHashes = c.keys.map((k) => {
414
+ const keyEntry =
415
+ k.key.kind === 'self'
416
+ ? `key_kind => 'self'`
417
+ : `key_kind => 'field', key => '${k.key.field}'`
418
+ return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`
419
+ })
420
+ return `bf->sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
421
+ }
422
+
423
+ // `.flat(depth?)` → `bf->flat($recv, $depth)`. The `Infinity` form lowers
424
+ // to the `-1` sentinel (flatten fully); a finite depth flattens that many
425
+ // levels (`0` = shallow copy). See `sub flat` in BarefootJS.pm. (#1448)
426
+ export function renderFlatMethod(recv: string, depth: FlatDepth): string {
427
+ const d = depth === 'infinity' ? -1 : depth
428
+ return `bf->flat(${recv}, ${d})`
429
+ }