@barefootjs/mojolicious 0.15.2 → 0.17.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/adapter/analysis/component-tree.d.ts +30 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +96 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +77 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +34 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.js +1490 -1334
- package/dist/adapter/lib/constants.d.ts +27 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +40 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/perl-naming.d.ts +29 -0
- package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +28 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +43 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/mojo-adapter.d.ts +46 -104
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +48 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +64 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +26 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.js +1490 -1334
- package/dist/index.js +1490 -1334
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +163 -62
- package/src/__tests__/query-href.test.ts +94 -0
- package/src/adapter/analysis/component-tree.ts +128 -0
- package/src/adapter/emit-context.ts +107 -0
- package/src/adapter/expr/array-method.ts +408 -0
- package/src/adapter/expr/emitters.ts +607 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/lib/constants.ts +39 -0
- package/src/adapter/lib/ir-scope.ts +70 -0
- package/src/adapter/lib/perl-naming.ts +36 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +126 -0
- package/src/adapter/mojo-adapter.ts +230 -1476
- package/src/adapter/props/prop-classes.ts +87 -0
- package/src/adapter/spread/spread-codegen.ts +181 -0
- package/src/adapter/value/parsed-literal.ts +34 -0
- package/src/test-render.ts +2 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract the extracted expression-emitter modules depend on instead of
|
|
3
|
+
* the concrete `MojoAdapter`.
|
|
4
|
+
*
|
|
5
|
+
* The Mojo adapter's top-level expression lowering is mutually recursive with
|
|
6
|
+
* the adapter's own const/record resolution and its filter-predicate emitter,
|
|
7
|
+
* so the extracted `MojoTopLevelEmitter` still needs to call back into shared
|
|
8
|
+
* per-compile state and recursive entry points. `MojoEmitContext` is that
|
|
9
|
+
* seam: the emitter takes a `MojoEmitContext` built by the adapter's private
|
|
10
|
+
* `emitCtx` getter (the adapter does NOT `implements` this interface, so the
|
|
11
|
+
* wrapped members stay private and off its exported public type — matching the
|
|
12
|
+
* Go adapter's `emitCtx`). The emitter depends on this narrow interface rather
|
|
13
|
+
* than the full ~3k-line class, so the coupling is explicit and it's
|
|
14
|
+
* unit-testable against a stub.
|
|
15
|
+
*
|
|
16
|
+
* Keep this surface minimal: add a member only when an extracted module
|
|
17
|
+
* genuinely needs it, so the seam documents the real cross-module coupling
|
|
18
|
+
* rather than re-exposing the whole adapter.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
|
|
22
|
+
|
|
23
|
+
export interface MojoEmitContext {
|
|
24
|
+
/**
|
|
25
|
+
* (#1922) Local binding names the request-scoped `searchParams()` env signal
|
|
26
|
+
* is imported under. Non-empty enables the env-signal method-call lowering.
|
|
27
|
+
*/
|
|
28
|
+
readonly _searchParamsLocals: Set<string>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inline a module-scope pure string-literal const by name as the resolved
|
|
32
|
+
* literal value, or null when the name is not such a const.
|
|
33
|
+
*/
|
|
34
|
+
resolveModuleStringConst(name: string): string | null
|
|
35
|
+
|
|
36
|
+
/** Resolve a literal const (`const totalPages = 5`) to its Perl value, or null. */
|
|
37
|
+
resolveLiteralConst(name: string): string | null
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a static property access on a module object-literal const
|
|
41
|
+
* (`variantClasses.ghost`) to its Perl value at compile time, or null.
|
|
42
|
+
*/
|
|
43
|
+
resolveStaticRecordLiteral(objectName: string, key: string): string | null
|
|
44
|
+
|
|
45
|
+
/** Whether a getter/prop name resolves to a string-typed SSR value. */
|
|
46
|
+
_isStringValueName(name: string): boolean
|
|
47
|
+
|
|
48
|
+
/** Record a BF101 unsupported-expression diagnostic. */
|
|
49
|
+
_recordExprBF101(message: string, reason?: string): void
|
|
50
|
+
|
|
51
|
+
/** Lower a filter/predicate body to its Perl form, bound to `param`. */
|
|
52
|
+
_renderPerlFilterExprPublic(expr: ParsedExpr, param: string): string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The contract the extracted object-literal / conditional-spread lowering
|
|
57
|
+
* (`spread/spread-codegen.ts`) depends on. The spread lowering recurses into
|
|
58
|
+
* the core expression lowering and records its own BF101 diagnostics, so it
|
|
59
|
+
* needs the recursive entry point plus the per-compile bookkeeping the
|
|
60
|
+
* adapter owns. Declared separately from `MojoEmitContext` so each extracted
|
|
61
|
+
* module's real coupling is documented precisely.
|
|
62
|
+
*/
|
|
63
|
+
export interface MojoSpreadContext {
|
|
64
|
+
/** Component name, for diagnostic source locations. */
|
|
65
|
+
readonly componentName: string
|
|
66
|
+
|
|
67
|
+
/** Per-compile diagnostic list the spread lowering appends to. */
|
|
68
|
+
readonly errors: CompilerError[]
|
|
69
|
+
|
|
70
|
+
/** Local-constant metadata, for resolving `Record[key]` spread values. */
|
|
71
|
+
readonly localConstants: IRMetadata['localConstants']
|
|
72
|
+
|
|
73
|
+
/** Prop params, for classifying a bare-identifier index as a prop. */
|
|
74
|
+
readonly propsParams: { name: string }[]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Lower a JS expression to its Perl form (the core recursive entry).
|
|
78
|
+
*
|
|
79
|
+
* When the IR already carries a structured `ParsedExpr` tree, pass it as
|
|
80
|
+
* `preParsed` so the converter threads it straight through instead of
|
|
81
|
+
* re-parsing `expr` — mirrors go-template's
|
|
82
|
+
* `convertExpressionToGo(jsExpr, out?, preParsed?)`. With `preParsed` set,
|
|
83
|
+
* `expr` is unused for parsing (the converter derives any diagnostic text
|
|
84
|
+
* from the tree), so callers may pass `''`.
|
|
85
|
+
*/
|
|
86
|
+
convertExpressionToPerl(expr: string, preParsed?: ParsedExpr): string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The contract the extracted in-template memo / context seeding
|
|
91
|
+
* (`memo/seed.ts`) depends on. The seed lowering recurses into the core
|
|
92
|
+
* expression lowering to compute a derived signal/memo value or a context
|
|
93
|
+
* default; that recursive entry is its only adapter coupling.
|
|
94
|
+
*/
|
|
95
|
+
export interface MojoMemoContext {
|
|
96
|
+
/**
|
|
97
|
+
* Lower a JS expression to its Perl form (the core recursive entry).
|
|
98
|
+
*
|
|
99
|
+
* When the IR already carries a structured `ParsedExpr` tree, pass it as
|
|
100
|
+
* `preParsed` so the converter threads it straight through instead of
|
|
101
|
+
* re-parsing `expr` — mirrors go-template's
|
|
102
|
+
* `convertExpressionToGo(jsExpr, out?, preParsed?)`. With `preParsed` set,
|
|
103
|
+
* `expr` is unused for parsing (the converter derives any diagnostic text
|
|
104
|
+
* from the tree), so callers may pass `''`.
|
|
105
|
+
*/
|
|
106
|
+
convertExpressionToPerl(expr: string, preParsed?: ParsedExpr): string
|
|
107
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
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
|
+
* Shared Mojo emit for `.sort(cmp)` / `.toSorted(cmp)` (#1448 Tier B).
|
|
377
|
+
* Used by both the filter-context emitter and the top-level emitter,
|
|
378
|
+
* plus the loop-hoist path in `renderLoop` — same emit shape across
|
|
379
|
+
* all three so a regression in any one path surfaces consistently.
|
|
380
|
+
*
|
|
381
|
+
* The Perl helper accepts a hash-ref opts bag whose `keys` entry is
|
|
382
|
+
* an ordered list of per-key hashes (room for a future `nulls` knob
|
|
383
|
+
* without arity churn), and returns a fresh ARRAY ref so downstream
|
|
384
|
+
* composition (`@{bf->sort(...)}` in `join(...)`, etc.) stays
|
|
385
|
+
* straightforward.
|
|
386
|
+
*/
|
|
387
|
+
export function renderSortMethod(recv: string, c: SortComparator): string {
|
|
388
|
+
// One hash per comparison key, in priority order, under `keys`. A
|
|
389
|
+
// simple comparator yields a one-element list; a `||`-chained
|
|
390
|
+
// multi-key comparator yields one per operand. `bf->sort` walks them
|
|
391
|
+
// in order, falling through to the next on a tie.
|
|
392
|
+
const keyHashes = c.keys.map((k) => {
|
|
393
|
+
const keyEntry =
|
|
394
|
+
k.key.kind === 'self'
|
|
395
|
+
? `key_kind => 'self'`
|
|
396
|
+
: `key_kind => 'field', key => '${k.key.field}'`
|
|
397
|
+
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`
|
|
398
|
+
})
|
|
399
|
+
return `bf->sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// `.flat(depth?)` → `bf->flat($recv, $depth)`. The `Infinity` form lowers
|
|
403
|
+
// to the `-1` sentinel (flatten fully); a finite depth flattens that many
|
|
404
|
+
// levels (`0` = shallow copy). See `sub flat` in BarefootJS.pm. (#1448)
|
|
405
|
+
export function renderFlatMethod(recv: string, depth: FlatDepth): string {
|
|
406
|
+
const d = depth === 'infinity' ? -1 : depth
|
|
407
|
+
return `bf->flat(${recv}, ${d})`
|
|
408
|
+
}
|