@barefootjs/mojolicious 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/analysis/component-tree.d.ts +30 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +96 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +77 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +34 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.js +1490 -1336
- package/dist/adapter/lib/constants.d.ts +27 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +40 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/perl-naming.d.ts +29 -0
- package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +28 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +43 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/mojo-adapter.d.ts +46 -104
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +48 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +64 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +26 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.js +1490 -1336
- package/dist/index.js +1490 -1336
- 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 +124 -60
- package/src/__tests__/query-href.test.ts +94 -0
- package/src/adapter/analysis/component-tree.ts +128 -0
- package/src/adapter/emit-context.ts +107 -0
- package/src/adapter/expr/array-method.ts +408 -0
- package/src/adapter/expr/emitters.ts +607 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/lib/constants.ts +39 -0
- package/src/adapter/lib/ir-scope.ts +70 -0
- package/src/adapter/lib/perl-naming.ts +36 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +126 -0
- package/src/adapter/mojo-adapter.ts +223 -1476
- package/src/adapter/props/prop-classes.ts +87 -0
- package/src/adapter/spread/spread-codegen.ts +181 -0
- package/src/adapter/value/parsed-literal.ts +34 -0
- package/src/test-render.ts +2 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParsedExpr → Perl emitters for the Mojolicious EP template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
|
|
5
|
+
* track D). Two `ParsedExprEmitter` implementations:
|
|
6
|
+
*
|
|
7
|
+
* - `MojoFilterEmitter` — filter/predicate context (loop param + local
|
|
8
|
+
* aliases + bare `$name` signal fallback); self-contained, reads no
|
|
9
|
+
* adapter state.
|
|
10
|
+
* - `MojoTopLevelEmitter` — top-level / stash context; depends on the
|
|
11
|
+
* adapter only through the narrow `MojoEmitContext` seam.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type ParsedExprEmitter,
|
|
16
|
+
type HigherOrderMethod,
|
|
17
|
+
type ArrayMethod,
|
|
18
|
+
type LiteralType,
|
|
19
|
+
type ParsedExpr,
|
|
20
|
+
type ObjectLiteralProperty,
|
|
21
|
+
type FlatDepth,
|
|
22
|
+
type TemplatePart,
|
|
23
|
+
emitParsedExpr,
|
|
24
|
+
identifierPath,
|
|
25
|
+
matchSearchParamsMethodCall,
|
|
26
|
+
sortComparatorFromArrow,
|
|
27
|
+
asCallbackMethodCall,
|
|
28
|
+
} from '@barefootjs/jsx'
|
|
29
|
+
|
|
30
|
+
import type { MojoEmitContext } from '../emit-context.ts'
|
|
31
|
+
import { MOJO_TEMPLATE_PRIMITIVES } from '../lib/constants.ts'
|
|
32
|
+
import { emitIndexAccessPerl, isStringTypedOperand } from './operand.ts'
|
|
33
|
+
import {
|
|
34
|
+
renderArrayMethod,
|
|
35
|
+
renderSortMethod,
|
|
36
|
+
renderSortEval,
|
|
37
|
+
renderReduceEval,
|
|
38
|
+
renderPredicateEval,
|
|
39
|
+
renderFlatMethod,
|
|
40
|
+
renderFlatMapEval,
|
|
41
|
+
} from './array-method.ts'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Local shape for the predicate-method fallback chain (#2018 P5). The
|
|
45
|
+
* higher-order callback arrives as a generic `call`; `callbackMethod` recovers
|
|
46
|
+
* `{ method, object, param, predicate }` from it before threading it through
|
|
47
|
+
* the inline `grep` / `bf->find` lowering the old `higher-order` node fed.
|
|
48
|
+
*/
|
|
49
|
+
type PredicateCall = {
|
|
50
|
+
method: HigherOrderMethod
|
|
51
|
+
object: ParsedExpr
|
|
52
|
+
param: string
|
|
53
|
+
predicate: ParsedExpr
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lowering for the predicate body of a filter / every / some / find,
|
|
58
|
+
* plus the same shape used by `renderBlockBodyCondition` for complex
|
|
59
|
+
* block-body filters. Identifiers resolve against:
|
|
60
|
+
* - the predicate's loop param (`$param`),
|
|
61
|
+
* - `localVarMap` aliases declared inside the block body, then
|
|
62
|
+
* - a bare `$name` fallback for signals captured by the closure.
|
|
63
|
+
*
|
|
64
|
+
* Methods that have no filter-context meaning (template-literal,
|
|
65
|
+
* arrow-fn, conditional, unsupported) fall back to the `'1'` literal
|
|
66
|
+
* the original switch's `default` arm returned — those shapes never
|
|
67
|
+
* arose inside the predicates the adapter actually accepts.
|
|
68
|
+
*/
|
|
69
|
+
// Predicate-shaped higher-order methods (filter/every/some land here through
|
|
70
|
+
// nested `.filter(...)` chains). Module-level so `callbackMethod`, which runs
|
|
71
|
+
// for many nodes during template generation, reuses one set per process.
|
|
72
|
+
const PREDICATE_METHODS: ReadonlySet<string> = new Set([
|
|
73
|
+
'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'every', 'some',
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
77
|
+
constructor(
|
|
78
|
+
private readonly param: string,
|
|
79
|
+
private readonly localVarMap: Map<string, string>,
|
|
80
|
+
// Reports whether a getter/prop name is string-typed, so `===`/`!==`
|
|
81
|
+
// against it lowers to `eq`/`ne` (#1672). Defaults to "never" for callers
|
|
82
|
+
// that don't thread it through.
|
|
83
|
+
private readonly isStringName: (n: string) => boolean = () => false,
|
|
84
|
+
) {}
|
|
85
|
+
|
|
86
|
+
identifier(name: string): string {
|
|
87
|
+
if (name === this.param) return `$${this.param}`
|
|
88
|
+
const signal = this.localVarMap.get(name)
|
|
89
|
+
if (signal) return `$${signal}`
|
|
90
|
+
return `$${name}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
94
|
+
if (literalType === 'string') return `'${value}'`
|
|
95
|
+
if (literalType === 'boolean') return value ? '1' : '0'
|
|
96
|
+
if (literalType === 'null') return 'undef'
|
|
97
|
+
return String(value)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
101
|
+
// `.length` on a higher-order result (e.g.
|
|
102
|
+
// `x.tags.filter(t => t.active).length > 0` inside the outer
|
|
103
|
+
// filter predicate, #1443). The higher-order emit produces an
|
|
104
|
+
// anonymous array ref `[grep ...]`; reading `->{length}` on that
|
|
105
|
+
// is undef at runtime, which is why the pre-#1443 `containsHigherOrder`
|
|
106
|
+
// gate refused this shape outright. Lowering `.length` to
|
|
107
|
+
// `scalar(@{...})` makes the result a real Perl integer.
|
|
108
|
+
if (property === 'length' && (asCallbackMethodCall(object) !== null || object.kind === 'array-literal')) {
|
|
109
|
+
return `scalar(@{${emit(object)}})`
|
|
110
|
+
}
|
|
111
|
+
return `${emit(object)}->{${property}}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
115
|
+
return emitIndexAccessPerl(object, index, emit, this.isStringName)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
119
|
+
// Signal getter calls: filter() → $filter
|
|
120
|
+
if (callee.kind === 'identifier' && args.length === 0) {
|
|
121
|
+
return `$${callee.name}`
|
|
122
|
+
}
|
|
123
|
+
return emit(callee)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
127
|
+
const arg = emit(argument)
|
|
128
|
+
if (op === '!') {
|
|
129
|
+
// Wrap binary/logical operands in parens to dodge Perl precedence surprises.
|
|
130
|
+
const needsParens = argument.kind === 'binary' || argument.kind === 'logical'
|
|
131
|
+
return needsParens ? `!(${arg})` : `!${arg}`
|
|
132
|
+
}
|
|
133
|
+
if (op === '-') return `-${arg}`
|
|
134
|
+
return arg
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
138
|
+
const l = emit(left)
|
|
139
|
+
const r = emit(right)
|
|
140
|
+
// String equality: `eq`/`ne` when EITHER operand is string-typed — a string
|
|
141
|
+
// literal, a string signal getter, or a string prop. Numeric `==`/`!=`
|
|
142
|
+
// would coerce both sides to 0 and match unrelated non-numeric strings (#1672).
|
|
143
|
+
const isStr = (e: ParsedExpr) => isStringTypedOperand(e, this.isStringName)
|
|
144
|
+
const stringCmp = isStr(left) || isStr(right)
|
|
145
|
+
if ((op === '===' || op === '==') && stringCmp) {
|
|
146
|
+
return `${l} eq ${r}`
|
|
147
|
+
}
|
|
148
|
+
if ((op === '!==' || op === '!=') && stringCmp) {
|
|
149
|
+
return `${l} ne ${r}`
|
|
150
|
+
}
|
|
151
|
+
const opMap: Record<string, string> = {
|
|
152
|
+
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
153
|
+
'+': '+', '-': '-', '*': '*', '/': '/',
|
|
154
|
+
}
|
|
155
|
+
return `${l} ${opMap[op] ?? op} ${r}`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
159
|
+
const l = emit(left)
|
|
160
|
+
const r = emit(right)
|
|
161
|
+
if (op === '&&') return `(${l} && ${r})`
|
|
162
|
+
if (op === '||') return `(${l} || ${r})`
|
|
163
|
+
return `(${l} // ${r})`
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
callbackMethod(
|
|
167
|
+
method: string,
|
|
168
|
+
object: ParsedExpr,
|
|
169
|
+
arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
|
|
170
|
+
_restArgs: ParsedExpr[],
|
|
171
|
+
emit: (e: ParsedExpr) => string,
|
|
172
|
+
): string {
|
|
173
|
+
// Filter context only meaningfully handles the predicate methods
|
|
174
|
+
// (filter / every / some land here through nested `.filter(...)` chains).
|
|
175
|
+
// Sort / reduce / flatMap never arise inside a predicate, so route them to
|
|
176
|
+
// the truthy sentinel like the old `default` arm did.
|
|
177
|
+
if (!PREDICATE_METHODS.has(method)) return '1'
|
|
178
|
+
// The predicate body is also a filter context, but with this
|
|
179
|
+
// callback's own `param` (potentially shadowing the outer one),
|
|
180
|
+
// so we spin up a nested emitter with the inner param.
|
|
181
|
+
const param = arrow.params[0]
|
|
182
|
+
const predicate = arrow.body
|
|
183
|
+
const arrayExpr = emit(object)
|
|
184
|
+
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName))
|
|
185
|
+
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, 'g'), '$_')
|
|
186
|
+
if (method === 'filter') return `[grep { ${grepBody} } @{${arrayExpr}}]`
|
|
187
|
+
if (method === 'every') return `!(grep { !(${grepBody}) } @{${arrayExpr}})`
|
|
188
|
+
if (method === 'some') return `!!(grep { ${grepBody} } @{${arrayExpr}})`
|
|
189
|
+
return arrayExpr
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
193
|
+
// Perl array ref: `[$a, $b]`. Filter-context use is rare (the
|
|
194
|
+
// outer emitter routes most array-literal arrivals via
|
|
195
|
+
// MojoTopLevelEmitter), but #1443's chain
|
|
196
|
+
// `[a, b].filter(Boolean).join(' ')` can land here when the
|
|
197
|
+
// outer `.filter()` recurses into a nested filter whose own
|
|
198
|
+
// source is an array literal.
|
|
199
|
+
return `[${elements.map(emit).join(', ')}]`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
arrayMethod(
|
|
203
|
+
method: ArrayMethod,
|
|
204
|
+
object: ParsedExpr,
|
|
205
|
+
args: ParsedExpr[],
|
|
206
|
+
emit: (e: ParsedExpr) => string,
|
|
207
|
+
): string {
|
|
208
|
+
// Filter-context array methods are vanishingly rare — predicates
|
|
209
|
+
// operate on scalars, not arrays. Defer to the top-level rendering
|
|
210
|
+
// (`join(sep, @{...})`) for any case that does land here so the
|
|
211
|
+
// emission stays consistent across contexts.
|
|
212
|
+
return renderArrayMethod(method, object, args, emit)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
216
|
+
return renderFlatMethod(emit(object), depth)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
|
|
220
|
+
return '1'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
templateLiteral(_parts: TemplatePart[]): string {
|
|
224
|
+
return '1'
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
arrow(_params: string[], _body: ParsedExpr, _emit: (e: ParsedExpr) => string): string {
|
|
228
|
+
// A standalone arrow only reaches here outside a callback position, which
|
|
229
|
+
// never arises in a predicate context — emit the truthy sentinel like the
|
|
230
|
+
// pre-#2018 `arrowFn` fallback.
|
|
231
|
+
return '1'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
regex(_raw: string): string {
|
|
235
|
+
// A standalone regex has no filter-predicate meaning — truthy sentinel.
|
|
236
|
+
return '1'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
unsupported(_raw: string, _reason: string): string {
|
|
240
|
+
return '1'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
244
|
+
// Filter-predicate context: an object literal is not a boolean leaf, so
|
|
245
|
+
// emit the truthy sentinel exactly as `unsupported` does (byte-identical
|
|
246
|
+
// with the pre-`object-literal` fallback; Roadmap A-1). Object *values*
|
|
247
|
+
// are lowered to Perl hashrefs in the conditional/attr paths, not here.
|
|
248
|
+
return '1'
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Lowering for top-level expressions whose identifiers resolve against
|
|
254
|
+
* the Mojo template's stash (signals, props, locals introduced by
|
|
255
|
+
* `% my $x = ...;` lines). Differs from the filter emitter mainly in
|
|
256
|
+
* - `.length` → `scalar(@{...})` (filter contexts never see arrays
|
|
257
|
+
* in lvalue position),
|
|
258
|
+
* - `conditional` is supported (filter predicates can't return
|
|
259
|
+
* ternaries),
|
|
260
|
+
* - the `unsupported` fallback drops to the regex pipeline so legacy
|
|
261
|
+
* shapes the AST can't classify still emit something coherent.
|
|
262
|
+
*/
|
|
263
|
+
export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
264
|
+
constructor(private readonly ctx: MojoEmitContext) {}
|
|
265
|
+
|
|
266
|
+
identifier(name: string): string {
|
|
267
|
+
// `undefined` / `null` nested inside a larger expression tree
|
|
268
|
+
// (#1897, pagination's `props.isActive ? 'page' : undefined`) — the
|
|
269
|
+
// top-level short-circuits don't see them.
|
|
270
|
+
if (name === 'undefined' || name === 'null') return 'undef'
|
|
271
|
+
// Module pure-string const (e.g. `const baseClasses = '...'` used in a
|
|
272
|
+
// className template literal): inline the literal value rather than emit
|
|
273
|
+
// `$baseClasses` against a stash variable that is never bound.
|
|
274
|
+
const inlined = this.ctx.resolveModuleStringConst(name)
|
|
275
|
+
if (inlined !== null) return inlined
|
|
276
|
+
// Same for a literal const of any scope (`const totalPages = 5`,
|
|
277
|
+
// #1897 pagination's `Page {currentPage()} of {totalPages}`).
|
|
278
|
+
const literalConst = this.ctx.resolveLiteralConst(name)
|
|
279
|
+
if (literalConst !== null) return literalConst
|
|
280
|
+
return `$${name}`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
284
|
+
if (literalType === 'string') return `'${value}'`
|
|
285
|
+
if (literalType === 'boolean') return value ? '1' : '0'
|
|
286
|
+
if (literalType === 'null') return 'undef'
|
|
287
|
+
return String(value)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
291
|
+
// `props.x` flattens to the bare `$x` the Mojo SSR caller binds each
|
|
292
|
+
// prop to (props arrive as individual `my $x = ...` vars, not a
|
|
293
|
+
// `$props` hashref).
|
|
294
|
+
if (object.kind === 'identifier' && object.name === 'props') {
|
|
295
|
+
return `$${property}`
|
|
296
|
+
}
|
|
297
|
+
// Static property access on a module object-literal const
|
|
298
|
+
// (`variantClasses.ghost`, #1897) resolves at compile time — the
|
|
299
|
+
// generic hash lowering below would dereference a Perl var that
|
|
300
|
+
// doesn't exist server-side.
|
|
301
|
+
if (object.kind === 'identifier') {
|
|
302
|
+
const staticValue = this.ctx.resolveStaticRecordLiteral(object.name, property)
|
|
303
|
+
if (staticValue !== null) return staticValue
|
|
304
|
+
}
|
|
305
|
+
const obj = emit(object)
|
|
306
|
+
if (property === 'length') return `scalar(@{${obj}})`
|
|
307
|
+
return `${obj}->{${property}}`
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
311
|
+
return emitIndexAccessPerl(object, index, emit, n => this.ctx._isStringValueName(n))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
315
|
+
// Signal getter: count() → $count
|
|
316
|
+
if (callee.kind === 'identifier' && args.length === 0) {
|
|
317
|
+
return `$${callee.name}`
|
|
318
|
+
}
|
|
319
|
+
// Env-signal method call (#1922): `searchParams().get('sort')` is a real
|
|
320
|
+
// method call on the per-request `$searchParams` reader object, not the
|
|
321
|
+
// generic hash deref `member` would emit (`$searchParams->{get}`, which
|
|
322
|
+
// drops the arg). Matches the local import binding (incl. an alias).
|
|
323
|
+
if (this.ctx._searchParamsLocals.size > 0) {
|
|
324
|
+
const sp = matchSearchParamsMethodCall(callee, args, this.ctx._searchParamsLocals)
|
|
325
|
+
if (sp) {
|
|
326
|
+
return `$searchParams->${sp.method}(${sp.args.map(emit).join(', ')})`
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Identifier-path templatePrimitive (#1189): `JSON.stringify(x)` /
|
|
330
|
+
// `Math.floor(x)` → `bf->json($x)` / `bf->floor($x)`. Args render
|
|
331
|
+
// recursively through this same emitter so prop refs / signal calls
|
|
332
|
+
// inside them get the standard transforms. Mirrors the Go adapter's
|
|
333
|
+
// `call()` primitive dispatch. A wrong-arity call records BF101 and
|
|
334
|
+
// returns the safe `''` placeholder (never silently emits a bad call).
|
|
335
|
+
const path = identifierPath(callee)
|
|
336
|
+
const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined
|
|
337
|
+
if (path && spec) {
|
|
338
|
+
if (args.length === spec.arity) {
|
|
339
|
+
return spec.emit(args.map(emit))
|
|
340
|
+
}
|
|
341
|
+
this.ctx._recordExprBF101(
|
|
342
|
+
`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`,
|
|
343
|
+
`Call '${path}' with exactly ${spec.arity} argument(s).`,
|
|
344
|
+
)
|
|
345
|
+
// Don't fall through to the generic `emit(callee)` below — for a
|
|
346
|
+
// member callee (`JSON.stringify`) that emits an invalid Perl
|
|
347
|
+
// hash-deref (`$JSON->{stringify}`). Return the same safe
|
|
348
|
+
// empty-string placeholder the other BF101 paths use.
|
|
349
|
+
return "''"
|
|
350
|
+
}
|
|
351
|
+
// Array methods (`.join` and any others added to ArrayMethod, #1443)
|
|
352
|
+
// are lifted into the `array-method` IR kind at parse time, so they
|
|
353
|
+
// never reach this dispatcher. Per-method detection here would mix
|
|
354
|
+
// value-builtin lowering with signal-call lowering — keeping them
|
|
355
|
+
// separated forces every adapter to declare the full array-method
|
|
356
|
+
// surface in one place (the `arrayMethod` emitter below).
|
|
357
|
+
return emit(callee)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
361
|
+
const arg = emit(argument)
|
|
362
|
+
if (op === '!') return `!${arg}`
|
|
363
|
+
if (op === '-') return `-${arg}`
|
|
364
|
+
return arg
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
368
|
+
const l = emit(left)
|
|
369
|
+
const r = emit(right)
|
|
370
|
+
// String equality: `eq`/`ne` when EITHER operand is string-typed — a string
|
|
371
|
+
// literal (`role() === 'admin'`), a string signal getter (`sel()`), or a
|
|
372
|
+
// string prop (`props.x`). Falling back to numeric `==`/`!=` would make
|
|
373
|
+
// Perl coerce both sides to 0 and match unrelated non-numeric strings
|
|
374
|
+
// (`"b" == "a"` → true), so all loop items render their true branch (#1672).
|
|
375
|
+
const isStr = (e: ParsedExpr) => isStringTypedOperand(e, n => this.ctx._isStringValueName(n))
|
|
376
|
+
const stringCmp = isStr(left) || isStr(right)
|
|
377
|
+
if ((op === '===' || op === '==') && stringCmp) {
|
|
378
|
+
return `${l} eq ${r}`
|
|
379
|
+
}
|
|
380
|
+
if ((op === '!==' || op === '!=') && stringCmp) {
|
|
381
|
+
return `${l} ne ${r}`
|
|
382
|
+
}
|
|
383
|
+
const opMap: Record<string, string> = {
|
|
384
|
+
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
385
|
+
'+': '+', '-': '-', '*': '*',
|
|
386
|
+
}
|
|
387
|
+
return `${l} ${opMap[op] ?? op} ${r}`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
391
|
+
const l = emit(left)
|
|
392
|
+
const r = emit(right)
|
|
393
|
+
if (op === '&&') return `(${l} && ${r})`
|
|
394
|
+
if (op === '||') return `(${l} || ${r})`
|
|
395
|
+
return `(${l} // ${r})`
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
callbackMethod(
|
|
399
|
+
method: string,
|
|
400
|
+
object: ParsedExpr,
|
|
401
|
+
arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
|
|
402
|
+
restArgs: ParsedExpr[],
|
|
403
|
+
emit: (e: ParsedExpr) => string,
|
|
404
|
+
): string {
|
|
405
|
+
const recv = emit(object)
|
|
406
|
+
const body = arrow.body
|
|
407
|
+
const params = arrow.params
|
|
408
|
+
|
|
409
|
+
// sort / toSorted: eval-first, then the structured `bf->sort` fallback
|
|
410
|
+
// (recovered from the arrow by `sortComparatorFromArrow`, e.g. a
|
|
411
|
+
// `localeCompare` comparator), then BF101.
|
|
412
|
+
if (method === 'sort' || method === 'toSorted') {
|
|
413
|
+
const evalForm = renderSortEval(recv, body, params, emit)
|
|
414
|
+
if (evalForm !== null) return evalForm
|
|
415
|
+
const c = sortComparatorFromArrow(arrow)
|
|
416
|
+
if (c !== null) return renderSortMethod(recv, c)
|
|
417
|
+
this.ctx._recordExprBF101(
|
|
418
|
+
`.${method}(...) comparator is not lowerable to a template sort`,
|
|
419
|
+
`Pre-sort the array in the route handler, or mark the loop @client-only.`,
|
|
420
|
+
)
|
|
421
|
+
return "''"
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// reduce / reduceRight: eval-only (the arithmetic catalogue always
|
|
425
|
+
// serializes); BF101 when the body is outside the evaluator surface or the
|
|
426
|
+
// seed isn't a literal.
|
|
427
|
+
if (method === 'reduce' || method === 'reduceRight') {
|
|
428
|
+
const direction = method === 'reduceRight' ? 'right' : 'left'
|
|
429
|
+
const init = restArgs[0]
|
|
430
|
+
const evalForm =
|
|
431
|
+
init !== undefined ? renderReduceEval(recv, body, params, init, direction, emit) : null
|
|
432
|
+
if (evalForm !== null) return evalForm
|
|
433
|
+
this.ctx._recordExprBF101(
|
|
434
|
+
`.${method}(...) is not lowerable to a template fold`,
|
|
435
|
+
`Pre-compute the fold in the route handler, or mark the loop @client-only.`,
|
|
436
|
+
)
|
|
437
|
+
return "''"
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// flatMap: eval-only; BF101 when the projection is outside the surface.
|
|
441
|
+
if (method === 'flatMap') {
|
|
442
|
+
const evalForm = renderFlatMapEval(recv, body, params[0], emit)
|
|
443
|
+
if (evalForm !== null) return evalForm
|
|
444
|
+
this.ctx._recordExprBF101(
|
|
445
|
+
`.flatMap(...) projection is not lowerable to a template flat-map`,
|
|
446
|
+
`Pre-compute the projection in the route handler, or mark the loop @client-only.`,
|
|
447
|
+
)
|
|
448
|
+
return "''"
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Predicate methods: filter / find / every / some / findIndex / findLast /
|
|
452
|
+
// findLastIndex. Eval-first (#2018 P2), then the inline `grep` / `bf->find`
|
|
453
|
+
// fallback for a predicate the evaluator can't model.
|
|
454
|
+
const cb: PredicateCall = {
|
|
455
|
+
method: method as HigherOrderMethod,
|
|
456
|
+
object,
|
|
457
|
+
param: params[0],
|
|
458
|
+
predicate: body,
|
|
459
|
+
}
|
|
460
|
+
return this.renderPredicate(cb, recv, emit)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private renderPredicate(
|
|
464
|
+
cb: PredicateCall,
|
|
465
|
+
arrayExpr: string,
|
|
466
|
+
emit: (e: ParsedExpr) => string,
|
|
467
|
+
): string {
|
|
468
|
+
const { method, param, predicate } = cb
|
|
469
|
+
// Evaluator path (#2018 P2): serialize the predicate body + emit the
|
|
470
|
+
// matching `bf->*_eval` helper (isomorphic with the Go adapter). Falls
|
|
471
|
+
// back to the inline `grep` / `bf->find` lowering below for a predicate
|
|
472
|
+
// the evaluator can't model (e.g. a method-call predicate).
|
|
473
|
+
const evalFn: Record<string, [string, boolean?]> = {
|
|
474
|
+
filter: ['filter_eval'], every: ['every_eval'], some: ['some_eval'],
|
|
475
|
+
find: ['find_eval', true], findLast: ['find_eval', false],
|
|
476
|
+
findIndex: ['find_index_eval', true], findLastIndex: ['find_index_eval', false],
|
|
477
|
+
}
|
|
478
|
+
// `.filter(Boolean)` (identity predicate `_t => _t`) keeps the inline
|
|
479
|
+
// `grep { $_ }` form — it composes through the array-method chain
|
|
480
|
+
// (`.filter(Boolean).join(' ')` in the registry Slot) and renders
|
|
481
|
+
// identically to a truthiness filter.
|
|
482
|
+
const isIdentity =
|
|
483
|
+
method === 'filter' && predicate.kind === 'identifier' && predicate.name === param
|
|
484
|
+
const spec = evalFn[method]
|
|
485
|
+
if (spec && !isIdentity) {
|
|
486
|
+
const evalForm = renderPredicateEval(spec[0], arrayExpr, predicate, param, emit, spec[1])
|
|
487
|
+
if (evalForm !== null) return evalForm
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const predBody = this.ctx._renderPerlFilterExprPublic(predicate, param)
|
|
491
|
+
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, 'g'), '$_')
|
|
492
|
+
if (method === 'filter') return `[grep { ${grepBody} } @{${arrayExpr}}]`
|
|
493
|
+
if (method === 'every') return `!(grep { !(${grepBody}) } @{${arrayExpr}})`
|
|
494
|
+
if (method === 'some') return `!!(grep { ${grepBody} } @{${arrayExpr}})`
|
|
495
|
+
// `.find` / `.findIndex` / `.findLast` / `.findLastIndex` → the runtime
|
|
496
|
+
// helpers (`bf->find` / `find_index` / `find_last` / `find_last_index`),
|
|
497
|
+
// which call the predicate as a per-element coderef — same shape Xslate
|
|
498
|
+
// emits via a Kolon lambda. The JS camelCase names map to the snake_case
|
|
499
|
+
// helpers (like index_of / last_index_of).
|
|
500
|
+
const findHelper: Record<string, string> = {
|
|
501
|
+
find: 'find',
|
|
502
|
+
findIndex: 'find_index',
|
|
503
|
+
findLast: 'find_last',
|
|
504
|
+
findLastIndex: 'find_last_index',
|
|
505
|
+
}
|
|
506
|
+
if (findHelper[method]) {
|
|
507
|
+
return `bf->${findHelper[method]}(${arrayExpr}, sub { my $${param} = $_[0]; ${predBody} })`
|
|
508
|
+
}
|
|
509
|
+
return arrayExpr
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
513
|
+
// Perl array ref. Identifiers inside elements resolve through the
|
|
514
|
+
// top-level emitter so `[className, childClass]` becomes
|
|
515
|
+
// `[$className, $childClass]` (the registry Slot's chain in
|
|
516
|
+
// #1443). Empty `[]` stays as `[]` — a valid empty Perl array
|
|
517
|
+
// ref that grep/join handle naturally.
|
|
518
|
+
return `[${elements.map(emit).join(', ')}]`
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
arrayMethod(
|
|
522
|
+
method: ArrayMethod,
|
|
523
|
+
object: ParsedExpr,
|
|
524
|
+
args: ParsedExpr[],
|
|
525
|
+
emit: (e: ParsedExpr) => string,
|
|
526
|
+
): string {
|
|
527
|
+
return renderArrayMethod(method, object, args, emit)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
531
|
+
return renderFlatMethod(emit(object), depth)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
conditional(
|
|
535
|
+
test: ParsedExpr,
|
|
536
|
+
consequent: ParsedExpr,
|
|
537
|
+
alternate: ParsedExpr,
|
|
538
|
+
emit: (e: ParsedExpr) => string,
|
|
539
|
+
): string {
|
|
540
|
+
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string {
|
|
544
|
+
// `` `n=${count() + 1}` `` → Perl string concatenation
|
|
545
|
+
// (`"n=" . ($count + 1)`), NOT double-quote interpolation. Perl only
|
|
546
|
+
// interpolates simple `$var` reads inside `"..."`, so complex `${...}`
|
|
547
|
+
// parts — arithmetic, helper calls (`bf->json(...)`), ternaries —
|
|
548
|
+
// would render unevaluated if inlined into a quoted string.
|
|
549
|
+
// - Static chunks are emitted as quoted literals with the sigils
|
|
550
|
+
// that interpolate inside `"..."` (`$`/`@`) plus `"`/`\` escaped,
|
|
551
|
+
// so literal text survives verbatim.
|
|
552
|
+
// - Expression terms whose Perl precedence is below `.` (binary /
|
|
553
|
+
// logical / conditional) wrap in parens so they bind before the
|
|
554
|
+
// concatenation.
|
|
555
|
+
const terms: string[] = []
|
|
556
|
+
for (const part of parts) {
|
|
557
|
+
if (part.type === 'string') {
|
|
558
|
+
if (part.value !== '') {
|
|
559
|
+
terms.push(`"${part.value.replace(/[\\"$@]/g, m => `\\${m}`)}"`)
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
const rendered = emit(part.expr)
|
|
563
|
+
const needsParens =
|
|
564
|
+
part.expr.kind === 'binary' ||
|
|
565
|
+
part.expr.kind === 'logical' ||
|
|
566
|
+
part.expr.kind === 'conditional'
|
|
567
|
+
terms.push(needsParens ? `(${rendered})` : rendered)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (terms.length === 0) return '""'
|
|
571
|
+
return terms.join(' . ')
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
arrow(_params: string[], _body: ParsedExpr, _emit: (e: ParsedExpr) => string): string {
|
|
575
|
+
// A bare arrow function never stands alone at a render position (it's
|
|
576
|
+
// only meaningful as a callback argument, handled by `callbackMethod`).
|
|
577
|
+
// Return the safe Perl empty-string literal `''` — consistent with the
|
|
578
|
+
// BF101 / `unsupported` paths — so a stray emit can't produce a `<%= %>`
|
|
579
|
+
// syntax error.
|
|
580
|
+
return "''"
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
regex(_raw: string): string {
|
|
584
|
+
// A standalone regex literal has no template render form (it only appears
|
|
585
|
+
// as a `.replace(/re/, …)` argument the parser refuses upstream). Mirror
|
|
586
|
+
// `arrow` / `unsupported` with the safe Perl empty-string literal.
|
|
587
|
+
return "''"
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
unsupported(_raw: string, _reason: string): string {
|
|
591
|
+
// Unreachable in the parse-first flow: `convertExpressionToPerl`
|
|
592
|
+
// gates on `isSupported` before dispatching, and `isSupported`
|
|
593
|
+
// recurses, so a top-level supported expression never contains an
|
|
594
|
+
// `unsupported` node. Return a safe Perl empty-string literal in
|
|
595
|
+
// case a future caller renders a node tree directly.
|
|
596
|
+
return "''"
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
600
|
+
// Mirror `unsupported`: a bare object literal reaching the dispatcher
|
|
601
|
+
// lowers to the safe Perl empty-string literal, exactly as before the
|
|
602
|
+
// `object-literal` kind existed (byte-identical; Roadmap A-1). Object
|
|
603
|
+
// values that round-trip to a Perl hashref go through the dedicated
|
|
604
|
+
// `objectLiteralToPerlHashref` lowering in the conditional/attr paths.
|
|
605
|
+
return "''"
|
|
606
|
+
}
|
|
607
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operand-type classification + index-access lowering for the Mojolicious
|
|
3
|
+
* EP template adapter.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
|
|
6
|
+
* track D). Pure functions over `ParsedExpr` — they take an `isStringName`
|
|
7
|
+
* predicate (supplied by the emitter from adapter state) rather than reading
|
|
8
|
+
* adapter instance state directly.
|
|
9
|
+
*
|
|
10
|
+
* SHARED CANDIDATE: `isStringTypedOperand` is byte-identical to the Xslate
|
|
11
|
+
* adapter's copy and is adapter-agnostic — an extraction candidate for a
|
|
12
|
+
* shared Perl-family codegen module (groundwork for the future Perl evaluator
|
|
13
|
+
* integration, issue #2018 track D). `emitIndexAccessPerl` stays Mojo-specific
|
|
14
|
+
* (Perl's `->[]` vs `->{}` split has no Kolon equivalent).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ParsedExpr } from '@barefootjs/jsx'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a comparison operand is string-typed, so JS `===`/`!==` against it
|
|
21
|
+
* must lower to Perl `eq`/`ne` instead of numeric `==`/`!=` (#1672). Covers a
|
|
22
|
+
* string literal, a string-signal getter call (`sel()`), and a string prop
|
|
23
|
+
* access (`props.x`). `isStringName` reports whether a getter/prop name is
|
|
24
|
+
* known-string. Loop-element fields (`t.id`) on untyped arrays have no known
|
|
25
|
+
* type and stay undetected — a separate, narrower gap.
|
|
26
|
+
*/
|
|
27
|
+
export function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string) => boolean): boolean {
|
|
28
|
+
if (expr.kind === 'literal' && expr.literalType === 'string') return true
|
|
29
|
+
if (expr.kind === 'call' && expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
30
|
+
return isStringName(expr.callee.name)
|
|
31
|
+
}
|
|
32
|
+
if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
|
|
33
|
+
return isStringName(expr.property)
|
|
34
|
+
}
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lower `arr[index]` to a Perl deref. Perl distinguishes array
|
|
40
|
+
* (`->[$i]`) from hash (`->{$k}`) access, which JS's single `[]` does
|
|
41
|
+
* not — so we pick by the index expression's type: a string-typed key
|
|
42
|
+
* derefs the hash, anything else (the common loop-index / arithmetic
|
|
43
|
+
* case, e.g. `selected()[index]`) derefs the array. #1897.
|
|
44
|
+
*/
|
|
45
|
+
export function emitIndexAccessPerl(
|
|
46
|
+
object: ParsedExpr,
|
|
47
|
+
index: ParsedExpr,
|
|
48
|
+
emit: (e: ParsedExpr) => string,
|
|
49
|
+
isStringName: (n: string) => boolean,
|
|
50
|
+
): string {
|
|
51
|
+
const i = emit(index)
|
|
52
|
+
return isStringTypedOperand(index, isStringName)
|
|
53
|
+
? `${emit(object)}->{${i}}`
|
|
54
|
+
: `${emit(object)}->[${i}]`
|
|
55
|
+
}
|