@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.
- 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 +85 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +78 -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 +1482 -1339
- 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 +33 -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 +34 -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 +1482 -1339
- package/dist/index.js +1482 -1339
- 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 +286 -69
- 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 +429 -0
- package/src/adapter/expr/emitters.ts +639 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/lib/constants.ts +39 -0
- package/src/adapter/lib/ir-scope.ts +57 -0
- package/src/adapter/lib/perl-naming.ts +36 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +73 -0
- package/src/adapter/mojo-adapter.ts +248 -1485
- 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
|
@@ -32,26 +32,21 @@ import {
|
|
|
32
32
|
type AdapterOutput,
|
|
33
33
|
type AdapterGenerateOptions,
|
|
34
34
|
type TemplateSections,
|
|
35
|
-
type ParsedExprEmitter,
|
|
36
|
-
type HigherOrderMethod,
|
|
37
|
-
type ArrayMethod,
|
|
38
|
-
type LiteralType,
|
|
39
35
|
type IRNodeEmitter,
|
|
40
36
|
type EmitIRNode,
|
|
41
37
|
type AttrValueEmitter,
|
|
42
38
|
isBooleanAttr,
|
|
43
39
|
parseExpression,
|
|
40
|
+
stringifyParsedExpr,
|
|
44
41
|
parseStyleObjectEntries,
|
|
45
42
|
isSupported,
|
|
46
43
|
exprToString,
|
|
47
44
|
parseProviderObjectLiteral,
|
|
48
|
-
identifierPath,
|
|
49
45
|
emitParsedExpr,
|
|
50
46
|
emitIRNode,
|
|
51
47
|
emitAttrValue,
|
|
52
48
|
augmentInheritedPropAccesses,
|
|
53
49
|
parseRecordIndexAccess,
|
|
54
|
-
evalStringArrayJoin,
|
|
55
50
|
extractArrowBodyExpression,
|
|
56
51
|
collectContextConsumers,
|
|
57
52
|
isLowerableObjectRestDestructure,
|
|
@@ -59,158 +54,45 @@ import {
|
|
|
59
54
|
collectModuleStringConsts,
|
|
60
55
|
lookupStaticRecordLiteral,
|
|
61
56
|
searchParamsLocalNames,
|
|
62
|
-
|
|
57
|
+
prepareLoweringMatchers,
|
|
58
|
+
queryHrefArgs,
|
|
59
|
+
sortComparatorFromArrow,
|
|
63
60
|
} from '@barefootjs/jsx'
|
|
64
61
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Mojo adapter's IRNode render context. Mojo's lowering currently
|
|
68
|
-
* doesn't consume any render-position flags (`isRootOfClientComponent`
|
|
69
|
-
* is handled differently here than in Hono/Go), so the Ctx is empty.
|
|
70
|
-
* Kept as a named alias so future flags can extend it without changing
|
|
71
|
-
* the `IRNodeEmitter` interface.
|
|
72
|
-
*/
|
|
73
|
-
type MojoRenderCtx = Record<string, never>
|
|
74
|
-
import type { ParsedExpr, ParsedStatement, SortComparator, ReduceOp, FlatDepth, FlatMapOp, TemplatePart } from '@barefootjs/jsx'
|
|
62
|
+
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
75
63
|
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* record. Per-instance derivation would re-build the same Map on
|
|
106
|
-
* every `new MojoAdapter()` call.
|
|
107
|
-
*/
|
|
108
|
-
const MOJO_PRIMITIVE_EMIT_MAP: Record<string, (args: string[]) => string> =
|
|
109
|
-
Object.fromEntries(
|
|
110
|
-
Object.entries(MOJO_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit])
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Find the `children` prop's `jsx-children` payload (#1326). Narrowed
|
|
115
|
-
* via the AttrValue `kind` discriminator so adapter code stays type-
|
|
116
|
-
* safe if the IR shape evolves — adding a new AttrValue variant or
|
|
117
|
-
* renaming `children` to `jsxChildren` becomes a TS compile error
|
|
118
|
-
* here instead of silently dropping the children at runtime.
|
|
119
|
-
*/
|
|
120
|
-
function resolveJsxChildrenProp(props: readonly IRProp[]): IRNode[] {
|
|
121
|
-
const prop = props.find(p => p.name === 'children')
|
|
122
|
-
if (!prop) return []
|
|
123
|
-
if (prop.value.kind !== 'jsx-children') return []
|
|
124
|
-
return prop.value.children
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export interface MojoAdapterOptions {
|
|
128
|
-
/** Base path for client JS files (default: '/static/components/') */
|
|
129
|
-
clientJsBasePath?: string
|
|
130
|
-
|
|
131
|
-
/** Path to barefoot.js runtime (default: '/static/components/barefoot.js') */
|
|
132
|
-
barefootJsPath?: string
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Parse a const initializer's source text. Returns the unescaped string
|
|
137
|
-
* value when the whole initializer is a single string literal (or a
|
|
138
|
-
* no-substitution template literal), else `null`. Uses the TS parser so
|
|
139
|
-
* escapes/quotes resolve exactly as JS would, matching the value the Hono
|
|
140
|
-
* reference inlines at runtime.
|
|
141
|
-
*/
|
|
142
|
-
function parsePureStringLiteral(source: string): string | null {
|
|
143
|
-
const sf = ts.createSourceFile(
|
|
144
|
-
'__const.ts',
|
|
145
|
-
`const __x = (${source});`,
|
|
146
|
-
ts.ScriptTarget.Latest,
|
|
147
|
-
/*setParentNodes*/ false,
|
|
148
|
-
)
|
|
149
|
-
const stmt = sf.statements[0]
|
|
150
|
-
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
151
|
-
const decl = stmt.declarationList.declarations[0]
|
|
152
|
-
let init = decl?.initializer
|
|
153
|
-
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
154
|
-
if (!init) return null
|
|
155
|
-
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
156
|
-
return init.text
|
|
157
|
-
}
|
|
158
|
-
// `[<literals>].join(' ')` module consts (e.g. Switch's `trackStateClasses`)
|
|
159
|
-
// → inline the flattened string byte-for-byte. See `evalStringArrayJoin`.
|
|
160
|
-
return evalStringArrayJoin(source)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* (#checkbox) Quote a `render_child` named-arg / hashref key when it isn't a
|
|
165
|
-
* bare Perl identifier. A JSX attribute name like `data-slot` would otherwise
|
|
166
|
-
* emit `data-slot => '...'`, which Perl parses as the subtraction
|
|
167
|
-
* `data - slot`. Identifier-safe names (`className`, `size`, `_bf_slot`) pass
|
|
168
|
-
* through unquoted to keep the generated template readable.
|
|
169
|
-
*/
|
|
170
|
-
function perlHashKey(name: string): string {
|
|
171
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Collect the component's root scope element node(s) — the elements that
|
|
176
|
-
* become the rendered root and so carry `data-key` for a keyed loop item. A
|
|
177
|
-
* plain element root is itself; an `if-statement` (early-return) root
|
|
178
|
-
* contributes the top element of each branch (`consequent` + the `alternate`
|
|
179
|
-
* chain), since exactly one branch renders at runtime. Non-element branch
|
|
180
|
-
* tops (fragments / nested shapes) are walked one level so an
|
|
181
|
-
* `if (…) return <A/>` still resolves to `<A>`. (#1297)
|
|
182
|
-
*/
|
|
183
|
-
function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
184
|
-
const out = new Set<IRNode>()
|
|
185
|
-
const visit = (n: IRNode | null): void => {
|
|
186
|
-
if (!n) return
|
|
187
|
-
if (n.type === 'element') { out.add(n); return }
|
|
188
|
-
if (n.type === 'if-statement') {
|
|
189
|
-
const s = n as IRIfStatement
|
|
190
|
-
visit(s.consequent)
|
|
191
|
-
visit(s.alternate)
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
if (n.type === 'fragment') {
|
|
195
|
-
for (const c of (n as IRFragment).children) visit(c)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
visit(node)
|
|
199
|
-
return out
|
|
200
|
-
}
|
|
65
|
+
import type { MojoRenderCtx } from './lib/types.ts'
|
|
66
|
+
import { MOJO_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
67
|
+
import { perlHashKey, perlIdentifierFromMarkerId } from './lib/perl-naming.ts'
|
|
68
|
+
import {
|
|
69
|
+
resolveJsxChildrenProp,
|
|
70
|
+
collectRootScopeNodes,
|
|
71
|
+
} from './lib/ir-scope.ts'
|
|
72
|
+
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
73
|
+
import { MojoFilterEmitter, MojoTopLevelEmitter } from './expr/emitters.ts'
|
|
74
|
+
import type { MojoEmitContext, MojoSpreadContext, MojoMemoContext } from './emit-context.ts'
|
|
75
|
+
import {
|
|
76
|
+
hasClientInteractivity,
|
|
77
|
+
collectImportedLoopChildComponentErrors,
|
|
78
|
+
} from './analysis/component-tree.ts'
|
|
79
|
+
import {
|
|
80
|
+
conditionalSpreadToPerl,
|
|
81
|
+
objectLiteralExprToPerlHashref,
|
|
82
|
+
} from './spread/spread-codegen.ts'
|
|
83
|
+
import {
|
|
84
|
+
generateContextConsumerSeed,
|
|
85
|
+
generateDerivedMemoSeed,
|
|
86
|
+
} from './memo/seed.ts'
|
|
87
|
+
import {
|
|
88
|
+
collectProviderDataNames,
|
|
89
|
+
collectBooleanTypedProps,
|
|
90
|
+
collectNullableOptionalProps,
|
|
91
|
+
collectStringValueNames,
|
|
92
|
+
} from './props/prop-classes.ts'
|
|
201
93
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
* in the available set — i.e. the template already has that var in scope.
|
|
205
|
-
* Guards in-template memo seeding from referencing an out-of-scope binding
|
|
206
|
-
* (which would trip Perl strict mode). (#1297)
|
|
207
|
-
*/
|
|
208
|
-
function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
|
|
209
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
210
|
-
if (!available.has(m[1])) return false
|
|
211
|
-
}
|
|
212
|
-
return true
|
|
213
|
-
}
|
|
94
|
+
export type { MojoAdapterOptions } from './lib/types.ts'
|
|
95
|
+
import type { MojoAdapterOptions } from './lib/types.ts'
|
|
214
96
|
|
|
215
97
|
export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRenderCtx> {
|
|
216
98
|
name = 'mojolicious'
|
|
@@ -254,6 +136,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
254
136
|
private propsObjectName: string | null = null
|
|
255
137
|
private propsParams: { name: string }[] = []
|
|
256
138
|
private booleanTypedProps: Set<string> = new Set()
|
|
139
|
+
/**
|
|
140
|
+
* (#1971) Names that resolve to a real SSR template var — prop param, signal
|
|
141
|
+
* getter, or memo. A `<Ctx.Provider value>` member referencing a name NOT in
|
|
142
|
+
* this set is a client-only function (a local handler const like `scrollPrev`
|
|
143
|
+
* or a signal setter like `setCanScrollPrev`) with no SSR value: it would
|
|
144
|
+
* emit an undeclared `$var`, so it's lowered to `undef` instead.
|
|
145
|
+
*/
|
|
146
|
+
private providerDataNames: Set<string> = new Set()
|
|
257
147
|
/**
|
|
258
148
|
* Names (signal getters + props) whose value is a string, so `===`/`!==`
|
|
259
149
|
* against them lowers to Perl `eq`/`ne` rather than numeric `==`/`!=`.
|
|
@@ -269,7 +159,15 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
269
159
|
* the generic hash deref. Set at `generate()` entry from `ir.metadata.imports`;
|
|
270
160
|
* read by the top-level ParsedExpr emitter.
|
|
271
161
|
*/
|
|
272
|
-
_searchParamsLocals: Set<string> = new Set()
|
|
162
|
+
private _searchParamsLocals: Set<string> = new Set()
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Call-lowering matchers active for this component (#2057). Bound at
|
|
166
|
+
* `generate()` entry via `prepareLoweringMatchers` and read by the top-level
|
|
167
|
+
* emitter. Covers both userland plugins and the compiler's built-in plugins
|
|
168
|
+
* (e.g. `queryHref` → `bf->query`, #2042) — one uniform path, no per-API branch.
|
|
169
|
+
*/
|
|
170
|
+
private _loweringMatchers: LoweringMatcher[] = []
|
|
273
171
|
/**
|
|
274
172
|
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
275
173
|
* file top-level), keyed by name → resolved literal value. Populated at
|
|
@@ -336,56 +234,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
336
234
|
// serialization happens before `generate`, so this mutation doesn't reach it).
|
|
337
235
|
augmentInheritedPropAccesses(ir)
|
|
338
236
|
this.propsParams = ir.metadata.propsParams.map(p => ({ name: p.name }))
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.
|
|
344
|
-
ir.metadata.propsParams
|
|
345
|
-
.filter(prop => prop.type?.primitive === 'boolean' || prop.type?.raw === 'boolean')
|
|
346
|
-
.map(prop => prop.name),
|
|
347
|
-
)
|
|
348
|
-
// No-destructure-default props → `undef` when the caller omits them
|
|
349
|
-
// → guard their bare-reference attribute emission with Perl `defined`
|
|
350
|
-
// so the attribute drops instead of rendering `attr=""` (Hono-style
|
|
351
|
-
// nullish omission). A prop WITH a destructure default (`value = ''`)
|
|
352
|
-
// is never `undef` in the body and must stay unconditional, so it is
|
|
353
|
-
// excluded. This mirrors the Go adapter's nillable-field guard: there
|
|
354
|
-
// the witness is the resolved `interface{}` field type; here it is
|
|
355
|
-
// the absence of a default (the analyzer reports `rows` — a
|
|
356
|
-
// `TextareaHTMLAttributes` member destructured without a default — as
|
|
357
|
-
// no-default, `type.kind: 'unknown'`).
|
|
358
|
-
// Excludes concrete-primitive types (`string`/`number`/`boolean`)
|
|
359
|
-
// to match the Go adapter's scope, which guards only `interface{}`
|
|
360
|
-
// (nillable) fields and leaves concrete fields unconditional. So a
|
|
361
|
-
// required, no-default `string` prop still emits `attr=""` like Hono,
|
|
362
|
-
// and only nillable (`unknown`/object/array) no-default props guard.
|
|
363
|
-
this.nullableOptionalProps = new Set(
|
|
364
|
-
ir.metadata.propsParams
|
|
365
|
-
.filter(
|
|
366
|
-
p =>
|
|
367
|
-
p.defaultValue === undefined &&
|
|
368
|
-
!p.isRest &&
|
|
369
|
-
p.type?.kind !== 'primitive',
|
|
370
|
-
)
|
|
371
|
-
.map(p => p.name),
|
|
372
|
-
)
|
|
373
|
-
// Record string-typed signals and props so equality comparisons against
|
|
374
|
-
// them lower to `eq`/`ne` (#1672). A signal is string-typed when its
|
|
375
|
-
// inferred type is `string` (the analyzer infers this from a string-literal
|
|
376
|
-
// initial value) or, defensively, when its initial value is a bare string
|
|
377
|
-
// literal; a prop when its annotated type is `string`.
|
|
378
|
-
this.stringValueNames = new Set<string>()
|
|
379
|
-
for (const s of ir.metadata.signals) {
|
|
380
|
-
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
381
|
-
this.stringValueNames.add(s.getter)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
for (const p of ir.metadata.propsParams) {
|
|
385
|
-
if (isStringTypeInfo(p.type)) this.stringValueNames.add(p.name)
|
|
386
|
-
}
|
|
237
|
+
// Per-compile prop classifications (see `props/prop-classes.ts`).
|
|
238
|
+
this.providerDataNames = collectProviderDataNames(ir)
|
|
239
|
+
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
240
|
+
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
241
|
+
this.stringValueNames = collectStringValueNames(ir)
|
|
387
242
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
388
243
|
this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
|
|
244
|
+
this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
389
245
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
390
246
|
this.loopBoundNames.clear()
|
|
391
247
|
this.errors = []
|
|
@@ -403,7 +259,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
403
259
|
// sibling templates are registered on the same template instance
|
|
404
260
|
// at render time.
|
|
405
261
|
if (!options?.siblingTemplatesRegistered) {
|
|
406
|
-
this.
|
|
262
|
+
this.errors.push(...collectImportedLoopChildComponentErrors(ir, this.componentName))
|
|
407
263
|
}
|
|
408
264
|
|
|
409
265
|
this.rootScopeNodes = collectRootScopeNodes(ir.root)
|
|
@@ -420,14 +276,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
420
276
|
// from the active provider value (or the `createContext` default) so the
|
|
421
277
|
// body's `$x` resolves. The provider side pushes the value via
|
|
422
278
|
// `emitProvider`; here the consumer reads it. (#1297)
|
|
423
|
-
const ctxSeed =
|
|
279
|
+
const ctxSeed = generateContextConsumerSeed(ir)
|
|
424
280
|
|
|
425
281
|
// Prop/signal-derived memos that aren't statically evaluable (e.g.
|
|
426
282
|
// `createMemo(() => props.value * 10)`) have a `null` SSR default, so
|
|
427
283
|
// their `$x` would render empty. Compute them in-template from the
|
|
428
284
|
// already-seeded prop/signal vars — mirroring Go's generated child
|
|
429
285
|
// constructor that evaluates the memo from the passed prop. (#1297)
|
|
430
|
-
const memoSeed = this.
|
|
286
|
+
const memoSeed = generateDerivedMemoSeed(this.memoCtx, ir)
|
|
431
287
|
|
|
432
288
|
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}\n`
|
|
433
289
|
|
|
@@ -513,7 +369,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
513
369
|
* function-scope consts never reach the per-render stash, so a bare
|
|
514
370
|
* `$totalPages` faults under strict mode.
|
|
515
371
|
*/
|
|
516
|
-
resolveLiteralConst(name: string): string | null {
|
|
372
|
+
private resolveLiteralConst(name: string): string | null {
|
|
517
373
|
if (this.loopBoundNames?.has?.(name)) return null
|
|
518
374
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
519
375
|
if (c?.value === undefined) return null
|
|
@@ -524,7 +380,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
524
380
|
return null
|
|
525
381
|
}
|
|
526
382
|
|
|
527
|
-
resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
383
|
+
private resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
528
384
|
if (this.loopBoundNames?.has?.(objectName)) return null
|
|
529
385
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
530
386
|
if (!hit) return null
|
|
@@ -533,7 +389,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
533
389
|
: `'${hit.text.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
534
390
|
}
|
|
535
391
|
|
|
536
|
-
resolveModuleStringConst(name: string): string | null {
|
|
392
|
+
private resolveModuleStringConst(name: string): string | null {
|
|
537
393
|
// A loop body introduces `my $<param>` / `my $<index>` bindings that
|
|
538
394
|
// shadow a module const of the same name — never inline inside one.
|
|
539
395
|
if (this.loopBoundNames.has(name)) return null
|
|
@@ -547,7 +403,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
547
403
|
// ===========================================================================
|
|
548
404
|
|
|
549
405
|
private generateScriptRegistrations(ir: ComponentIR, scriptBaseName?: string): string {
|
|
550
|
-
const hasInteractivity =
|
|
406
|
+
const hasInteractivity = hasClientInteractivity(ir)
|
|
551
407
|
if (!hasInteractivity) return ''
|
|
552
408
|
|
|
553
409
|
const name = scriptBaseName ?? ir.metadata.componentName
|
|
@@ -561,15 +417,6 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
561
417
|
return lines.join('\n')
|
|
562
418
|
}
|
|
563
419
|
|
|
564
|
-
private hasClientInteractivity(ir: ComponentIR): boolean {
|
|
565
|
-
return (
|
|
566
|
-
ir.metadata.signals.length > 0 ||
|
|
567
|
-
ir.metadata.effects.length > 0 ||
|
|
568
|
-
ir.metadata.onMounts.length > 0 ||
|
|
569
|
-
(ir.metadata.clientAnalysis?.needsInit ?? false)
|
|
570
|
-
)
|
|
571
|
-
}
|
|
572
|
-
|
|
573
420
|
// ===========================================================================
|
|
574
421
|
// Node Rendering
|
|
575
422
|
// ===========================================================================
|
|
@@ -685,109 +532,29 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
685
532
|
const key = `'${m.name.replace(/[\\']/g, c => `\\${c}`)}'`
|
|
686
533
|
if (m.kind === 'function' || /^on[A-Z]/.test(m.name)) return `${key} => undef`
|
|
687
534
|
const src = m.kind === 'getter' ? m.body : m.expr
|
|
535
|
+
// (#1971) A member whose value is a bare identifier that doesn't resolve
|
|
536
|
+
// to a prop/signal/memo is a client-only function reference (a local
|
|
537
|
+
// handler const like `scrollPrev`, or a signal setter like
|
|
538
|
+
// `setCanScrollPrev`) — no SSR value, and emitting `$scrollPrev` would
|
|
539
|
+
// trip Perl strict mode on an undeclared var. Lower to undef.
|
|
540
|
+
if (this.isClientOnlyContextIdentifier(src)) return `${key} => undef`
|
|
688
541
|
return `${key} => ${this.convertExpressionToPerl(src)}`
|
|
689
542
|
})
|
|
690
543
|
return `{ ${entries.join(', ')} }`
|
|
691
544
|
}
|
|
692
545
|
|
|
693
|
-
/** Perl literal for a context-consumer's `createContext` default. */
|
|
694
|
-
private contextDefaultPerl(c: ContextConsumer): string {
|
|
695
|
-
const d = c.defaultValue
|
|
696
|
-
if (d === null || d === undefined) return 'undef'
|
|
697
|
-
if (typeof d === 'string') return `'${d.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
698
|
-
if (typeof d === 'boolean') return d ? '1' : '0'
|
|
699
|
-
return String(d)
|
|
700
|
-
}
|
|
701
|
-
|
|
702
546
|
/**
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
547
|
+
* (#1971) True when `src` is a bare identifier that doesn't resolve to a
|
|
548
|
+
* prop/signal/memo or an SSR-inlinable module string const — i.e. a
|
|
549
|
+
* client-only function reference in a context value (a local handler const
|
|
550
|
+
* like `scrollPrev`, or a signal setter like `setCanScrollPrev`). See
|
|
551
|
+
* `providerDataNames`. Module-scope string consts (`carouselClasses`) ARE
|
|
552
|
+
* SSR-resolvable via `moduleStringConsts`, so they're excluded here.
|
|
706
553
|
*/
|
|
707
|
-
private
|
|
708
|
-
const
|
|
709
|
-
if (
|
|
710
|
-
return (
|
|
711
|
-
consumers
|
|
712
|
-
.map(
|
|
713
|
-
c =>
|
|
714
|
-
`% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`,
|
|
715
|
-
)
|
|
716
|
-
.join('\n') + '\n'
|
|
717
|
-
)
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Seed memos whose SSR default is `null` (not statically evaluable) by
|
|
722
|
-
* computing them in-template from the already-seeded prop / signal vars.
|
|
723
|
-
* Targets the prop-derived memo shape (`createMemo(() => props.value * 10)`)
|
|
724
|
-
* that the static `extractSsrDefaults` evaluator can't fold — without this
|
|
725
|
-
* the memo's `$x` renders empty (the reason `props-reactivity-comparison`
|
|
726
|
-
* was skipped). Only emitted when the lowered expression references vars the
|
|
727
|
-
* template already has in scope (props params + signals + prior memos), so a
|
|
728
|
-
* memo over an out-of-scope binding stays on the null path rather than
|
|
729
|
-
* tripping Perl strict mode. (#1297)
|
|
730
|
-
*/
|
|
731
|
-
private generateDerivedMemoSeed(ir: ComponentIR): string {
|
|
732
|
-
const memos = ir.metadata.memos ?? []
|
|
733
|
-
const signals = ir.metadata.signals ?? []
|
|
734
|
-
if (memos.length === 0 && signals.length === 0) return ''
|
|
735
|
-
// Props seed first; each signal/memo adds its own name as it lands so a
|
|
736
|
-
// later one can reference an earlier one.
|
|
737
|
-
const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
|
|
738
|
-
const lines: string[] = []
|
|
739
|
-
|
|
740
|
-
// Prop/signal-derived signals (`createSignal(props.defaultOn ?? false)`):
|
|
741
|
-
// a loop-child render receives no stash seed for the signal, so its `$on`
|
|
742
|
-
// would trip strict mode; and even when an entry render seeds it, the
|
|
743
|
-
// static default can't capture the per-call prop. Seed it in-template from
|
|
744
|
-
// the passed prop — but ONLY when the init lowers cleanly AND references an
|
|
745
|
-
// in-scope var (i.e. it's genuinely derived). Object/array/constant inits
|
|
746
|
-
// (`createSignal({…})`, `createSignal([…])`, `createSignal('b')`) keep the
|
|
747
|
-
// existing ssr-defaults seeding, so the spread / loop fixtures are
|
|
748
|
-
// untouched.
|
|
749
|
-
for (const signal of signals) {
|
|
750
|
-
const perl = this.tryLowerToPerl(signal.initialValue, available)
|
|
751
|
-
if (perl !== null) lines.push(`% my $${signal.getter} = ${perl};`)
|
|
752
|
-
available.add(signal.getter)
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
for (const memo of memos) {
|
|
756
|
-
// Seed every memo whose body lowers cleanly — not just the ones whose
|
|
757
|
-
// static SSR default is null. A statically-foldable prop-derived memo
|
|
758
|
-
// (`createMemo(() => props.disabled ?? false)` → default `false`)
|
|
759
|
-
// still depends on the per-call prop: the static stash seed bakes in
|
|
760
|
-
// the absent-prop fold, so a caller passing `disabled => 1` would
|
|
761
|
-
// render the default branch (#1897, select's disabled item). The
|
|
762
|
-
// in-template recomputation reads the prop lexical the stash already
|
|
763
|
-
// seeded, so it's correct per call; block-bodied arrows /
|
|
764
|
-
// out-of-scope references fall back to the static ssr-defaults seed.
|
|
765
|
-
const body = extractArrowBodyExpression(memo.computation)
|
|
766
|
-
if (body !== null) {
|
|
767
|
-
const perl = this.tryLowerToPerl(body, available)
|
|
768
|
-
if (perl !== null) lines.push(`% my $${memo.name} = ${perl};`)
|
|
769
|
-
}
|
|
770
|
-
available.add(memo.name)
|
|
771
|
-
}
|
|
772
|
-
return lines.length > 0 ? lines.join('\n') + '\n' : ''
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Lower a signal init / memo body to Perl for an in-template SSR seed, or
|
|
777
|
-
* `null` when it shouldn't be seeded this way. Returns null — without
|
|
778
|
-
* recording a BF101 — when the expression isn't a supported shape
|
|
779
|
-
* (`isSupported` pre-check, so object/array literals don't fail the build),
|
|
780
|
-
* when the lowering references no in-scope var (a constant — keep the
|
|
781
|
-
* existing ssr-defaults seeding), or when it references an out-of-scope
|
|
782
|
-
* binding. (#1297)
|
|
783
|
-
*/
|
|
784
|
-
private tryLowerToPerl(expr: string, available: ReadonlySet<string>): string | null {
|
|
785
|
-
const trimmed = expr.trim()
|
|
786
|
-
if (!trimmed) return null
|
|
787
|
-
if (!isSupported(parseExpression(trimmed)).supported) return null
|
|
788
|
-
const perl = this.convertExpressionToPerl(trimmed)
|
|
789
|
-
if (perl === '' || !/\$[A-Za-z_]\w*/.test(perl)) return null
|
|
790
|
-
return referencedVarsAreAvailable(perl, available) ? perl : null
|
|
554
|
+
private isClientOnlyContextIdentifier(src: string): boolean {
|
|
555
|
+
const t = src.trim()
|
|
556
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(t)) return false
|
|
557
|
+
return !this.providerDataNames.has(t) && !this.moduleStringConsts.has(t)
|
|
791
558
|
}
|
|
792
559
|
|
|
793
560
|
emitAsync(node: IRAsync, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string {
|
|
@@ -923,97 +690,20 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
923
690
|
return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`
|
|
924
691
|
}
|
|
925
692
|
|
|
926
|
-
// ===========================================================================
|
|
927
|
-
// Imported-component-in-loop check (BF103, #1266)
|
|
928
|
-
// ===========================================================================
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Push a `BF103` diagnostic for every component reference inside a
|
|
932
|
-
* loop body whose name is imported from a relative-path module.
|
|
933
|
-
* Mirror of the Go adapter's check — the Mojo adapter has the same
|
|
934
|
-
* cross-template-registration constraint at request time.
|
|
935
|
-
*/
|
|
936
|
-
private checkImportedLoopChildComponents(ir: ComponentIR): void {
|
|
937
|
-
// Collect every name imported from a relative-path module (no
|
|
938
|
-
// case filter — `IRComponent` nodes only exist for PascalCase JSX
|
|
939
|
-
// usages, so a lowercase utility import in the set can't match
|
|
940
|
-
// anyway, and any heuristic on the import name itself would be
|
|
941
|
-
// strictly less robust than the structural IR check below).
|
|
942
|
-
const relativeImports = new Set<string>()
|
|
943
|
-
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
944
|
-
if (!imp.source.startsWith('./') && !imp.source.startsWith('../')) continue
|
|
945
|
-
if (imp.isTypeOnly) continue
|
|
946
|
-
for (const spec of imp.specifiers) {
|
|
947
|
-
relativeImports.add(spec.alias ?? spec.name)
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
if (relativeImports.size === 0) return
|
|
951
|
-
|
|
952
|
-
const loc = { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
|
|
953
|
-
const visit = (node: IRNode, inLoop: boolean): void => {
|
|
954
|
-
switch (node.type) {
|
|
955
|
-
case 'component': {
|
|
956
|
-
const comp = node as IRComponent
|
|
957
|
-
if (inLoop && relativeImports.has(comp.name)) {
|
|
958
|
-
this.errors.push({
|
|
959
|
-
code: 'BF103',
|
|
960
|
-
severity: 'error',
|
|
961
|
-
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Mojo adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
962
|
-
loc: comp.loc ?? loc,
|
|
963
|
-
suggestion: {
|
|
964
|
-
message:
|
|
965
|
-
`Options:\n` +
|
|
966
|
-
` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Mojo template alongside the parent at render time.\n` +
|
|
967
|
-
` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.\n` +
|
|
968
|
-
` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`,
|
|
969
|
-
},
|
|
970
|
-
})
|
|
971
|
-
}
|
|
972
|
-
for (const child of comp.children) visit(child, inLoop)
|
|
973
|
-
break
|
|
974
|
-
}
|
|
975
|
-
case 'element':
|
|
976
|
-
for (const child of (node as IRElement).children) visit(child, inLoop)
|
|
977
|
-
break
|
|
978
|
-
case 'fragment':
|
|
979
|
-
for (const child of (node as IRFragment).children) visit(child, inLoop)
|
|
980
|
-
break
|
|
981
|
-
case 'conditional': {
|
|
982
|
-
const cond = node as IRConditional
|
|
983
|
-
visit(cond.whenTrue, inLoop)
|
|
984
|
-
if (cond.whenFalse) visit(cond.whenFalse, inLoop)
|
|
985
|
-
break
|
|
986
|
-
}
|
|
987
|
-
case 'loop':
|
|
988
|
-
for (const child of (node as IRLoop).children) visit(child, true)
|
|
989
|
-
break
|
|
990
|
-
case 'if-statement': {
|
|
991
|
-
const stmt = node as IRIfStatement
|
|
992
|
-
visit(stmt.consequent, inLoop)
|
|
993
|
-
if (stmt.alternate) visit(stmt.alternate, inLoop)
|
|
994
|
-
break
|
|
995
|
-
}
|
|
996
|
-
case 'provider':
|
|
997
|
-
for (const child of (node as IRProvider).children) visit(child, inLoop)
|
|
998
|
-
break
|
|
999
|
-
case 'async': {
|
|
1000
|
-
const a = node as IRAsync
|
|
1001
|
-
visit(a.fallback, inLoop)
|
|
1002
|
-
for (const child of a.children) visit(child, inLoop)
|
|
1003
|
-
break
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
visit(ir.root, false)
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
693
|
// ===========================================================================
|
|
1011
694
|
// Loop Rendering
|
|
1012
695
|
// ===========================================================================
|
|
1013
696
|
|
|
1014
697
|
renderLoop(loop: IRLoop): string {
|
|
1015
|
-
//
|
|
1016
|
-
|
|
698
|
+
// clientOnly loops must not render items at SSR time, but must still emit
|
|
699
|
+
// the `loop:`/`/loop:` boundary marker pair (Hono and Go parity) so the
|
|
700
|
+
// client runtime's mapArray() can locate the insertion anchor when
|
|
701
|
+
// hydrating the array. Without the markers, mapArray() resolves
|
|
702
|
+
// anchor = null and appends after sibling markers (#872). The marker id
|
|
703
|
+
// disambiguates sibling `.map()` calls under the same parent (#1087).
|
|
704
|
+
if (loop.clientOnly) {
|
|
705
|
+
return `<%== bf->comment("loop:${loop.markerId}") %><%== bf->comment("/loop:${loop.markerId}") %>`
|
|
706
|
+
}
|
|
1017
707
|
|
|
1018
708
|
// An array/object-destructure loop param (`([emoji, users]) => ...`
|
|
1019
709
|
// or `({ name, age }) => ...`) lowers to invalid Perl — the adapter
|
|
@@ -1051,11 +741,11 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1051
741
|
}
|
|
1052
742
|
|
|
1053
743
|
const rawArray = this.convertExpressionToPerl(loop.array)
|
|
1054
|
-
// Apply sort if present (#1448 Tier B): wrap the loop array in
|
|
1055
|
-
//
|
|
1056
|
-
// feeds both this loop-chain hoist and the
|
|
1057
|
-
// `
|
|
1058
|
-
//
|
|
744
|
+
// Apply sort if present (#1448 Tier B): wrap the loop array in the
|
|
745
|
+
// shared sort helper. The same `renderSortEval` / `renderSortMethod`
|
|
746
|
+
// pair feeds both this loop-chain hoist and the emitter's
|
|
747
|
+
// `callbackMethod` sort arm, so a regression in either path surfaces
|
|
748
|
+
// with the identical emit shape.
|
|
1059
749
|
//
|
|
1060
750
|
// Sort hoist: the loop bound (`0..$#{…}`) and the per-item
|
|
1061
751
|
// lookup (`…->[$_i]`) both reference the same array — if the
|
|
@@ -1107,7 +797,49 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1107
797
|
// each get their own reconciliation range (#1087).
|
|
1108
798
|
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`)
|
|
1109
799
|
if (sortedHoist && loop.sortComparator) {
|
|
1110
|
-
|
|
800
|
+
// Evaluator-first (#2018 P3): serialize the comparator + emit
|
|
801
|
+
// `bf->sort_eval`; fall back to the structured `bf->sort` for a
|
|
802
|
+
// comparator the evaluator can't model (e.g. `localeCompare`).
|
|
803
|
+
//
|
|
804
|
+
// The hoisted sort runs OUTSIDE this loop, so this loop's bound names
|
|
805
|
+
// must not shadow the comparator's captured free vars while emitting the
|
|
806
|
+
// env — otherwise a captured var that happens to share a loop-param name
|
|
807
|
+
// is blocked from inlining its module const and renders as an undefined
|
|
808
|
+
// `$name` (strict-mode fault, Copilot review #2035). Drop this loop's
|
|
809
|
+
// bound names for the sort emit, then restore (a nested loop's outer
|
|
810
|
+
// bindings, ref-counted, stay in effect).
|
|
811
|
+
for (const n of loopBound) {
|
|
812
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
813
|
+
if (c <= 0) this.loopBoundNames.delete(n)
|
|
814
|
+
else this.loopBoundNames.set(n, c)
|
|
815
|
+
}
|
|
816
|
+
const sortEmit = (e: ParsedExpr) => this.convertExpressionToPerl('', e)
|
|
817
|
+
// `loop.sortComparator` is the generic `IRLoopSort` (#2018 P5): serialize
|
|
818
|
+
// the arrow body for the evaluator (eval-first), recover the structured
|
|
819
|
+
// comparator from the arrow for the `localeCompare` fallback.
|
|
820
|
+
const sortArrow = loop.sortComparator.arrow
|
|
821
|
+
let sorted: string | null = null
|
|
822
|
+
if (sortArrow.kind === 'arrow') {
|
|
823
|
+
sorted = renderSortEval(rawArray, sortArrow.body, sortArrow.params, sortEmit)
|
|
824
|
+
}
|
|
825
|
+
if (sorted === null) {
|
|
826
|
+
const structured = sortComparatorFromArrow(sortArrow)
|
|
827
|
+
if (structured !== null) sorted = renderSortMethod(rawArray, structured)
|
|
828
|
+
}
|
|
829
|
+
if (sorted === null) {
|
|
830
|
+
// Neither the evaluator nor the structured fallback can model this
|
|
831
|
+
// comparator — record BF101 and fall through with the unsorted array
|
|
832
|
+
// so the hoist line stays syntactically valid.
|
|
833
|
+
this._recordExprBF101(
|
|
834
|
+
`.sort(...) loop comparator is not lowerable to a template sort`,
|
|
835
|
+
`Pre-sort the array in the route handler, or mark the loop @client-only.`,
|
|
836
|
+
)
|
|
837
|
+
sorted = rawArray
|
|
838
|
+
}
|
|
839
|
+
for (const n of loopBound) {
|
|
840
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
841
|
+
}
|
|
842
|
+
lines.push(`% my $${sortedHoist} = ${sorted};`)
|
|
1111
843
|
}
|
|
1112
844
|
lines.push(`% for my ${indexVar} (0..$#{${array}}) {`)
|
|
1113
845
|
if (loop.iterationShape !== 'keys') {
|
|
@@ -1130,12 +862,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1130
862
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
1131
863
|
if (loop.filterPredicate) {
|
|
1132
864
|
let filterCond: string
|
|
1133
|
-
if (loop.filterPredicate.
|
|
1134
|
-
filterCond = this.renderBlockBodyCondition(
|
|
1135
|
-
loop.filterPredicate.blockBody,
|
|
1136
|
-
loop.filterPredicate.param
|
|
1137
|
-
)
|
|
1138
|
-
} else if (loop.filterPredicate.predicate) {
|
|
865
|
+
if (loop.filterPredicate.predicate) {
|
|
1139
866
|
filterCond = this.renderPerlFilterExpr(
|
|
1140
867
|
loop.filterPredicate.predicate,
|
|
1141
868
|
loop.filterPredicate.param
|
|
@@ -1195,6 +922,17 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1195
922
|
if (value.parts) {
|
|
1196
923
|
return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`
|
|
1197
924
|
}
|
|
925
|
+
// Inline object-literal child prop (carousel's `opts={{ align: 'start' }}`):
|
|
926
|
+
// lower to a Perl hashref so the child can serialize it (`data-opts`),
|
|
927
|
+
// instead of refusing the bare object with BF101. (#1971 Perl) Read the
|
|
928
|
+
// IR-carried structured `ParsedExpr` tree (#2018) instead of re-parsing
|
|
929
|
+
// `value.expr` with `ts.createSourceFile`; the lowering returns null for
|
|
930
|
+
// any non-object-literal shape, so the common non-object case falls
|
|
931
|
+
// straight through to the bare-expression path below.
|
|
932
|
+
if (value.parsed) {
|
|
933
|
+
const hashref = objectLiteralExprToPerlHashref(this.spreadCtx, value.parsed)
|
|
934
|
+
if (hashref !== null) return `${perlHashKey(name)} => ${hashref}`
|
|
935
|
+
}
|
|
1198
936
|
return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`
|
|
1199
937
|
},
|
|
1200
938
|
emitSpread: (value) => {
|
|
@@ -1511,7 +1249,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1511
1249
|
// OMITS the key (`bf->spread_attrs` does NOT filter empty
|
|
1512
1250
|
// strings, so we cannot always-include it). Mirrors the Go
|
|
1513
1251
|
// adapter's IIFE-of-maps lowering (#textarea).
|
|
1514
|
-
|
|
1252
|
+
// Read the spread's IR-carried `ParsedExpr` tree (#2018) instead of
|
|
1253
|
+
// re-parsing `trimmed` with `ts.createSourceFile`.
|
|
1254
|
+
const ternaryHashref = conditionalSpreadToPerl(this.spreadCtx, value.parsed)
|
|
1515
1255
|
if (ternaryHashref !== null) {
|
|
1516
1256
|
return `<%== bf->spread_attrs(${ternaryHashref}) %>`
|
|
1517
1257
|
}
|
|
@@ -1528,7 +1268,15 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1528
1268
|
if (localConst?.value !== undefined) {
|
|
1529
1269
|
const initTrimmed = localConst.value.trim()
|
|
1530
1270
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
1531
|
-
const
|
|
1271
|
+
// The local const's initializer text isn't carried as a structured
|
|
1272
|
+
// tree on the spread attr, so parse it once via the shared
|
|
1273
|
+
// `parseExpression` (the analyzer's own entry) — not
|
|
1274
|
+
// `ts.createSourceFile` — mirroring go-template's same local-const
|
|
1275
|
+
// resolution path.
|
|
1276
|
+
const resolved = conditionalSpreadToPerl(
|
|
1277
|
+
this.spreadCtx,
|
|
1278
|
+
parseExpression(initTrimmed),
|
|
1279
|
+
)
|
|
1532
1280
|
if (resolved !== null) {
|
|
1533
1281
|
return `<%== bf->spread_attrs(${resolved}) %>`
|
|
1534
1282
|
}
|
|
@@ -1648,106 +1396,22 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1648
1396
|
// now lowering `.length` on a higher-order object to
|
|
1649
1397
|
// `scalar(@{...})`, the canonical
|
|
1650
1398
|
// `x.tags.filter(t => t.active).length > 0` shape lowers
|
|
1651
|
-
// cleanly
|
|
1652
|
-
//
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
//
|
|
1656
|
-
// canonical case
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
): string {
|
|
1668
|
-
const localVarMap = new Map<string, string>()
|
|
1669
|
-
const paths = this.collectReturnPaths(statements, [], localVarMap, param)
|
|
1670
|
-
|
|
1671
|
-
if (paths.length === 0) return '1'
|
|
1672
|
-
if (paths.length === 1) return this.buildSinglePathCondition(paths[0], param, localVarMap)
|
|
1673
|
-
|
|
1674
|
-
// Multiple paths: build OR condition
|
|
1675
|
-
const parts: string[] = []
|
|
1676
|
-
for (const path of paths) {
|
|
1677
|
-
if (path.result.kind === 'literal' && path.result.literalType === 'boolean' && path.result.value === false) continue
|
|
1678
|
-
const cond = this.buildSinglePathCondition(path, param, localVarMap)
|
|
1679
|
-
if (cond !== '0') parts.push(cond)
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
if (parts.length === 0) return '0'
|
|
1683
|
-
if (parts.length === 1) return parts[0]
|
|
1684
|
-
return `(${parts.join(' || ')})`
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
private collectReturnPaths(
|
|
1688
|
-
statements: ParsedStatement[],
|
|
1689
|
-
currentConditions: ParsedExpr[],
|
|
1690
|
-
localVarMap: Map<string, string>,
|
|
1691
|
-
param: string
|
|
1692
|
-
): Array<{ conditions: ParsedExpr[]; result: ParsedExpr }> {
|
|
1693
|
-
const paths: Array<{ conditions: ParsedExpr[]; result: ParsedExpr }> = []
|
|
1694
|
-
|
|
1695
|
-
for (const stmt of statements) {
|
|
1696
|
-
if (stmt.kind === 'var-decl') {
|
|
1697
|
-
if (stmt.init.kind === 'call' && stmt.init.callee.kind === 'identifier') {
|
|
1698
|
-
localVarMap.set(stmt.name, stmt.init.callee.name)
|
|
1699
|
-
}
|
|
1700
|
-
} else if (stmt.kind === 'return') {
|
|
1701
|
-
paths.push({ conditions: [...currentConditions], result: stmt.value })
|
|
1702
|
-
break
|
|
1703
|
-
} else if (stmt.kind === 'if') {
|
|
1704
|
-
const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param)
|
|
1705
|
-
paths.push(...thenPaths)
|
|
1706
|
-
|
|
1707
|
-
if (stmt.alternate) {
|
|
1708
|
-
const negated: ParsedExpr = { kind: 'unary', op: '!', argument: stmt.condition }
|
|
1709
|
-
const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param)
|
|
1710
|
-
paths.push(...elsePaths)
|
|
1711
|
-
} else {
|
|
1712
|
-
currentConditions.push({ kind: 'unary', op: '!', argument: stmt.condition })
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
return paths
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
private buildSinglePathCondition(
|
|
1721
|
-
path: { conditions: ParsedExpr[]; result: ParsedExpr },
|
|
1722
|
-
param: string,
|
|
1723
|
-
localVarMap: Map<string, string>
|
|
1724
|
-
): string {
|
|
1725
|
-
if (path.result.kind === 'literal' && path.result.literalType === 'boolean') {
|
|
1726
|
-
if (path.result.value === true) {
|
|
1727
|
-
if (path.conditions.length === 0) return '1'
|
|
1728
|
-
return this.renderConditionsAnd(path.conditions, param, localVarMap)
|
|
1729
|
-
}
|
|
1730
|
-
return '0'
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
if (path.conditions.length === 0) {
|
|
1734
|
-
return this.renderPerlFilterExpr(path.result, param, localVarMap)
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap)
|
|
1738
|
-
const resultPart = this.renderPerlFilterExpr(path.result, param, localVarMap)
|
|
1739
|
-
return `(${condPart} && ${resultPart})`
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
private renderConditionsAnd(
|
|
1743
|
-
conditions: ParsedExpr[],
|
|
1744
|
-
param: string,
|
|
1745
|
-
localVarMap: Map<string, string>
|
|
1746
|
-
): string {
|
|
1747
|
-
if (conditions.length === 0) return '1'
|
|
1748
|
-
if (conditions.length === 1) return this.renderPerlFilterExpr(conditions[0], param, localVarMap)
|
|
1749
|
-
const parts = conditions.map(c => this.renderPerlFilterExpr(c, param, localVarMap))
|
|
1750
|
-
return `(${parts.join(' && ')})`
|
|
1399
|
+
// cleanly, and nested `filter` / `every` / `some` lower to real
|
|
1400
|
+
// inline `grep` forms. The shapes the emitter can only DEGRADE
|
|
1401
|
+
// (nested `find*`, sort / reduce / flatMap inside a predicate)
|
|
1402
|
+
// surface BF101 through the hook below instead of silently
|
|
1403
|
+
// rewriting the predicate (#2038). Wholesale refusal would block
|
|
1404
|
+
// the canonical case #1443 exists to enable, so the gate lives at
|
|
1405
|
+
// the exact degrade points inside `MojoFilterEmitter.callbackMethod`.
|
|
1406
|
+
return emitParsedExpr(
|
|
1407
|
+
expr,
|
|
1408
|
+
new MojoFilterEmitter(
|
|
1409
|
+
param,
|
|
1410
|
+
localVarMap,
|
|
1411
|
+
n => this._isStringValueName(n),
|
|
1412
|
+
(message, reason) => this._recordExprBF101(message, reason),
|
|
1413
|
+
),
|
|
1414
|
+
)
|
|
1751
1415
|
}
|
|
1752
1416
|
|
|
1753
1417
|
// ===========================================================================
|
|
@@ -1863,145 +1527,48 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1863
1527
|
return true
|
|
1864
1528
|
}
|
|
1865
1529
|
|
|
1866
|
-
|
|
1867
1530
|
/**
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
1874
|
-
* The condition is translated via `convertExpressionToPerl` (a bare
|
|
1875
|
-
* prop ident becomes `$describedBy`; Perl truthiness handles the
|
|
1876
|
-
* test). Object literals become Perl hashrefs with `=>`; string-
|
|
1877
|
-
* literal keys are quoted, values resolve via `convertExpressionToPerl`.
|
|
1878
|
-
*
|
|
1879
|
-
* Returns null when the expression is NOT this shape, or when a part
|
|
1880
|
-
* can't be faithfully lowered (non-static key, etc.) so the caller
|
|
1881
|
-
* falls back to the standard `convertExpressionToPerl` path (which
|
|
1882
|
-
* records BF101). Scoped strictly to ternary-of-object-literals so no
|
|
1883
|
-
* other spread shape regresses.
|
|
1531
|
+
* Build the EmitContext seam the top-level `ParsedExpr` emitter depends on.
|
|
1532
|
+
* Built as a private object (the adapter does NOT `implements MojoEmitContext`)
|
|
1533
|
+
* so the wrapped bookkeeping — `_searchParamsLocals`, the const/record
|
|
1534
|
+
* resolvers, BF101 recording, the filter-predicate entry — stays private and
|
|
1535
|
+
* off the exported adapter's public type, matching the Go adapter's
|
|
1536
|
+
* `emitCtx` and the `spreadCtx` / `memoCtx` seams below.
|
|
1884
1537
|
*/
|
|
1885
|
-
private
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
let n = e
|
|
1895
|
-
while (ts.isParenthesizedExpression(n)) n = n.expression
|
|
1896
|
-
return n
|
|
1897
|
-
}
|
|
1898
|
-
const whenTrue = unwrap(node.whenTrue)
|
|
1899
|
-
const whenFalse = unwrap(node.whenFalse)
|
|
1900
|
-
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
1901
|
-
return null
|
|
1538
|
+
private get emitCtx(): MojoEmitContext {
|
|
1539
|
+
return {
|
|
1540
|
+
_searchParamsLocals: this._searchParamsLocals,
|
|
1541
|
+
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
|
|
1542
|
+
resolveLiteralConst: (name) => this.resolveLiteralConst(name),
|
|
1543
|
+
resolveStaticRecordLiteral: (o, k) => this.resolveStaticRecordLiteral(o, k),
|
|
1544
|
+
_isStringValueName: (name) => this._isStringValueName(name),
|
|
1545
|
+
_recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
|
|
1546
|
+
_renderPerlFilterExprPublic: (e, p) => this._renderPerlFilterExprPublic(e, p),
|
|
1902
1547
|
}
|
|
1903
|
-
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf))
|
|
1904
|
-
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf)
|
|
1905
|
-
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf)
|
|
1906
|
-
if (truePerl === null || falsePerl === null) return null
|
|
1907
|
-
return `${condPerl} ? ${truePerl} : ${falsePerl}`
|
|
1908
1548
|
}
|
|
1909
1549
|
|
|
1910
1550
|
/**
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1551
|
+
* Build the narrow context the extracted spread lowering depends on. Passing
|
|
1552
|
+
* a purpose-built object (rather than `this`) keeps the adapter's bookkeeping
|
|
1553
|
+
* members private — they stay internal implementation detail, not part of the
|
|
1554
|
+
* exported class's public surface.
|
|
1915
1555
|
*/
|
|
1916
|
-
private
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
let key: string
|
|
1924
|
-
if (ts.isIdentifier(prop.name)) {
|
|
1925
|
-
key = prop.name.text
|
|
1926
|
-
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
1927
|
-
key = prop.name.text
|
|
1928
|
-
} else {
|
|
1929
|
-
return null
|
|
1930
|
-
}
|
|
1931
|
-
const initNode = (() => {
|
|
1932
|
-
let n: ts.Expression = prop.initializer
|
|
1933
|
-
while (ts.isParenthesizedExpression(n)) n = n.expression
|
|
1934
|
-
return n
|
|
1935
|
-
})()
|
|
1936
|
-
const indexed = this.recordIndexAccessToPerl(initNode)
|
|
1937
|
-
if (
|
|
1938
|
-
indexed === null &&
|
|
1939
|
-
ts.isElementAccessExpression(initNode) &&
|
|
1940
|
-
initNode.argumentExpression &&
|
|
1941
|
-
!ts.isNumericLiteral(initNode.argumentExpression) &&
|
|
1942
|
-
!ts.isStringLiteral(initNode.argumentExpression)
|
|
1943
|
-
) {
|
|
1944
|
-
// Variable-index record access (`sizeMap[size]`) that the
|
|
1945
|
-
// static-inline path couldn't resolve — a non-scalar record
|
|
1946
|
-
// value, or a non-const receiver. Since #1897 made variable
|
|
1947
|
-
// indices parseable (`index-access`), the generic value lowering
|
|
1948
|
-
// would now emit `$sizeMap->{$size}` against an UNBOUND module
|
|
1949
|
-
// const instead of refusing. Record BF101 and bail so the whole
|
|
1950
|
-
// spread surfaces the out-of-shape diagnostic, matching the
|
|
1951
|
-
// pre-#1897 behaviour (the refusal then was a side effect of the
|
|
1952
|
-
// value lowering). (A bound receiver — a signal getter like
|
|
1953
|
-
// `selected()[index]` — is an attribute value, not a spread
|
|
1954
|
-
// member, and never reaches here.)
|
|
1955
|
-
this.errors.push({
|
|
1956
|
-
code: 'BF101',
|
|
1957
|
-
severity: 'error',
|
|
1958
|
-
message: `Spread object value '${initNode.getText(sf)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
|
|
1959
|
-
loc: { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1960
|
-
suggestion: {
|
|
1961
|
-
message: 'Index a record whose values are number/string literals, or move the spread into a `\'use client\'` component so hydration computes it.',
|
|
1962
|
-
},
|
|
1963
|
-
})
|
|
1964
|
-
return null
|
|
1965
|
-
}
|
|
1966
|
-
const valPerl =
|
|
1967
|
-
indexed !== null
|
|
1968
|
-
? indexed
|
|
1969
|
-
: this.convertExpressionToPerl(prop.initializer.getText(sf))
|
|
1970
|
-
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`)
|
|
1556
|
+
private get spreadCtx(): MojoSpreadContext {
|
|
1557
|
+
return {
|
|
1558
|
+
componentName: this.componentName,
|
|
1559
|
+
errors: this.errors,
|
|
1560
|
+
localConstants: this.localConstants,
|
|
1561
|
+
propsParams: this.propsParams,
|
|
1562
|
+
convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed),
|
|
1971
1563
|
}
|
|
1972
|
-
return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`
|
|
1973
1564
|
}
|
|
1974
1565
|
|
|
1975
|
-
/**
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
* literal whose property values are all scalar (number/string)
|
|
1979
|
-
* literals under static (string-literal or identifier) keys
|
|
1980
|
-
* (a `Record<staticKeys, scalar>` map like `sizeMap`), AND
|
|
1981
|
-
* - `KEY` is a bare identifier that is a prop.
|
|
1982
|
-
* Emits an inline indexed Perl hashref:
|
|
1983
|
-
* `{ 'sm' => 16, 'md' => 20, ... }->{$size}`
|
|
1984
|
-
*
|
|
1985
|
-
* Returns the Perl string when convertible, else `null` so the caller
|
|
1986
|
-
* falls back to its normal value lowering (which records BF101 for an
|
|
1987
|
-
* unsupported shape). Mirror of the Go adapter's `recordIndexAccessToGoMap`.
|
|
1988
|
-
*/
|
|
1989
|
-
private recordIndexAccessToPerl(val: ts.Expression): string | null {
|
|
1990
|
-
// Shared structural parse (single source of truth in `@barefootjs/jsx`);
|
|
1991
|
-
// this wrapper only does the Perl-specific emit (single-quote escaping)
|
|
1992
|
-
// from the structured result.
|
|
1993
|
-
const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams)
|
|
1994
|
-
if (!parsed) return null
|
|
1995
|
-
const entries = parsed.entries.map(e => {
|
|
1996
|
-
const mapVal =
|
|
1997
|
-
e.value.kind === 'number' ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`
|
|
1998
|
-
return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`
|
|
1999
|
-
})
|
|
2000
|
-
return `{ ${entries.join(', ')} }->{$${parsed.indexPropName}}`
|
|
1566
|
+
/** Build the narrow context the extracted memo seeding depends on. */
|
|
1567
|
+
private get memoCtx(): MojoMemoContext {
|
|
1568
|
+
return { convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed) }
|
|
2001
1569
|
}
|
|
2002
1570
|
|
|
2003
|
-
|
|
2004
|
-
private convertExpressionToPerl(expr: string): string {
|
|
1571
|
+
private convertExpressionToPerl(expr: string, preParsed?: ParsedExpr): string {
|
|
2005
1572
|
// Parse-first lowering — parity with the Go adapter's
|
|
2006
1573
|
// `convertExpressionToGo`. Parse the JS expression once, gate it on
|
|
2007
1574
|
// the shared `isSupported`, and render every supported shape through
|
|
@@ -2011,16 +1578,48 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
2011
1578
|
// string-rewriting pipeline. Unsupported shapes (un-lowered methods,
|
|
2012
1579
|
// unparseable hand-written JS, etc.) surface as BF101 with the
|
|
2013
1580
|
// `/* @client */` escape hatch instead of being silently mangled.
|
|
2014
|
-
|
|
2015
|
-
|
|
1581
|
+
//
|
|
1582
|
+
// `preParsed` is the IR-carried `ParsedExpr` tree (cf. go-template's
|
|
1583
|
+
// `convertExpressionToGo(jsExpr, out?, preParsed?)`); when present it is
|
|
1584
|
+
// used directly instead of re-parsing `expr`, so spread condition/value
|
|
1585
|
+
// lowering threads the carried tree through without a stringify→re-parse
|
|
1586
|
+
// round-trip. The diagnostic text is then derived from the tree
|
|
1587
|
+
// (`stringifyParsedExpr`) so callers can pass `''` for `expr`.
|
|
1588
|
+
let parsed: ParsedExpr
|
|
1589
|
+
if (preParsed) {
|
|
1590
|
+
parsed = preParsed
|
|
1591
|
+
} else {
|
|
1592
|
+
const trimmed = expr.trim()
|
|
1593
|
+
if (trimmed === '') return "''"
|
|
1594
|
+
parsed = parseExpression(trimmed)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// Registered call lowerings (#2057) — including the built-in `queryHref`
|
|
1598
|
+
// plugin (#2042), which lowers `queryHref(base, { … })` to a neutral
|
|
1599
|
+
// `guard-list` on the `query` helper → `bf->query(base, <triples>)`.
|
|
1600
|
+
// Recognised before the support gate because the object-literal arg is
|
|
1601
|
+
// otherwise `unsupported` (BF101). The `bf->query` helper includes a pair iff
|
|
1602
|
+
// its guard is truthy AND its value is a non-empty string (the client's
|
|
1603
|
+
// `if (value)`): a plain `key: v` passes guard `1`, a conditional
|
|
1604
|
+
// `key: cond ? v : undefined` passes the lowered cond. Only the `query`
|
|
1605
|
+
// helper renders to `bf->query`; another guard-list helper must not be
|
|
1606
|
+
// silently mis-rendered as a query.
|
|
1607
|
+
if (parsed.kind === 'call') {
|
|
1608
|
+
for (const matcher of this._loweringMatchers) {
|
|
1609
|
+
const node = matcher(parsed.callee, parsed.args)
|
|
1610
|
+
if (node?.kind === 'guard-list' && node.helper === 'query') {
|
|
1611
|
+
const argsGo = queryHrefArgs(node, n => this.renderParsedExprToPerl(n))
|
|
1612
|
+
return `bf->query(${argsGo.join(', ')})`
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
2016
1616
|
|
|
2017
|
-
const parsed = parseExpression(trimmed)
|
|
2018
1617
|
const support = isSupported(parsed)
|
|
2019
1618
|
if (!support.supported) {
|
|
2020
1619
|
this.errors.push({
|
|
2021
1620
|
code: 'BF101',
|
|
2022
1621
|
severity: 'error',
|
|
2023
|
-
message: `Expression not supported: ${
|
|
1622
|
+
message: `Expression not supported: ${preParsed ? stringifyParsedExpr(parsed) : expr.trim()}`,
|
|
2024
1623
|
loc: { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
2025
1624
|
suggestion: {
|
|
2026
1625
|
message: support.reason
|
|
@@ -2045,7 +1644,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
2045
1644
|
* (#1250 phase 1B).
|
|
2046
1645
|
*/
|
|
2047
1646
|
private renderParsedExprToPerl(expr: ParsedExpr): string {
|
|
2048
|
-
return emitParsedExpr(expr, new MojoTopLevelEmitter(this))
|
|
1647
|
+
return emitParsedExpr(expr, new MojoTopLevelEmitter(this.emitCtx))
|
|
2049
1648
|
}
|
|
2050
1649
|
|
|
2051
1650
|
/**
|
|
@@ -2055,11 +1654,11 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
2055
1654
|
*/
|
|
2056
1655
|
/** Whether `name` (a signal getter or prop) holds a string value, so an
|
|
2057
1656
|
* equality comparison against it should use Perl `eq`/`ne` (#1672). */
|
|
2058
|
-
_isStringValueName(name: string): boolean {
|
|
1657
|
+
private _isStringValueName(name: string): boolean {
|
|
2059
1658
|
return this.stringValueNames.has(name)
|
|
2060
1659
|
}
|
|
2061
1660
|
|
|
2062
|
-
_recordExprBF101(message: string, reason?: string): void {
|
|
1661
|
+
private _recordExprBF101(message: string, reason?: string): void {
|
|
2063
1662
|
this.errors.push({
|
|
2064
1663
|
code: 'BF101',
|
|
2065
1664
|
severity: 'error',
|
|
@@ -2074,845 +1673,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
2074
1673
|
}
|
|
2075
1674
|
|
|
2076
1675
|
/** Internal hook for higher-order: predicate body re-uses the filter emitter. */
|
|
2077
|
-
_renderPerlFilterExprPublic(expr: ParsedExpr, param: string): string {
|
|
1676
|
+
private _renderPerlFilterExprPublic(expr: ParsedExpr, param: string): string {
|
|
2078
1677
|
return this.renderPerlFilterExpr(expr, param)
|
|
2079
1678
|
}
|
|
2080
1679
|
}
|
|
2081
1680
|
|
|
2082
|
-
// ===========================================================================
|
|
2083
|
-
// ParsedExpr emitters (#1250 phase 1B)
|
|
2084
|
-
// ===========================================================================
|
|
2085
|
-
|
|
2086
|
-
/**
|
|
2087
|
-
* Lowering for `array-method` IR nodes (#1443) — shared between the
|
|
2088
|
-
* filter and top-level emitters so the Embedded Perl form stays
|
|
2089
|
-
* consistent regardless of which context the chain lands in.
|
|
2090
|
-
*
|
|
2091
|
-
* The exhaustive switch on `method` paired with `assertNever` makes
|
|
2092
|
-
* adding a new variant to `ArrayMethod` a TS compile error here, not
|
|
2093
|
-
* a silent runtime no-op — the drift defence we already apply to
|
|
2094
|
-
* `ParsedExpr.kind` extended to its sub-discriminator.
|
|
2095
|
-
*/
|
|
2096
|
-
function renderArrayMethod(
|
|
2097
|
-
method: ArrayMethod,
|
|
2098
|
-
object: ParsedExpr,
|
|
2099
|
-
args: ParsedExpr[],
|
|
2100
|
-
emit: (e: ParsedExpr) => string,
|
|
2101
|
-
): string {
|
|
2102
|
-
switch (method) {
|
|
2103
|
-
case 'join': {
|
|
2104
|
-
// arr.join(sep) → join(sep, @{arr}). The default `${obj}->{join}`
|
|
2105
|
-
// hash-lookup fallback would emit invalid Perl, which is why the
|
|
2106
|
-
// IR carves out a dedicated method node instead of routing
|
|
2107
|
-
// through the generic call dispatcher. `.join()` defaults the
|
|
2108
|
-
// separator to `,` (JS) and ignores any extra argument.
|
|
2109
|
-
const obj = emit(object)
|
|
2110
|
-
const sep = args.length >= 1 ? emit(args[0]) : `','`
|
|
2111
|
-
return `join(${sep}, @{${obj}})`
|
|
2112
|
-
}
|
|
2113
|
-
case 'includes': {
|
|
2114
|
-
// Both `arr.includes(x)` and `str.includes(sub)` route here —
|
|
2115
|
-
// the parser can't disambiguate the receiver type. The Mojo
|
|
2116
|
-
// runtime's `bf->includes($recv, $elem)` inspects `ref($recv)`
|
|
2117
|
-
// and dispatches: ARRAY ref scans the list with `eq`, scalar
|
|
2118
|
-
// falls back to `index(..., ...) != -1`. Helper lives in
|
|
2119
|
-
// packages/adapter-perl/lib/BarefootJS.pm.
|
|
2120
|
-
//
|
|
2121
|
-
// The `bf->` (no `$`) form matches every other helper emit —
|
|
2122
|
-
// in real Mojolicious `bf` is a controller helper; the
|
|
2123
|
-
// standalone test-render in test-render.ts rewrites the bare
|
|
2124
|
-
// `bf->` to `$bf->` so both render paths stay consistent.
|
|
2125
|
-
const obj = emit(object)
|
|
2126
|
-
const needle = emit(args[0])
|
|
2127
|
-
return `bf->includes(${obj}, ${needle})`
|
|
2128
|
-
}
|
|
2129
|
-
case 'indexOf':
|
|
2130
|
-
case 'lastIndexOf': {
|
|
2131
|
-
// Array `.indexOf(x)` / `.lastIndexOf(x)` value-equality
|
|
2132
|
-
// search. The Perl helpers (`bf->index_of`, `bf->last_index_of`)
|
|
2133
|
-
// walk the array forward / backward and compare with `eq`
|
|
2134
|
-
// (with defined/undef parity). The existing `.find` lowering
|
|
2135
|
-
// uses Perl `grep` for struct-field find — disjoint surface,
|
|
2136
|
-
// disjoint helpers.
|
|
2137
|
-
const fn = method === 'indexOf' ? 'index_of' : 'last_index_of'
|
|
2138
|
-
const obj = emit(object)
|
|
2139
|
-
const needle = emit(args[0])
|
|
2140
|
-
return `bf->${fn}(${obj}, ${needle})`
|
|
2141
|
-
}
|
|
2142
|
-
case 'at': {
|
|
2143
|
-
// `.at(i)` with negative-index support — `.at(-1)` is the
|
|
2144
|
-
// last element. The Mojo helper wraps the same `length + i`
|
|
2145
|
-
// arithmetic the Go `bf_at` does so the lowering stays
|
|
2146
|
-
// symmetric across adapters. `.at()` with no argument is `.at(0)`
|
|
2147
|
-
// (the first element); extra arguments are ignored.
|
|
2148
|
-
const obj = emit(object)
|
|
2149
|
-
const idx = args.length >= 1 ? emit(args[0]) : '0'
|
|
2150
|
-
return `bf->at(${obj}, ${idx})`
|
|
2151
|
-
}
|
|
2152
|
-
case 'concat': {
|
|
2153
|
-
// `.concat(other)` merges two arrays. Returns a new ARRAY
|
|
2154
|
-
// ref so the result composes with `.join(...)` / other
|
|
2155
|
-
// array-shape methods downstream (the canonical Tier A
|
|
2156
|
-
// conformance fixture chains `.concat(...).join(' ')`).
|
|
2157
|
-
// `.concat()` with no argument is a shallow copy — indistinguishable
|
|
2158
|
-
// from the receiver in an SSR snapshot, so it lowers to the receiver.
|
|
2159
|
-
if (args.length === 0) {
|
|
2160
|
-
return emit(object)
|
|
2161
|
-
}
|
|
2162
|
-
const a = emit(object)
|
|
2163
|
-
const b = emit(args[0])
|
|
2164
|
-
return `bf->concat(${a}, ${b})`
|
|
2165
|
-
}
|
|
2166
|
-
case 'slice': {
|
|
2167
|
-
// `.slice()` / `.slice(start)` / `.slice(start, end)`. The Mojo
|
|
2168
|
-
// helper mirrors the Go arithmetic (negative-index normalisation,
|
|
2169
|
-
// out-of-bounds clamping, empty result on start >= end). A
|
|
2170
|
-
// missing `start` defaults to 0 (full copy); an absent `end`
|
|
2171
|
-
// lowers as `undef`, which the helper treats as "to length". JS
|
|
2172
|
-
// ignores a third+ argument. Returns a new ARRAY ref so the
|
|
2173
|
-
// result composes with `.join(...)` downstream.
|
|
2174
|
-
const recv = emit(object)
|
|
2175
|
-
const start = args.length >= 1 ? emit(args[0]) : '0'
|
|
2176
|
-
const end = args.length >= 2 ? emit(args[1]) : 'undef'
|
|
2177
|
-
return `bf->slice(${recv}, ${start}, ${end})`
|
|
2178
|
-
}
|
|
2179
|
-
case 'reverse':
|
|
2180
|
-
case 'toReversed': {
|
|
2181
|
-
// Both shapes share a lowering — see the parser arm + Go
|
|
2182
|
-
// emit for the SSR-mutation-rationale. Returns a new ARRAY
|
|
2183
|
-
// ref so the result composes with `.join(...)` downstream.
|
|
2184
|
-
const recv = emit(object)
|
|
2185
|
-
return `bf->reverse(${recv})`
|
|
2186
|
-
}
|
|
2187
|
-
case 'toLowerCase': {
|
|
2188
|
-
// Perl's native `lc` is the obvious lowering — no helper
|
|
2189
|
-
// method needed. The receiver flows through `emit` so any
|
|
2190
|
-
// upstream coercion (`$value`, `$bf->string(...)`, etc.)
|
|
2191
|
-
// composes naturally.
|
|
2192
|
-
const recv = emit(object)
|
|
2193
|
-
return `lc(${recv})`
|
|
2194
|
-
}
|
|
2195
|
-
case 'toUpperCase': {
|
|
2196
|
-
// Perl's native `uc` — mirrors `toLowerCase` exactly.
|
|
2197
|
-
const recv = emit(object)
|
|
2198
|
-
return `uc(${recv})`
|
|
2199
|
-
}
|
|
2200
|
-
case 'trim': {
|
|
2201
|
-
// No Perl native `trim`; route through the `bf->trim`
|
|
2202
|
-
// helper so the regex stays in one place (and so an undef
|
|
2203
|
-
// receiver doesn't trigger a warning about applying `s///`
|
|
2204
|
-
// to undef).
|
|
2205
|
-
const recv = emit(object)
|
|
2206
|
-
return `bf->trim(${recv})`
|
|
2207
|
-
}
|
|
2208
|
-
case 'toFixed': {
|
|
2209
|
-
// `.toFixed(digits?)` — Number → fixed-decimal string. `bf->to_fixed`
|
|
2210
|
-
// mirrors JS rounding + zero-padding (default 0 digits). #1897.
|
|
2211
|
-
const recv = emit(object)
|
|
2212
|
-
const digits = args.length >= 1 ? emit(args[0]) : '0'
|
|
2213
|
-
return `bf->to_fixed(${recv}, ${digits})`
|
|
2214
|
-
}
|
|
2215
|
-
case 'split': {
|
|
2216
|
-
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
|
|
2217
|
-
// ARRAY ref via `bf->split`. With no separator the helper returns
|
|
2218
|
-
// the whole string as a single element; otherwise it quotemetas
|
|
2219
|
-
// the separator (literal match, not regex) and keeps trailing
|
|
2220
|
-
// empties (`-1`), staying byte-equal with Go's `bf_split`. The
|
|
2221
|
-
// optional `limit` caps the pieces; JS ignores a third+ argument.
|
|
2222
|
-
// See #1448 Tier B.
|
|
2223
|
-
const recv = emit(object)
|
|
2224
|
-
if (args.length === 0) {
|
|
2225
|
-
return `bf->split(${recv})`
|
|
2226
|
-
}
|
|
2227
|
-
const sep = emit(args[0])
|
|
2228
|
-
if (args.length === 1) {
|
|
2229
|
-
return `bf->split(${recv}, ${sep})`
|
|
2230
|
-
}
|
|
2231
|
-
const limit = emit(args[1])
|
|
2232
|
-
return `bf->split(${recv}, ${sep}, ${limit})`
|
|
2233
|
-
}
|
|
2234
|
-
case 'startsWith':
|
|
2235
|
-
case 'endsWith': {
|
|
2236
|
-
// `.startsWith(prefix, position?)` / `.endsWith(suffix,
|
|
2237
|
-
// endPosition?)` — string → boolean. The Perl helpers
|
|
2238
|
-
// (`bf->starts_with` / `bf->ends_with`) do a `substr`-anchored
|
|
2239
|
-
// comparison so the search string is matched literally (no regex
|
|
2240
|
-
// metachar surprises) and undef receivers stay quiet. The optional
|
|
2241
|
-
// second argument re-anchors the test; JS ignores a third+
|
|
2242
|
-
// argument. See #1448 Tier B.
|
|
2243
|
-
const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
|
|
2244
|
-
const recv = emit(object)
|
|
2245
|
-
const arg = emit(args[0])
|
|
2246
|
-
if (args.length >= 2) {
|
|
2247
|
-
return `bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`
|
|
2248
|
-
}
|
|
2249
|
-
return `bf->${fn}(${recv}, ${arg})`
|
|
2250
|
-
}
|
|
2251
|
-
case 'replace': {
|
|
2252
|
-
// `.replace(old, new)` — string-pattern form, first occurrence.
|
|
2253
|
-
// The `bf->replace` helper splices via index/substr (not `s///`)
|
|
2254
|
-
// so both the pattern and the replacement are literal — no Perl
|
|
2255
|
-
// regex metacharacters and no `$1` / `$&` interpolation in the
|
|
2256
|
-
// replacement, keeping it byte-equal with Go's `bf_replace`. The
|
|
2257
|
-
// regex-pattern form is refused upstream at the parser. See
|
|
2258
|
-
// #1448 Tier B.
|
|
2259
|
-
const recv = emit(object)
|
|
2260
|
-
const oldS = emit(args[0])
|
|
2261
|
-
const newS = emit(args[1])
|
|
2262
|
-
return `bf->replace(${recv}, ${oldS}, ${newS})`
|
|
2263
|
-
}
|
|
2264
|
-
case 'repeat': {
|
|
2265
|
-
// `.repeat(n)` — string repeated `n` times. The `bf->repeat`
|
|
2266
|
-
// helper wraps Perl's `x` operator with the same negative-count
|
|
2267
|
-
// → "" clamp and integer truncation Go's `bf_repeat` applies, so
|
|
2268
|
-
// the two adapters stay byte-equal. Full JS arity: the no-argument
|
|
2269
|
-
// form is `repeat(0)` → ""; a second+ argument is ignored.
|
|
2270
|
-
// See #1448 Tier B.
|
|
2271
|
-
const recv = emit(object)
|
|
2272
|
-
const count = args.length === 0 ? '0' : emit(args[0])
|
|
2273
|
-
return `bf->repeat(${recv}, ${count})`
|
|
2274
|
-
}
|
|
2275
|
-
case 'padStart':
|
|
2276
|
-
case 'padEnd': {
|
|
2277
|
-
// `.padStart(target, pad?)` / `.padEnd(target, pad?)`. The
|
|
2278
|
-
// `bf->pad_*` helpers default the pad to a single space when the
|
|
2279
|
-
// arg is omitted and measure length in characters, matching Go's
|
|
2280
|
-
// rune-based `bf_pad_*`. Full JS arity: the no-argument form is
|
|
2281
|
-
// `padStart(0)` → the receiver unchanged; a third+ argument is
|
|
2282
|
-
// ignored. See #1448 Tier B.
|
|
2283
|
-
const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
|
|
2284
|
-
const recv = emit(object)
|
|
2285
|
-
if (args.length === 0) {
|
|
2286
|
-
return `bf->${fn}(${recv}, 0)`
|
|
2287
|
-
}
|
|
2288
|
-
const target = emit(args[0])
|
|
2289
|
-
if (args.length === 1) {
|
|
2290
|
-
return `bf->${fn}(${recv}, ${target})`
|
|
2291
|
-
}
|
|
2292
|
-
const pad = emit(args[1])
|
|
2293
|
-
return `bf->${fn}(${recv}, ${target}, ${pad})`
|
|
2294
|
-
}
|
|
2295
|
-
default: {
|
|
2296
|
-
// TS-level exhaustiveness guard. If this throws at runtime, the
|
|
2297
|
-
// IR was constructed against a newer `ArrayMethod` variant that
|
|
2298
|
-
// this adapter hasn't been updated for — loud failure is better
|
|
2299
|
-
// than emitting a silent empty string downstream.
|
|
2300
|
-
const _exhaustive: never = method
|
|
2301
|
-
throw new Error(
|
|
2302
|
-
`renderArrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`,
|
|
2303
|
-
)
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
/**
|
|
2309
|
-
* Shared Mojo emit for `.sort(cmp)` / `.toSorted(cmp)` (#1448 Tier B).
|
|
2310
|
-
* Used by both the filter-context emitter and the top-level emitter,
|
|
2311
|
-
* plus the loop-hoist path in `renderLoop` — same emit shape across
|
|
2312
|
-
* all three so a regression in any one path surfaces consistently.
|
|
2313
|
-
*
|
|
2314
|
-
* The Perl helper accepts a hash-ref opts bag whose `keys` entry is
|
|
2315
|
-
* an ordered list of per-key hashes (room for a future `nulls` knob
|
|
2316
|
-
* without arity churn), and returns a fresh ARRAY ref so downstream
|
|
2317
|
-
* composition (`@{bf->sort(...)}` in `join(...)`, etc.) stays
|
|
2318
|
-
* straightforward.
|
|
2319
|
-
*/
|
|
2320
|
-
/**
|
|
2321
|
-
* Encode an `IRLoop.markerId` into a Perl-identifier-safe suffix
|
|
2322
|
-
* for the `bf_iter_…` hoist var. Collision-free for marker ids
|
|
2323
|
-
* that differ in any character — `-` and `_` map to distinct
|
|
2324
|
-
* encodings (`_x2d` vs `__`) so `l-0` and `l_0` stay distinct.
|
|
2325
|
-
*
|
|
2326
|
-
* Today the IR only emits `l<digits>` so the encoding is mostly
|
|
2327
|
-
* an identity, but pinning collision-freeness up front avoids a
|
|
2328
|
-
* silent variable-shadow bug if a future marker generator widens
|
|
2329
|
-
* the alphabet.
|
|
2330
|
-
*/
|
|
2331
|
-
function perlIdentifierFromMarkerId(markerId: string): string {
|
|
2332
|
-
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) =>
|
|
2333
|
-
ch === '_' ? '__' : `_x${ch.charCodeAt(0).toString(16)}`
|
|
2334
|
-
)
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
function renderSortMethod(recv: string, c: SortComparator): string {
|
|
2338
|
-
// One hash per comparison key, in priority order, under `keys`. A
|
|
2339
|
-
// simple comparator yields a one-element list; a `||`-chained
|
|
2340
|
-
// multi-key comparator yields one per operand. `bf->sort` walks them
|
|
2341
|
-
// in order, falling through to the next on a tie.
|
|
2342
|
-
const keyHashes = c.keys.map((k) => {
|
|
2343
|
-
const keyEntry =
|
|
2344
|
-
k.key.kind === 'self'
|
|
2345
|
-
? `key_kind => 'self'`
|
|
2346
|
-
: `key_kind => 'field', key => '${k.key.field}'`
|
|
2347
|
-
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`
|
|
2348
|
-
})
|
|
2349
|
-
return `bf->sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
/**
|
|
2353
|
-
* Render a `.reduce(fn, init)` arithmetic fold (#1448 Tier C) as a
|
|
2354
|
-
* `bf->reduce(...)` call. The structured `ReduceOp` maps to the Perl
|
|
2355
|
-
* helper's options hash:
|
|
2356
|
-
*
|
|
2357
|
-
* bf->reduce($recv, { op => '+', key_kind => 'field', key => 'duration',
|
|
2358
|
-
* type => 'numeric', init => 0 })
|
|
2359
|
-
*
|
|
2360
|
-
* A numeric init passes through as a bare Perl number (`0`, `-1`); a
|
|
2361
|
-
* string init (concat fold) is re-quoted from its literal contents.
|
|
2362
|
-
*/
|
|
2363
|
-
function renderReduceMethod(recv: string, op: ReduceOp, direction: 'left' | 'right'): string {
|
|
2364
|
-
const keyEntry =
|
|
2365
|
-
op.key.kind === 'self'
|
|
2366
|
-
? `key_kind => 'self'`
|
|
2367
|
-
: `key_kind => 'field', key => '${op.key.field}'`
|
|
2368
|
-
// `op.init` is the decoded seed value. A numeric seed is already a
|
|
2369
|
-
// canonical decimal literal Perl reads directly; a concat seed is the
|
|
2370
|
-
// string contents, embedded in a single-quoted Perl literal. The `'`
|
|
2371
|
-
// escape is REQUIRED: a seed decoded from a double-quoted JS literal
|
|
2372
|
-
// (e.g. `"a'b"`) is escape-free yet contains an apostrophe. A literal
|
|
2373
|
-
// backslash can't occur (it would need a `\\` escape, which the parser
|
|
2374
|
-
// refuses), but escaping it too keeps this self-contained.
|
|
2375
|
-
const init =
|
|
2376
|
-
op.type === 'string'
|
|
2377
|
-
? `'${op.init.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
2378
|
-
: op.init
|
|
2379
|
-
// `direction` is "left" (reduce) or "right" (reduceRight); the Perl
|
|
2380
|
-
// helper reverses the list for "right". Only observable for concat.
|
|
2381
|
-
return `bf->reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
// `.flat(depth?)` → `bf->flat($recv, $depth)`. The `Infinity` form lowers
|
|
2385
|
-
// to the `-1` sentinel (flatten fully); a finite depth flattens that many
|
|
2386
|
-
// levels (`0` = shallow copy). See `sub flat` in BarefootJS.pm. (#1448)
|
|
2387
|
-
function renderFlatMethod(recv: string, depth: FlatDepth): string {
|
|
2388
|
-
const d = depth === 'infinity' ? -1 : depth
|
|
2389
|
-
return `bf->flat(${recv}, ${d})`
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
// `.flatMap(i => i)` / `.flatMap(i => i.field)` → `bf->flat_map($recv,
|
|
2393
|
-
// 'self'|'field', 'field')`, and the array-literal tuple form
|
|
2394
|
-
// `i => [i.a, i.b]` → `bf->flat_map_tuple($recv, ['field','a'], ...)`
|
|
2395
|
-
// (one arrayref per leaf). The field key is the raw JS prop name (Perl
|
|
2396
|
-
// hashes are keyed by it), mirroring `bf->reduce`. See `sub flat_map` /
|
|
2397
|
-
// `sub flat_map_tuple` in BarefootJS.pm.
|
|
2398
|
-
function renderFlatMapMethod(recv: string, op: FlatMapOp): string {
|
|
2399
|
-
const proj = op.projection
|
|
2400
|
-
if (proj.kind === 'tuple') {
|
|
2401
|
-
const specs = proj.elements
|
|
2402
|
-
.map(l => (l.kind === 'self' ? `['self', '']` : `['field', '${l.field}']`))
|
|
2403
|
-
.join(', ')
|
|
2404
|
-
return `bf->flat_map_tuple(${recv}, ${specs})`
|
|
2405
|
-
}
|
|
2406
|
-
if (proj.kind === 'self') return `bf->flat_map(${recv}, 'self', '')`
|
|
2407
|
-
return `bf->flat_map(${recv}, 'field', '${proj.field}')`
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
/** True when `type` is the `string` primitive. */
|
|
2411
|
-
function isStringTypeInfo(type: TypeInfo | undefined): boolean {
|
|
2412
|
-
return type?.kind === 'primitive' && type.primitive === 'string'
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
/** True when `initialValue` is a bare string-literal expression (`'x'` /
|
|
2416
|
-
* `"x"`), used as a fallback for signals whose type wasn't inferred. */
|
|
2417
|
-
function isBareStringLiteral(initialValue: string | undefined): boolean {
|
|
2418
|
-
if (!initialValue) return false
|
|
2419
|
-
const v = initialValue.trim()
|
|
2420
|
-
return (v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
/**
|
|
2424
|
-
* Whether a comparison operand is string-typed, so JS `===`/`!==` against it
|
|
2425
|
-
* must lower to Perl `eq`/`ne` instead of numeric `==`/`!=` (#1672). Covers a
|
|
2426
|
-
* string literal, a string-signal getter call (`sel()`), and a string prop
|
|
2427
|
-
* access (`props.x`). `isStringName` reports whether a getter/prop name is
|
|
2428
|
-
* known-string. Loop-element fields (`t.id`) on untyped arrays have no known
|
|
2429
|
-
* type and stay undetected — a separate, narrower gap.
|
|
2430
|
-
*/
|
|
2431
|
-
function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string) => boolean): boolean {
|
|
2432
|
-
if (expr.kind === 'literal' && expr.literalType === 'string') return true
|
|
2433
|
-
if (expr.kind === 'call' && expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
2434
|
-
return isStringName(expr.callee.name)
|
|
2435
|
-
}
|
|
2436
|
-
if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
|
|
2437
|
-
return isStringName(expr.property)
|
|
2438
|
-
}
|
|
2439
|
-
return false
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
/**
|
|
2443
|
-
* Lower `arr[index]` to a Perl deref. Perl distinguishes array
|
|
2444
|
-
* (`->[$i]`) from hash (`->{$k}`) access, which JS's single `[]` does
|
|
2445
|
-
* not — so we pick by the index expression's type: a string-typed key
|
|
2446
|
-
* derefs the hash, anything else (the common loop-index / arithmetic
|
|
2447
|
-
* case, e.g. `selected()[index]`) derefs the array. #1897.
|
|
2448
|
-
*/
|
|
2449
|
-
function emitIndexAccessPerl(
|
|
2450
|
-
object: ParsedExpr,
|
|
2451
|
-
index: ParsedExpr,
|
|
2452
|
-
emit: (e: ParsedExpr) => string,
|
|
2453
|
-
isStringName: (n: string) => boolean,
|
|
2454
|
-
): string {
|
|
2455
|
-
const i = emit(index)
|
|
2456
|
-
return isStringTypedOperand(index, isStringName)
|
|
2457
|
-
? `${emit(object)}->{${i}}`
|
|
2458
|
-
: `${emit(object)}->[${i}]`
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
/**
|
|
2462
|
-
* Lowering for the predicate body of a filter / every / some / find,
|
|
2463
|
-
* plus the same shape used by `renderBlockBodyCondition` for complex
|
|
2464
|
-
* block-body filters. Identifiers resolve against:
|
|
2465
|
-
* - the predicate's loop param (`$param`),
|
|
2466
|
-
* - `localVarMap` aliases declared inside the block body, then
|
|
2467
|
-
* - a bare `$name` fallback for signals captured by the closure.
|
|
2468
|
-
*
|
|
2469
|
-
* Methods that have no filter-context meaning (template-literal,
|
|
2470
|
-
* arrow-fn, conditional, unsupported) fall back to the `'1'` literal
|
|
2471
|
-
* the original switch's `default` arm returned — those shapes never
|
|
2472
|
-
* arose inside the predicates the adapter actually accepts.
|
|
2473
|
-
*/
|
|
2474
|
-
class MojoFilterEmitter implements ParsedExprEmitter {
|
|
2475
|
-
constructor(
|
|
2476
|
-
private readonly param: string,
|
|
2477
|
-
private readonly localVarMap: Map<string, string>,
|
|
2478
|
-
// Reports whether a getter/prop name is string-typed, so `===`/`!==`
|
|
2479
|
-
// against it lowers to `eq`/`ne` (#1672). Defaults to "never" for callers
|
|
2480
|
-
// that don't thread it through.
|
|
2481
|
-
private readonly isStringName: (n: string) => boolean = () => false,
|
|
2482
|
-
) {}
|
|
2483
|
-
|
|
2484
|
-
identifier(name: string): string {
|
|
2485
|
-
if (name === this.param) return `$${this.param}`
|
|
2486
|
-
const signal = this.localVarMap.get(name)
|
|
2487
|
-
if (signal) return `$${signal}`
|
|
2488
|
-
return `$${name}`
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
2492
|
-
if (literalType === 'string') return `'${value}'`
|
|
2493
|
-
if (literalType === 'boolean') return value ? '1' : '0'
|
|
2494
|
-
if (literalType === 'null') return 'undef'
|
|
2495
|
-
return String(value)
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
2499
|
-
// `.length` on a higher-order result (e.g.
|
|
2500
|
-
// `x.tags.filter(t => t.active).length > 0` inside the outer
|
|
2501
|
-
// filter predicate, #1443). The higher-order emit produces an
|
|
2502
|
-
// anonymous array ref `[grep ...]`; reading `->{length}` on that
|
|
2503
|
-
// is undef at runtime, which is why the pre-#1443 `containsHigherOrder`
|
|
2504
|
-
// gate refused this shape outright. Lowering `.length` to
|
|
2505
|
-
// `scalar(@{...})` makes the result a real Perl integer.
|
|
2506
|
-
if (property === 'length' && (object.kind === 'higher-order' || object.kind === 'array-literal')) {
|
|
2507
|
-
return `scalar(@{${emit(object)}})`
|
|
2508
|
-
}
|
|
2509
|
-
return `${emit(object)}->{${property}}`
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2513
|
-
return emitIndexAccessPerl(object, index, emit, this.isStringName)
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2517
|
-
// Signal getter calls: filter() → $filter
|
|
2518
|
-
if (callee.kind === 'identifier' && args.length === 0) {
|
|
2519
|
-
return `$${callee.name}`
|
|
2520
|
-
}
|
|
2521
|
-
return emit(callee)
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2525
|
-
const arg = emit(argument)
|
|
2526
|
-
if (op === '!') {
|
|
2527
|
-
// Wrap binary/logical operands in parens to dodge Perl precedence surprises.
|
|
2528
|
-
const needsParens = argument.kind === 'binary' || argument.kind === 'logical'
|
|
2529
|
-
return needsParens ? `!(${arg})` : `!${arg}`
|
|
2530
|
-
}
|
|
2531
|
-
if (op === '-') return `-${arg}`
|
|
2532
|
-
return arg
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2536
|
-
const l = emit(left)
|
|
2537
|
-
const r = emit(right)
|
|
2538
|
-
// String equality: `eq`/`ne` when EITHER operand is string-typed — a string
|
|
2539
|
-
// literal, a string signal getter, or a string prop. Numeric `==`/`!=`
|
|
2540
|
-
// would coerce both sides to 0 and match unrelated non-numeric strings (#1672).
|
|
2541
|
-
const isStr = (e: ParsedExpr) => isStringTypedOperand(e, this.isStringName)
|
|
2542
|
-
const stringCmp = isStr(left) || isStr(right)
|
|
2543
|
-
if ((op === '===' || op === '==') && stringCmp) {
|
|
2544
|
-
return `${l} eq ${r}`
|
|
2545
|
-
}
|
|
2546
|
-
if ((op === '!==' || op === '!=') && stringCmp) {
|
|
2547
|
-
return `${l} ne ${r}`
|
|
2548
|
-
}
|
|
2549
|
-
const opMap: Record<string, string> = {
|
|
2550
|
-
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
2551
|
-
'+': '+', '-': '-', '*': '*', '/': '/',
|
|
2552
|
-
}
|
|
2553
|
-
return `${l} ${opMap[op] ?? op} ${r}`
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2557
|
-
const l = emit(left)
|
|
2558
|
-
const r = emit(right)
|
|
2559
|
-
if (op === '&&') return `(${l} && ${r})`
|
|
2560
|
-
if (op === '||') return `(${l} || ${r})`
|
|
2561
|
-
return `(${l} // ${r})`
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
higherOrder(
|
|
2565
|
-
method: HigherOrderMethod,
|
|
2566
|
-
object: ParsedExpr,
|
|
2567
|
-
param: string,
|
|
2568
|
-
predicate: ParsedExpr,
|
|
2569
|
-
emit: (e: ParsedExpr) => string,
|
|
2570
|
-
): string {
|
|
2571
|
-
// The predicate body is also a filter context, but with this
|
|
2572
|
-
// higher-order's own `param` (potentially shadowing the outer one),
|
|
2573
|
-
// so we spin up a nested emitter with the inner param.
|
|
2574
|
-
const arrayExpr = emit(object)
|
|
2575
|
-
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName))
|
|
2576
|
-
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, 'g'), '$_')
|
|
2577
|
-
if (method === 'filter') return `[grep { ${grepBody} } @{${arrayExpr}}]`
|
|
2578
|
-
if (method === 'every') return `!(grep { !(${grepBody}) } @{${arrayExpr}})`
|
|
2579
|
-
if (method === 'some') return `!!(grep { ${grepBody} } @{${arrayExpr}})`
|
|
2580
|
-
return arrayExpr
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2584
|
-
// Perl array ref: `[$a, $b]`. Filter-context use is rare (the
|
|
2585
|
-
// outer emitter routes most array-literal arrivals via
|
|
2586
|
-
// MojoTopLevelEmitter), but #1443's chain
|
|
2587
|
-
// `[a, b].filter(Boolean).join(' ')` can land here when the
|
|
2588
|
-
// outer `.filter()` recurses into a nested filter whose own
|
|
2589
|
-
// source is an array literal.
|
|
2590
|
-
return `[${elements.map(emit).join(', ')}]`
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
arrayMethod(
|
|
2594
|
-
method: ArrayMethod,
|
|
2595
|
-
object: ParsedExpr,
|
|
2596
|
-
args: ParsedExpr[],
|
|
2597
|
-
emit: (e: ParsedExpr) => string,
|
|
2598
|
-
): string {
|
|
2599
|
-
// Filter-context array methods are vanishingly rare — predicates
|
|
2600
|
-
// operate on scalars, not arrays. Defer to the top-level rendering
|
|
2601
|
-
// (`join(sep, @{...})`) for any case that does land here so the
|
|
2602
|
-
// emission stays consistent across contexts.
|
|
2603
|
-
return renderArrayMethod(method, object, args, emit)
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
sortMethod(
|
|
2607
|
-
_method: 'sort' | 'toSorted',
|
|
2608
|
-
object: ParsedExpr,
|
|
2609
|
-
comparator: SortComparator,
|
|
2610
|
-
emit: (e: ParsedExpr) => string,
|
|
2611
|
-
): string {
|
|
2612
|
-
return renderSortMethod(emit(object), comparator)
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
|
|
2616
|
-
return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
2620
|
-
return renderFlatMethod(emit(object), depth)
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
|
|
2624
|
-
return renderFlatMapMethod(emit(object), op)
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
|
|
2628
|
-
return '1'
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
templateLiteral(_parts: TemplatePart[]): string {
|
|
2632
|
-
return '1'
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
arrowFn(_param: string, _body: ParsedExpr): string {
|
|
2636
|
-
return '1'
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
unsupported(_raw: string, _reason: string): string {
|
|
2640
|
-
return '1'
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
/**
|
|
2645
|
-
* Lowering for top-level expressions whose identifiers resolve against
|
|
2646
|
-
* the Mojo template's stash (signals, props, locals introduced by
|
|
2647
|
-
* `% my $x = ...;` lines). Differs from the filter emitter mainly in
|
|
2648
|
-
* - `.length` → `scalar(@{...})` (filter contexts never see arrays
|
|
2649
|
-
* in lvalue position),
|
|
2650
|
-
* - `conditional` is supported (filter predicates can't return
|
|
2651
|
-
* ternaries),
|
|
2652
|
-
* - the `unsupported` fallback drops to the regex pipeline so legacy
|
|
2653
|
-
* shapes the AST can't classify still emit something coherent.
|
|
2654
|
-
*/
|
|
2655
|
-
class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
2656
|
-
constructor(private readonly adapter: MojoAdapter) {}
|
|
2657
|
-
|
|
2658
|
-
identifier(name: string): string {
|
|
2659
|
-
// `undefined` / `null` nested inside a larger expression tree
|
|
2660
|
-
// (#1897, pagination's `props.isActive ? 'page' : undefined`) — the
|
|
2661
|
-
// top-level short-circuits don't see them.
|
|
2662
|
-
if (name === 'undefined' || name === 'null') return 'undef'
|
|
2663
|
-
// Module pure-string const (e.g. `const baseClasses = '...'` used in a
|
|
2664
|
-
// className template literal): inline the literal value rather than emit
|
|
2665
|
-
// `$baseClasses` against a stash variable that is never bound.
|
|
2666
|
-
const inlined = this.adapter.resolveModuleStringConst(name)
|
|
2667
|
-
if (inlined !== null) return inlined
|
|
2668
|
-
// Same for a literal const of any scope (`const totalPages = 5`,
|
|
2669
|
-
// #1897 pagination's `Page {currentPage()} of {totalPages}`).
|
|
2670
|
-
const literalConst = this.adapter.resolveLiteralConst(name)
|
|
2671
|
-
if (literalConst !== null) return literalConst
|
|
2672
|
-
return `$${name}`
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
2676
|
-
if (literalType === 'string') return `'${value}'`
|
|
2677
|
-
if (literalType === 'boolean') return value ? '1' : '0'
|
|
2678
|
-
if (literalType === 'null') return 'undef'
|
|
2679
|
-
return String(value)
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
2683
|
-
// `props.x` flattens to the bare `$x` the Mojo SSR caller binds each
|
|
2684
|
-
// prop to (props arrive as individual `my $x = ...` vars, not a
|
|
2685
|
-
// `$props` hashref).
|
|
2686
|
-
if (object.kind === 'identifier' && object.name === 'props') {
|
|
2687
|
-
return `$${property}`
|
|
2688
|
-
}
|
|
2689
|
-
// Static property access on a module object-literal const
|
|
2690
|
-
// (`variantClasses.ghost`, #1897) resolves at compile time — the
|
|
2691
|
-
// generic hash lowering below would dereference a Perl var that
|
|
2692
|
-
// doesn't exist server-side.
|
|
2693
|
-
if (object.kind === 'identifier') {
|
|
2694
|
-
const staticValue = this.adapter.resolveStaticRecordLiteral(object.name, property)
|
|
2695
|
-
if (staticValue !== null) return staticValue
|
|
2696
|
-
}
|
|
2697
|
-
const obj = emit(object)
|
|
2698
|
-
if (property === 'length') return `scalar(@{${obj}})`
|
|
2699
|
-
return `${obj}->{${property}}`
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2703
|
-
return emitIndexAccessPerl(object, index, emit, n => this.adapter._isStringValueName(n))
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2707
|
-
// Signal getter: count() → $count
|
|
2708
|
-
if (callee.kind === 'identifier' && args.length === 0) {
|
|
2709
|
-
return `$${callee.name}`
|
|
2710
|
-
}
|
|
2711
|
-
// Env-signal method call (#1922): `searchParams().get('sort')` is a real
|
|
2712
|
-
// method call on the per-request `$searchParams` reader object, not the
|
|
2713
|
-
// generic hash deref `member` would emit (`$searchParams->{get}`, which
|
|
2714
|
-
// drops the arg). Matches the local import binding (incl. an alias).
|
|
2715
|
-
if (this.adapter._searchParamsLocals.size > 0) {
|
|
2716
|
-
const sp = matchSearchParamsMethodCall(callee, args, this.adapter._searchParamsLocals)
|
|
2717
|
-
if (sp) {
|
|
2718
|
-
return `$searchParams->${sp.method}(${sp.args.map(emit).join(', ')})`
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2721
|
-
// Identifier-path templatePrimitive (#1189): `JSON.stringify(x)` /
|
|
2722
|
-
// `Math.floor(x)` → `bf->json($x)` / `bf->floor($x)`. Args render
|
|
2723
|
-
// recursively through this same emitter so prop refs / signal calls
|
|
2724
|
-
// inside them get the standard transforms. Mirrors the Go adapter's
|
|
2725
|
-
// `call()` primitive dispatch. A wrong-arity call records BF101 and
|
|
2726
|
-
// returns the safe `''` placeholder (never silently emits a bad call).
|
|
2727
|
-
const path = identifierPath(callee)
|
|
2728
|
-
const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined
|
|
2729
|
-
if (path && spec) {
|
|
2730
|
-
if (args.length === spec.arity) {
|
|
2731
|
-
return spec.emit(args.map(emit))
|
|
2732
|
-
}
|
|
2733
|
-
this.adapter._recordExprBF101(
|
|
2734
|
-
`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`,
|
|
2735
|
-
`Call '${path}' with exactly ${spec.arity} argument(s).`,
|
|
2736
|
-
)
|
|
2737
|
-
// Don't fall through to the generic `emit(callee)` below — for a
|
|
2738
|
-
// member callee (`JSON.stringify`) that emits an invalid Perl
|
|
2739
|
-
// hash-deref (`$JSON->{stringify}`). Return the same safe
|
|
2740
|
-
// empty-string placeholder the other BF101 paths use.
|
|
2741
|
-
return "''"
|
|
2742
|
-
}
|
|
2743
|
-
// Array methods (`.join` and any others added to ArrayMethod, #1443)
|
|
2744
|
-
// are lifted into the `array-method` IR kind at parse time, so they
|
|
2745
|
-
// never reach this dispatcher. Per-method detection here would mix
|
|
2746
|
-
// value-builtin lowering with signal-call lowering — keeping them
|
|
2747
|
-
// separated forces every adapter to declare the full array-method
|
|
2748
|
-
// surface in one place (the `arrayMethod` emitter below).
|
|
2749
|
-
return emit(callee)
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2753
|
-
const arg = emit(argument)
|
|
2754
|
-
if (op === '!') return `!${arg}`
|
|
2755
|
-
if (op === '-') return `-${arg}`
|
|
2756
|
-
return arg
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2760
|
-
const l = emit(left)
|
|
2761
|
-
const r = emit(right)
|
|
2762
|
-
// String equality: `eq`/`ne` when EITHER operand is string-typed — a string
|
|
2763
|
-
// literal (`role() === 'admin'`), a string signal getter (`sel()`), or a
|
|
2764
|
-
// string prop (`props.x`). Falling back to numeric `==`/`!=` would make
|
|
2765
|
-
// Perl coerce both sides to 0 and match unrelated non-numeric strings
|
|
2766
|
-
// (`"b" == "a"` → true), so all loop items render their true branch (#1672).
|
|
2767
|
-
const isStr = (e: ParsedExpr) => isStringTypedOperand(e, n => this.adapter._isStringValueName(n))
|
|
2768
|
-
const stringCmp = isStr(left) || isStr(right)
|
|
2769
|
-
if ((op === '===' || op === '==') && stringCmp) {
|
|
2770
|
-
return `${l} eq ${r}`
|
|
2771
|
-
}
|
|
2772
|
-
if ((op === '!==' || op === '!=') && stringCmp) {
|
|
2773
|
-
return `${l} ne ${r}`
|
|
2774
|
-
}
|
|
2775
|
-
const opMap: Record<string, string> = {
|
|
2776
|
-
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
2777
|
-
'+': '+', '-': '-', '*': '*',
|
|
2778
|
-
}
|
|
2779
|
-
return `${l} ${opMap[op] ?? op} ${r}`
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2783
|
-
const l = emit(left)
|
|
2784
|
-
const r = emit(right)
|
|
2785
|
-
if (op === '&&') return `(${l} && ${r})`
|
|
2786
|
-
if (op === '||') return `(${l} || ${r})`
|
|
2787
|
-
return `(${l} // ${r})`
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
higherOrder(
|
|
2791
|
-
method: HigherOrderMethod,
|
|
2792
|
-
object: ParsedExpr,
|
|
2793
|
-
param: string,
|
|
2794
|
-
predicate: ParsedExpr,
|
|
2795
|
-
emit: (e: ParsedExpr) => string,
|
|
2796
|
-
): string {
|
|
2797
|
-
const arrayExpr = emit(object)
|
|
2798
|
-
const predBody = this.adapter._renderPerlFilterExprPublic(predicate, param)
|
|
2799
|
-
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, 'g'), '$_')
|
|
2800
|
-
if (method === 'filter') return `[grep { ${grepBody} } @{${arrayExpr}}]`
|
|
2801
|
-
if (method === 'every') return `!(grep { !(${grepBody}) } @{${arrayExpr}})`
|
|
2802
|
-
if (method === 'some') return `!!(grep { ${grepBody} } @{${arrayExpr}})`
|
|
2803
|
-
// `.find` / `.findIndex` / `.findLast` / `.findLastIndex` → the runtime
|
|
2804
|
-
// helpers (`bf->find` / `find_index` / `find_last` / `find_last_index`),
|
|
2805
|
-
// which call the predicate as a per-element coderef — same shape Xslate
|
|
2806
|
-
// emits via a Kolon lambda. The JS camelCase names map to the snake_case
|
|
2807
|
-
// helpers (like index_of / last_index_of).
|
|
2808
|
-
const findHelper: Record<string, string> = {
|
|
2809
|
-
find: 'find',
|
|
2810
|
-
findIndex: 'find_index',
|
|
2811
|
-
findLast: 'find_last',
|
|
2812
|
-
findLastIndex: 'find_last_index',
|
|
2813
|
-
}
|
|
2814
|
-
if (findHelper[method]) {
|
|
2815
|
-
return `bf->${findHelper[method]}(${arrayExpr}, sub { my $${param} = $_[0]; ${predBody} })`
|
|
2816
|
-
}
|
|
2817
|
-
return arrayExpr
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2821
|
-
// Perl array ref. Identifiers inside elements resolve through the
|
|
2822
|
-
// top-level emitter so `[className, childClass]` becomes
|
|
2823
|
-
// `[$className, $childClass]` (the registry Slot's chain in
|
|
2824
|
-
// #1443). Empty `[]` stays as `[]` — a valid empty Perl array
|
|
2825
|
-
// ref that grep/join handle naturally.
|
|
2826
|
-
return `[${elements.map(emit).join(', ')}]`
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
arrayMethod(
|
|
2830
|
-
method: ArrayMethod,
|
|
2831
|
-
object: ParsedExpr,
|
|
2832
|
-
args: ParsedExpr[],
|
|
2833
|
-
emit: (e: ParsedExpr) => string,
|
|
2834
|
-
): string {
|
|
2835
|
-
return renderArrayMethod(method, object, args, emit)
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
sortMethod(
|
|
2839
|
-
_method: 'sort' | 'toSorted',
|
|
2840
|
-
object: ParsedExpr,
|
|
2841
|
-
comparator: SortComparator,
|
|
2842
|
-
emit: (e: ParsedExpr) => string,
|
|
2843
|
-
): string {
|
|
2844
|
-
return renderSortMethod(emit(object), comparator)
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
|
|
2848
|
-
return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
2852
|
-
return renderFlatMethod(emit(object), depth)
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
|
|
2856
|
-
return renderFlatMapMethod(emit(object), op)
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
conditional(
|
|
2860
|
-
test: ParsedExpr,
|
|
2861
|
-
consequent: ParsedExpr,
|
|
2862
|
-
alternate: ParsedExpr,
|
|
2863
|
-
emit: (e: ParsedExpr) => string,
|
|
2864
|
-
): string {
|
|
2865
|
-
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string {
|
|
2869
|
-
// `` `n=${count() + 1}` `` → Perl string concatenation
|
|
2870
|
-
// (`"n=" . ($count + 1)`), NOT double-quote interpolation. Perl only
|
|
2871
|
-
// interpolates simple `$var` reads inside `"..."`, so complex `${...}`
|
|
2872
|
-
// parts — arithmetic, helper calls (`bf->json(...)`), ternaries —
|
|
2873
|
-
// would render unevaluated if inlined into a quoted string.
|
|
2874
|
-
// - Static chunks are emitted as quoted literals with the sigils
|
|
2875
|
-
// that interpolate inside `"..."` (`$`/`@`) plus `"`/`\` escaped,
|
|
2876
|
-
// so literal text survives verbatim.
|
|
2877
|
-
// - Expression terms whose Perl precedence is below `.` (binary /
|
|
2878
|
-
// logical / conditional) wrap in parens so they bind before the
|
|
2879
|
-
// concatenation.
|
|
2880
|
-
const terms: string[] = []
|
|
2881
|
-
for (const part of parts) {
|
|
2882
|
-
if (part.type === 'string') {
|
|
2883
|
-
if (part.value !== '') {
|
|
2884
|
-
terms.push(`"${part.value.replace(/[\\"$@]/g, m => `\\${m}`)}"`)
|
|
2885
|
-
}
|
|
2886
|
-
} else {
|
|
2887
|
-
const rendered = emit(part.expr)
|
|
2888
|
-
const needsParens =
|
|
2889
|
-
part.expr.kind === 'binary' ||
|
|
2890
|
-
part.expr.kind === 'logical' ||
|
|
2891
|
-
part.expr.kind === 'conditional'
|
|
2892
|
-
terms.push(needsParens ? `(${rendered})` : rendered)
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
if (terms.length === 0) return '""'
|
|
2896
|
-
return terms.join(' . ')
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
arrowFn(_param: string, _body: ParsedExpr): string {
|
|
2900
|
-
// A bare arrow function never stands alone at a render position (it's
|
|
2901
|
-
// only meaningful as a higher-order predicate, handled above). Return
|
|
2902
|
-
// the safe Perl empty-string literal `''` — consistent with the BF101
|
|
2903
|
-
// / `unsupported` paths — so a stray emit can't produce a `<%= %>`
|
|
2904
|
-
// syntax error.
|
|
2905
|
-
return "''"
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
unsupported(_raw: string, _reason: string): string {
|
|
2909
|
-
// Unreachable in the parse-first flow: `convertExpressionToPerl`
|
|
2910
|
-
// gates on `isSupported` before dispatching, and `isSupported`
|
|
2911
|
-
// recurses, so a top-level supported expression never contains an
|
|
2912
|
-
// `unsupported` node. Return a safe Perl empty-string literal in
|
|
2913
|
-
// case a future caller renders a node tree directly.
|
|
2914
|
-
return "''"
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
1681
|
export const mojoAdapter = new MojoAdapter()
|