@rhinostone/swig-core 2.0.0-alpha.3

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/lib/ir.js ADDED
@@ -0,0 +1,873 @@
1
+ /**
2
+ * Swig IR — intermediate representation for the shared backend.
3
+ *
4
+ * Phase 1: typedef stubs only. No runtime code here — Phase 1 keeps the
5
+ * native frontend emitting JS source directly. Phase 2 ports the native
6
+ * frontend to emit IR, at which point `@rhinostone/swig-core/lib/backend.js`
7
+ * walks an IRTemplate and produces the compiled `new Function(...)` body.
8
+ *
9
+ * Every frontend (native Swig, Twig, Jinja2, Django) must lower its
10
+ * parse tree into these shapes. Constructs that cannot lower cleanly
11
+ * must throw at parse time — no silent partial behavior.
12
+ *
13
+ * See .claude/architecture/multi-flavor-ir.md for the full design doc,
14
+ * trade-offs considered, and open questions.
15
+ */
16
+
17
+ /**
18
+ * Source-location metadata carried by every IR node so backend errors
19
+ * surface the original line/column/filename unchanged.
20
+ *
21
+ * @typedef {Object} IRLoc
22
+ * @property {number} line
23
+ * @property {number} [column]
24
+ * @property {string} [filename]
25
+ */
26
+
27
+ /* ------------------------------------------------------------------ *
28
+ * Statement nodes — body-level.
29
+ * ------------------------------------------------------------------ */
30
+
31
+ /**
32
+ * Root of every compiled template.
33
+ *
34
+ * @typedef {Object} IRTemplate
35
+ * @property {'Template'} type
36
+ * @property {IRStatement[]} body
37
+ * @property {string} [parent] Resolved path of the parent template (from `extends`).
38
+ * @property {Object<string, IRBlock>} [blocks] Block-name → Block IR subtree.
39
+ * @property {IRLoc} [loc]
40
+ */
41
+
42
+ /**
43
+ * Literal text chunk. Value is JSON-escaped at backend emit time.
44
+ *
45
+ * @typedef {Object} IRText
46
+ * @property {'Text'} type
47
+ * @property {string} value
48
+ * @property {IRLoc} [loc]
49
+ */
50
+
51
+ /**
52
+ * Output an expression with optional filter chain.
53
+ * `safe: true` bypasses autoescape.
54
+ *
55
+ * `expr` is typed `IRExpr | IRLegacyJS` for Phase 2 — see Session 14b
56
+ * Commit 9. The IR shape can't represent legacy Swig's per-operand
57
+ * filter precedence (filter binds to the last operand across binary
58
+ * ops, e.g. `{{ a + b|upper }}` → `a + _filters["upper"](b)`); for
59
+ * those outputs the frontend falls back to the legacy JS-string
60
+ * emission and wraps it in {@link IRLegacyJS}. The narrow back to
61
+ * strict `IRExpr` is gated on lifting filter chains to expression-level
62
+ * ({@link IRExpr}-position filters) — a follow-up commit.
63
+ *
64
+ * @typedef {Object} IROutput
65
+ * @property {'Output'} type
66
+ * @property {IRExpr|IRLegacyJS} expr
67
+ * @property {IRFilterCall[]} [filters]
68
+ * @property {boolean} [safe]
69
+ * @property {IRLoc} [loc]
70
+ */
71
+
72
+ /**
73
+ * Filter invocation inside an Output or Filter region.
74
+ * Distinct from statement-level {@link IRFilter} (region pipe).
75
+ *
76
+ * @typedef {Object} IRFilterCall
77
+ * @property {string} name
78
+ * @property {IRExpr[]} [args]
79
+ */
80
+
81
+ /**
82
+ * if / elif / else chain. Each branch's `test` is `null` for the
83
+ * trailing else.
84
+ *
85
+ * @typedef {Object} IRIf
86
+ * @property {'If'} type
87
+ * @property {IRIfBranch[]} branches
88
+ * @property {IRLoc} [loc]
89
+ */
90
+
91
+ /**
92
+ * `test` is `null` for the trailing else branch. For regular `if` /
93
+ * `elseif` branches it is an {@link IRExpr} when the test expression
94
+ * lowers cleanly, or an {@link IRLegacyJS} escape-hatch when the test
95
+ * contains a top-level filter chain mixed with a binary op (e.g.
96
+ * `{% if a === b|upper %}`) — per-operand filter precedence cannot be
97
+ * represented in a flat IR, same widening as {@link IROutput.expr} in
98
+ * Session 14b Commit 9. The factory is opaque — it accepts any value
99
+ * and stores it — but consumers (backends) dispatch on the shape.
100
+ *
101
+ * @typedef {Object} IRIfBranch
102
+ * @property {IRExpr|IRLegacyJS|null} test
103
+ * @property {IRStatement[]} body
104
+ */
105
+
106
+ /**
107
+ * For-loop. `emptyBody` supports Twig/Django `{% for … %}{% else %}`.
108
+ *
109
+ * @typedef {Object} IRFor
110
+ * @property {'For'} type
111
+ * @property {string} [key] Loop key var (second binding).
112
+ * @property {string} value Loop value var (first binding).
113
+ * @property {IRExpr} iterable
114
+ * @property {IRStatement[]} body
115
+ * @property {IRStatement[]} [emptyBody]
116
+ * @property {IRLoc} [loc]
117
+ */
118
+
119
+ /**
120
+ * Named override point for template inheritance.
121
+ *
122
+ * @typedef {Object} IRBlock
123
+ * @property {'Block'} type
124
+ * @property {string} name
125
+ * @property {IRStatement[]} body
126
+ * @property {IRLoc} [loc]
127
+ */
128
+
129
+ /**
130
+ * `ignoreMissing` maps to swig's `ignore missing` modifier (silently swallow
131
+ * a compile-time load error from the included file). `resolveFrom` carries
132
+ * the including template's filename so the loader can resolve relative
133
+ * paths from the right anchor; empty string means "no anchor" (consumers
134
+ * must treat it as a JS-safe double-quoted-string fragment).
135
+ *
136
+ * @typedef {Object} IRInclude
137
+ * @property {'Include'} type
138
+ * @property {IRExpr} path Usually a string literal, but any expression is allowed.
139
+ * @property {IRExpr} [context] Explicit locals for the included template.
140
+ * @property {boolean} [isolated] Maps to Twig's `only`.
141
+ * @property {boolean} [ignoreMissing] Swallow loader errors when the file is missing.
142
+ * @property {string} [resolveFrom] Including template's filename (backslash-escaped) for loader-relative resolution.
143
+ * @property {IRLoc} [loc]
144
+ */
145
+
146
+ /**
147
+ * @typedef {Object} IRImport
148
+ * @property {'Import'} type
149
+ * @property {IRExpr} path
150
+ * @property {string} alias Namespace name. MUST pass the dangerousProps guard.
151
+ * @property {IRLoc} [loc]
152
+ */
153
+
154
+ /**
155
+ * Macro definition. Params are bare identifier names.
156
+ *
157
+ * @typedef {Object} IRMacro
158
+ * @property {'Macro'} type
159
+ * @property {string} name MUST pass the dangerousProps guard.
160
+ * @property {IRMacroParam[]} params
161
+ * @property {IRStatement[]} body
162
+ * @property {IRLoc} [loc]
163
+ */
164
+
165
+ /**
166
+ * @typedef {Object} IRMacroParam
167
+ * @property {string} name
168
+ * @property {IRExpr} [default]
169
+ */
170
+
171
+ /**
172
+ * Statement-level function / macro invocation (no output capture).
173
+ *
174
+ * @typedef {Object} IRCall
175
+ * @property {'Call'} type
176
+ * @property {IRExpr} callee
177
+ * @property {IRExpr[]} args
178
+ * @property {IRLoc} [loc]
179
+ */
180
+
181
+ /**
182
+ * Phase 2 Session 14b Commit 10: pure-dot LHS shapes (`foo`, `foo.bar`,
183
+ * `foo.bar.baz`) are now structured {@link IRVarRef} nodes. The
184
+ * bracket-touched path (`foo[bar]`, `foo["bar"]`, mixed dot+bracket)
185
+ * stays on the transitional `string` form — bracket-lvalue semantics
186
+ * (notably the runtime-variable-key case `foo[bar]`) is a cross-flavor
187
+ * design call deferred to a dedicated session. Backends MUST tolerate
188
+ * both shapes for now.
189
+ *
190
+ * `op` carries the assignment operator (`=`, `+=`, `-=`, `*=`, `/=`) so
191
+ * the backend can emit `<target> <op> <value>;` without re-parsing.
192
+ *
193
+ * @typedef {Object} IRSet
194
+ * @property {'Set'} type
195
+ * @property {IRVarRef|string} target MUST pass the dangerousProps guard at every path segment.
196
+ * @property {string} op Assignment operator.
197
+ * @property {IRExpr} value
198
+ * @property {IRLoc} [loc]
199
+ */
200
+
201
+ /**
202
+ * Verbatim text. Never autoescaped, never re-parsed by the frontend.
203
+ *
204
+ * @typedef {Object} IRRaw
205
+ * @property {'Raw'} type
206
+ * @property {string} value
207
+ * @property {IRLoc} [loc]
208
+ */
209
+
210
+ /**
211
+ * Emit the parent block's compiled content (super() / block.super).
212
+ *
213
+ * During Phase 2 (#T15), IRParent optionally carries a `body` slot so
214
+ * the native frontend's parent tag can resolve the parent-chain lookup
215
+ * at compile time (emitting the matched block's body) without the
216
+ * backend having to re-walk the parents array. Target shape is a bare
217
+ * marker — the backend resolves parent() by itself — reached once the
218
+ * IR owns the extends/blocks graph directly (post-Phase 2).
219
+ *
220
+ * @typedef {Object} IRParent
221
+ * @property {'Parent'} type
222
+ * @property {IRStatement[]} [body] Transitional: pre-resolved parent-block content.
223
+ * @property {IRLoc} [loc]
224
+ */
225
+
226
+ /**
227
+ * Push/pop an autoescape strategy for a body region.
228
+ *
229
+ * @typedef {Object} IRAutoescape
230
+ * @property {'Autoescape'} type
231
+ * @property {true|false|'html'|'js'} strategy
232
+ * @property {IRStatement[]} body
233
+ * @property {IRLoc} [loc]
234
+ */
235
+
236
+ /**
237
+ * Region-level filter pipe (Swig's `{% filter %}`, Twig's `{% apply %}`).
238
+ *
239
+ * @typedef {Object} IRFilter
240
+ * @property {'Filter'} type
241
+ * @property {string} name
242
+ * @property {IRExpr[]} [args]
243
+ * @property {IRStatement[]} body
244
+ * @property {IRLoc} [loc]
245
+ */
246
+
247
+ /**
248
+ * Scoped-context region (Twig's `{% with %}`, Jinja2's `{% with %}`).
249
+ *
250
+ * `context` is an optional {@link IRExpr} evaluated once at entry. When
251
+ * `isolated` is true, the body sees only `context` (or an empty object
252
+ * when `context` is absent). When `isolated` is false, the body sees
253
+ * `_utils.extend({}, _ctx, context)` — or a shallow copy of `_ctx` when
254
+ * `context` is absent. The backend emits the region as an IIFE that
255
+ * shadows `_ctx` for the body's lexical scope.
256
+ *
257
+ * @typedef {Object} IRWith
258
+ * @property {'With'} type
259
+ * @property {IRExpr} [context]
260
+ * @property {boolean} [isolated]
261
+ * @property {IRStatement[]} body
262
+ * @property {IRLoc} [loc]
263
+ */
264
+
265
+ /**
266
+ * Legacy JS-string escape hatch for constructs whose codegen still lives
267
+ * outside the IR emitters — userland `setTag`-registered tag `compile`
268
+ * functions, and built-in tags not yet migrated to real IR nodes. The
269
+ * backend concatenates `js` verbatim into the compiled template body.
270
+ *
271
+ * Transitional per the Phase 2 layering decision (hybrid / option iii);
272
+ * see .claude/architecture/multi-flavor-ir.md.
273
+ *
274
+ * @typedef {Object} IRLegacyJS
275
+ * @property {'LegacyJS'} type
276
+ * @property {string} js
277
+ * @property {IRLoc} [loc]
278
+ */
279
+
280
+ /**
281
+ * Any body-level IR node.
282
+ *
283
+ * @typedef {(
284
+ * IRText | IROutput | IRIf | IRFor | IRBlock | IRInclude | IRImport |
285
+ * IRMacro | IRCall | IRSet | IRRaw | IRParent | IRAutoescape | IRFilter |
286
+ * IRWith | IRLegacyJS
287
+ * )} IRStatement
288
+ */
289
+
290
+ /* ------------------------------------------------------------------ *
291
+ * Expression nodes.
292
+ * ------------------------------------------------------------------ */
293
+
294
+ /**
295
+ * @typedef {Object} IRLiteral
296
+ * @property {'Literal'} type
297
+ * @property {'string'|'number'|'bool'|'null'|'undefined'} kind
298
+ * @property {string|number|boolean|null|undefined} value
299
+ * @property {IRLoc} [loc]
300
+ */
301
+
302
+ /**
303
+ * Dot-path variable reference: `user.profile.name` → `{ path: ['user', 'profile', 'name'] }`.
304
+ * Every path segment MUST pass the dangerousProps guard at backend emit time.
305
+ *
306
+ * @typedef {Object} IRVarRef
307
+ * @property {'VarRef'} type
308
+ * @property {string[]} path
309
+ * @property {IRLoc} [loc]
310
+ */
311
+
312
+ /**
313
+ * Dynamic (bracket) property access: `obj[key]`. `key` is any expression.
314
+ * When `key` is an {@link IRLiteral} of kind `'string'`, the backend
315
+ * applies the dangerousProps guard.
316
+ *
317
+ * @typedef {Object} IRAccess
318
+ * @property {'Access'} type
319
+ * @property {IRExpr} object
320
+ * @property {IRExpr} key
321
+ * @property {IRLoc} [loc]
322
+ */
323
+
324
+ /**
325
+ * Binary operation. Frontends normalize aliases into canonical ops:
326
+ * `gt` → `>`, `and` → `&&`, `is` → `===` (with sentinel-RHS lowering
327
+ * for `is defined`, `is divisibleby(3)`, etc.).
328
+ *
329
+ * @typedef {Object} IRBinaryOp
330
+ * @property {'BinaryOp'} type
331
+ * @property {string} op
332
+ * @property {IRExpr} left
333
+ * @property {IRExpr} right
334
+ * @property {IRLoc} [loc]
335
+ */
336
+
337
+ /**
338
+ * @typedef {Object} IRUnaryOp
339
+ * @property {'UnaryOp'} type
340
+ * @property {'!'|'-'|'+'} op
341
+ * @property {IRExpr} operand
342
+ * @property {IRLoc} [loc]
343
+ */
344
+
345
+ /**
346
+ * Ternary.
347
+ *
348
+ * @typedef {Object} IRConditional
349
+ * @property {'Conditional'} type
350
+ * @property {IRExpr} test
351
+ * @property {IRExpr} then
352
+ * @property {IRExpr} else
353
+ * @property {IRLoc} [loc]
354
+ */
355
+
356
+ /**
357
+ * @typedef {Object} IRArrayLiteral
358
+ * @property {'ArrayLiteral'} type
359
+ * @property {IRExpr[]} elements
360
+ * @property {IRLoc} [loc]
361
+ */
362
+
363
+ /**
364
+ * @typedef {Object} IRObjectLiteral
365
+ * @property {'ObjectLiteral'} type
366
+ * @property {IRObjectProperty[]} properties
367
+ * @property {IRLoc} [loc]
368
+ */
369
+
370
+ /**
371
+ * @typedef {Object} IRObjectProperty
372
+ * @property {IRExpr} key Usually an IRLiteral of kind 'string', but any expression is allowed.
373
+ * @property {IRExpr} value
374
+ */
375
+
376
+ /**
377
+ * Function / method invocation at expression position.
378
+ *
379
+ * @typedef {Object} IRFnCall
380
+ * @property {'FnCall'} type
381
+ * @property {IRExpr} callee
382
+ * @property {IRExpr[]} args
383
+ * @property {IRLoc} [loc]
384
+ */
385
+
386
+ /**
387
+ * Filter invocation at expression position — distinct from the
388
+ * type-less {@link IRFilterCall} helper that lives inside
389
+ * `IROutput.filters`. This shape carries its own `input` expression so a
390
+ * filter can appear anywhere an expression can — inside a binary op
391
+ * (`a + b|upper`), inside a function call (`foo(bar|upper)`), inside a
392
+ * bracket access (`foo[bar|upper]`), chained (`a|upper|reverse`).
393
+ *
394
+ * The IRFilterCall helper stays the carrier for the top-level filter
395
+ * pipe on an Output, because that pipe feeds the accumulated output
396
+ * expression positionally — it doesn't fit a `{ input, ... }` shape.
397
+ *
398
+ * @typedef {Object} IRFilterCallExpr
399
+ * @property {'FilterCall'} type
400
+ * @property {string} name
401
+ * @property {IRExpr} input
402
+ * @property {IRExpr[]} [args]
403
+ * @property {IRLoc} [loc]
404
+ */
405
+
406
+ /**
407
+ * Any expression-position IR node.
408
+ *
409
+ * @typedef {(
410
+ * IRLiteral | IRVarRef | IRAccess | IRBinaryOp | IRUnaryOp |
411
+ * IRConditional | IRArrayLiteral | IRObjectLiteral | IRFnCall |
412
+ * IRFilterCallExpr
413
+ * )} IRExpr
414
+ */
415
+
416
+ /* ------------------------------------------------------------------ *
417
+ * Runtime node factories — Phase 2 scaffold (Session 7, 2026-04-14).
418
+ *
419
+ * Each factory returns a plain JSON-serialisable object matching one
420
+ * of the typedefs above. `loc` is always optional; when omitted it is
421
+ * not set on the returned node (consumers can distinguish via
422
+ * `'loc' in node`). All other parameters are required unless documented
423
+ * otherwise on the corresponding typedef.
424
+ *
425
+ * No consumers yet — this commit introduces the schema surface only.
426
+ * Subsequent sessions will migrate the native frontend's token-tree
427
+ * production over to these shapes. See the Phase 2 layering notes in
428
+ * .claude/architecture/multi-flavor-ir.md.
429
+ * ------------------------------------------------------------------ */
430
+
431
+ /*!
432
+ * Attach `loc` to the node if provided, skipping the assignment otherwise
433
+ * so consumers can tell "no source location available" from
434
+ * "source location is the default IRLoc".
435
+ * @private
436
+ */
437
+ function withLoc(node, loc) {
438
+ if (loc !== undefined) {
439
+ node.loc = loc;
440
+ }
441
+ return node;
442
+ }
443
+
444
+ /* -- Statement factories ------------------------------------------- */
445
+
446
+ /**
447
+ * Build a {@link IRTemplate} root node.
448
+ * @param {IRStatement[]} body
449
+ * @param {string} [parent]
450
+ * @param {Object<string, IRBlock>} [blocks]
451
+ * @param {IRLoc} [loc]
452
+ * @return {IRTemplate}
453
+ */
454
+ exports.template = function (body, parent, blocks, loc) {
455
+ var node = { type: 'Template', body: body };
456
+ if (parent !== undefined) { node.parent = parent; }
457
+ if (blocks !== undefined) { node.blocks = blocks; }
458
+ return withLoc(node, loc);
459
+ };
460
+
461
+ /**
462
+ * Build an {@link IRText} literal-text node.
463
+ * @param {string} value
464
+ * @param {IRLoc} [loc]
465
+ * @return {IRText}
466
+ */
467
+ exports.text = function (value, loc) {
468
+ return withLoc({ type: 'Text', value: value }, loc);
469
+ };
470
+
471
+ /**
472
+ * Build an {@link IROutput} node.
473
+ * @param {IRExpr} expr
474
+ * @param {IRFilterCall[]} [filters]
475
+ * @param {boolean} [safe]
476
+ * @param {IRLoc} [loc]
477
+ * @return {IROutput}
478
+ */
479
+ exports.output = function (expr, filters, safe, loc) {
480
+ var node = { type: 'Output', expr: expr };
481
+ if (filters !== undefined) { node.filters = filters; }
482
+ if (safe !== undefined) { node.safe = safe; }
483
+ return withLoc(node, loc);
484
+ };
485
+
486
+ /**
487
+ * Build an {@link IRFilterCall}. Used inside `Output.filters` and as
488
+ * the tag-level filter invocation carried by {@link IRFilter}.
489
+ * Note: not a statement — helper shape.
490
+ * @param {string} name
491
+ * @param {IRExpr[]} [args]
492
+ * @return {IRFilterCall}
493
+ */
494
+ exports.filterCall = function (name, args) {
495
+ var node = { name: name };
496
+ if (args !== undefined) { node.args = args; }
497
+ return node;
498
+ };
499
+
500
+ /**
501
+ * Build an {@link IRIf} node from a sequence of branches.
502
+ * @param {IRIfBranch[]} branches
503
+ * @param {IRLoc} [loc]
504
+ * @return {IRIf}
505
+ */
506
+ exports.ifStmt = function (branches, loc) {
507
+ return withLoc({ type: 'If', branches: branches }, loc);
508
+ };
509
+
510
+ /**
511
+ * Build an {@link IRIfBranch}. `test` is null for the trailing else.
512
+ *
513
+ * The factory stores `test` opaquely and does not inspect it; consumers
514
+ * expect a real {@link IRExpr} node or `null`.
515
+ *
516
+ * @param {IRExpr|null} test
517
+ * @param {IRStatement[]} body
518
+ * @return {IRIfBranch}
519
+ */
520
+ exports.ifBranch = function (test, body) {
521
+ return { test: test, body: body };
522
+ };
523
+
524
+ /**
525
+ * Build an {@link IRFor} node.
526
+ *
527
+ * `iterable` is typed `IRExpr | string` for Phase 2 — see the IRFor typedef
528
+ * for the transitional shape. The factory stores `iterable` opaquely and
529
+ * does not inspect it.
530
+ *
531
+ * @param {string} value Loop value identifier (first binding).
532
+ * @param {IRExpr|string} iterable
533
+ * @param {IRStatement[]} body
534
+ * @param {string} [key] Loop key identifier (second binding).
535
+ * @param {IRStatement[]} [emptyBody]
536
+ * @param {IRLoc} [loc]
537
+ * @return {IRFor}
538
+ */
539
+ exports.forStmt = function (value, iterable, body, key, emptyBody, loc) {
540
+ var node = { type: 'For', value: value, iterable: iterable, body: body };
541
+ if (key !== undefined) { node.key = key; }
542
+ if (emptyBody !== undefined) { node.emptyBody = emptyBody; }
543
+ return withLoc(node, loc);
544
+ };
545
+
546
+ /**
547
+ * Build an {@link IRBlock} override point.
548
+ * @param {string} name
549
+ * @param {IRStatement[]} body
550
+ * @param {IRLoc} [loc]
551
+ * @return {IRBlock}
552
+ */
553
+ exports.block = function (name, body, loc) {
554
+ return withLoc({ type: 'Block', name: name, body: body }, loc);
555
+ };
556
+
557
+ /**
558
+ * Build an {@link IRInclude} node.
559
+ *
560
+ * `path` and `context` are {@link IRExpr} nodes (Session 14b Commit 7).
561
+ * The factory stores both opaquely and does not inspect them — backward-
562
+ * compat string fallback is handled at backend emit time for userland
563
+ * setTag tags that may still hand in raw JS-source fragments.
564
+ * `ignoreMissing` and `resolveFrom` are swig-native modifiers carried
565
+ * through so the backend can build the `_swig.compileFile(...)` emission.
566
+ *
567
+ * @param {IRExpr} path
568
+ * @param {IRExpr} [context]
569
+ * @param {boolean} [isolated]
570
+ * @param {boolean} [ignoreMissing]
571
+ * @param {string} [resolveFrom]
572
+ * @param {IRLoc} [loc]
573
+ * @return {IRInclude}
574
+ */
575
+ exports.include = function (path, context, isolated, ignoreMissing, resolveFrom, loc) {
576
+ var node = { type: 'Include', path: path };
577
+ if (context !== undefined) { node.context = context; }
578
+ if (isolated !== undefined) { node.isolated = isolated; }
579
+ if (ignoreMissing !== undefined) { node.ignoreMissing = ignoreMissing; }
580
+ if (resolveFrom !== undefined) { node.resolveFrom = resolveFrom; }
581
+ return withLoc(node, loc);
582
+ };
583
+
584
+ /**
585
+ * Build an {@link IRImport} node. `alias` MUST pass the dangerousProps
586
+ * guard at backend emit time.
587
+ * @param {IRExpr} path
588
+ * @param {string} alias
589
+ * @param {IRLoc} [loc]
590
+ * @return {IRImport}
591
+ */
592
+ exports.importStmt = function (path, alias, loc) {
593
+ return withLoc({ type: 'Import', path: path, alias: alias }, loc);
594
+ };
595
+
596
+ /**
597
+ * Build an {@link IRMacro} definition. `name` MUST pass the
598
+ * dangerousProps guard at backend emit time.
599
+ * @param {string} name
600
+ * @param {IRMacroParam[]} params
601
+ * @param {IRStatement[]} body
602
+ * @param {IRLoc} [loc]
603
+ * @return {IRMacro}
604
+ */
605
+ exports.macro = function (name, params, body, loc) {
606
+ return withLoc({ type: 'Macro', name: name, params: params, body: body }, loc);
607
+ };
608
+
609
+ /**
610
+ * Build an {@link IRMacroParam}.
611
+ * @param {string} name
612
+ * @param {IRExpr} [defaultValue]
613
+ * @return {IRMacroParam}
614
+ */
615
+ exports.macroParam = function (name, defaultValue) {
616
+ var node = { name: name };
617
+ if (defaultValue !== undefined) { node['default'] = defaultValue; }
618
+ return node;
619
+ };
620
+
621
+ /**
622
+ * Build an {@link IRCall} statement-level invocation.
623
+ * @param {IRExpr} callee
624
+ * @param {IRExpr[]} args
625
+ * @param {IRLoc} [loc]
626
+ * @return {IRCall}
627
+ */
628
+ exports.call = function (callee, args, loc) {
629
+ return withLoc({ type: 'Call', callee: callee, args: args }, loc);
630
+ };
631
+
632
+ /**
633
+ * Build an {@link IRSet} node. `target` MUST pass the dangerousProps
634
+ * guard at every path segment at backend emit time.
635
+ *
636
+ * `target` is typed `IRVarRef | string` for Phase 2 — pure-dot LHS
637
+ * shapes are structured {@link IRVarRef} (Session 14b Commit 10);
638
+ * bracket-touched LHS stays a string fragment until the cross-flavor
639
+ * bracket-lvalue contract lands. The factory stores `target` and
640
+ * `value` opaquely and does not inspect them. `op` is the JS assignment
641
+ * operator (`=`, `+=`, etc.).
642
+ *
643
+ * @param {IRVarRef|string} target
644
+ * @param {string} op
645
+ * @param {IRExpr} value
646
+ * @param {IRLoc} [loc]
647
+ * @return {IRSet}
648
+ */
649
+ exports.set = function (target, op, value, loc) {
650
+ return withLoc({ type: 'Set', target: target, op: op, value: value }, loc);
651
+ };
652
+
653
+ /**
654
+ * Build an {@link IRRaw} verbatim-text node.
655
+ * @param {string} value
656
+ * @param {IRLoc} [loc]
657
+ * @return {IRRaw}
658
+ */
659
+ exports.raw = function (value, loc) {
660
+ return withLoc({ type: 'Raw', value: value }, loc);
661
+ };
662
+
663
+ /**
664
+ * Build an {@link IRParent} super()-equivalent node.
665
+ *
666
+ * Phase 2: optional `body` slot carries the pre-resolved parent-block
667
+ * content as IR statements. Swig's parent tag walks the parents chain
668
+ * at compile time and drops the matched block's body here; backends
669
+ * emit the body as-is. A bare Parent (no body) is valid and means the
670
+ * backend will resolve the lookup itself — reached post-Phase 2.
671
+ *
672
+ * @param {IRStatement[]} [body]
673
+ * @param {IRLoc} [loc]
674
+ * @return {IRParent}
675
+ */
676
+ exports.parent = function (body, loc) {
677
+ var node = { type: 'Parent' };
678
+ if (body !== undefined) { node.body = body; }
679
+ return withLoc(node, loc);
680
+ };
681
+
682
+ /**
683
+ * Build an {@link IRAutoescape} region.
684
+ * @param {true|false|'html'|'js'} strategy
685
+ * @param {IRStatement[]} body
686
+ * @param {IRLoc} [loc]
687
+ * @return {IRAutoescape}
688
+ */
689
+ exports.autoescape = function (strategy, body, loc) {
690
+ return withLoc({ type: 'Autoescape', strategy: strategy, body: body }, loc);
691
+ };
692
+
693
+ /**
694
+ * Build an {@link IRFilter} region-level filter pipe.
695
+ *
696
+ * The factory stores `args` opaquely and does not inspect its elements.
697
+ *
698
+ * @param {string} name
699
+ * @param {IRStatement[]} body
700
+ * @param {IRExpr[]} [args]
701
+ * @param {IRLoc} [loc]
702
+ * @return {IRFilter}
703
+ */
704
+ exports.filter = function (name, body, args, loc) {
705
+ var node = { type: 'Filter', name: name, body: body };
706
+ if (args !== undefined) { node.args = args; }
707
+ return withLoc(node, loc);
708
+ };
709
+
710
+ /**
711
+ * Build an {@link IRWith} scoped-context region.
712
+ *
713
+ * @param {IRExpr} [context] Optional context expression.
714
+ * @param {boolean} [isolated] `true` drops `_ctx` from the body.
715
+ * @param {IRStatement[]} body
716
+ * @param {IRLoc} [loc]
717
+ * @return {IRWith}
718
+ */
719
+ exports.withStmt = function (context, isolated, body, loc) {
720
+ var node = { type: 'With', body: body };
721
+ if (context !== undefined) { node.context = context; }
722
+ if (isolated !== undefined) { node.isolated = isolated; }
723
+ return withLoc(node, loc);
724
+ };
725
+
726
+ /**
727
+ * Build an {@link IRLegacyJS} escape-hatch node. Wraps a raw JS-source
728
+ * fragment so the backend walker can splice it into the compiled body
729
+ * without a dedicated emitter. The first consumer is `backend.compile`,
730
+ * which wraps every current parse-tree token (string, VarToken, TagToken)
731
+ * as an `IRLegacyJS` before emission; further built-in tags migrate to
732
+ * real IR shapes in subsequent sessions.
733
+ * @param {string} js
734
+ * @param {IRLoc} [loc]
735
+ * @return {IRLegacyJS}
736
+ */
737
+ exports.legacyJS = function (js, loc) {
738
+ return withLoc({ type: 'LegacyJS', js: js }, loc);
739
+ };
740
+
741
+ /* -- Expression factories ------------------------------------------ */
742
+
743
+ /**
744
+ * Build an {@link IRLiteral}.
745
+ * @param {'string'|'number'|'bool'|'null'|'undefined'} kind
746
+ * @param {string|number|boolean|null|undefined} value
747
+ * @param {IRLoc} [loc]
748
+ * @return {IRLiteral}
749
+ */
750
+ exports.literal = function (kind, value, loc) {
751
+ return withLoc({ type: 'Literal', kind: kind, value: value }, loc);
752
+ };
753
+
754
+ /**
755
+ * Build an {@link IRVarRef} dot-path variable reference. Every path
756
+ * segment MUST pass the dangerousProps guard at backend emit time.
757
+ * @param {string[]} path
758
+ * @param {IRLoc} [loc]
759
+ * @return {IRVarRef}
760
+ */
761
+ exports.varRef = function (path, loc) {
762
+ return withLoc({ type: 'VarRef', path: path }, loc);
763
+ };
764
+
765
+ /**
766
+ * Build an {@link IRAccess} dynamic-bracket property access.
767
+ * @param {IRExpr} object
768
+ * @param {IRExpr} key
769
+ * @param {IRLoc} [loc]
770
+ * @return {IRAccess}
771
+ */
772
+ exports.access = function (object, key, loc) {
773
+ return withLoc({ type: 'Access', object: object, key: key }, loc);
774
+ };
775
+
776
+ /**
777
+ * Build an {@link IRBinaryOp}.
778
+ * @param {string} op
779
+ * @param {IRExpr} left
780
+ * @param {IRExpr} right
781
+ * @param {IRLoc} [loc]
782
+ * @return {IRBinaryOp}
783
+ */
784
+ exports.binaryOp = function (op, left, right, loc) {
785
+ return withLoc({ type: 'BinaryOp', op: op, left: left, right: right }, loc);
786
+ };
787
+
788
+ /**
789
+ * Build an {@link IRUnaryOp}.
790
+ * @param {'!'|'-'|'+'} op
791
+ * @param {IRExpr} operand
792
+ * @param {IRLoc} [loc]
793
+ * @return {IRUnaryOp}
794
+ */
795
+ exports.unaryOp = function (op, operand, loc) {
796
+ return withLoc({ type: 'UnaryOp', op: op, operand: operand }, loc);
797
+ };
798
+
799
+ /**
800
+ * Build an {@link IRConditional} ternary. The parameter is named
801
+ * `els` to avoid shadowing the reserved-word `else`; the produced
802
+ * object uses `else` as a string key per the typedef.
803
+ * @param {IRExpr} test
804
+ * @param {IRExpr} then
805
+ * @param {IRExpr} els
806
+ * @param {IRLoc} [loc]
807
+ * @return {IRConditional}
808
+ */
809
+ exports.conditional = function (test, then, els, loc) {
810
+ var node = { type: 'Conditional', test: test, then: then };
811
+ node['else'] = els;
812
+ return withLoc(node, loc);
813
+ };
814
+
815
+ /**
816
+ * Build an {@link IRArrayLiteral}.
817
+ * @param {IRExpr[]} elements
818
+ * @param {IRLoc} [loc]
819
+ * @return {IRArrayLiteral}
820
+ */
821
+ exports.arrayLiteral = function (elements, loc) {
822
+ return withLoc({ type: 'ArrayLiteral', elements: elements }, loc);
823
+ };
824
+
825
+ /**
826
+ * Build an {@link IRObjectLiteral}.
827
+ * @param {IRObjectProperty[]} properties
828
+ * @param {IRLoc} [loc]
829
+ * @return {IRObjectLiteral}
830
+ */
831
+ exports.objectLiteral = function (properties, loc) {
832
+ return withLoc({ type: 'ObjectLiteral', properties: properties }, loc);
833
+ };
834
+
835
+ /**
836
+ * Build an {@link IRObjectProperty}.
837
+ * @param {IRExpr} key
838
+ * @param {IRExpr} value
839
+ * @return {IRObjectProperty}
840
+ */
841
+ exports.objectProperty = function (key, value) {
842
+ return { key: key, value: value };
843
+ };
844
+
845
+ /**
846
+ * Build an {@link IRFnCall} expression-position invocation.
847
+ * @param {IRExpr} callee
848
+ * @param {IRExpr[]} args
849
+ * @param {IRLoc} [loc]
850
+ * @return {IRFnCall}
851
+ */
852
+ exports.fnCall = function (callee, args, loc) {
853
+ return withLoc({ type: 'FnCall', callee: callee, args: args }, loc);
854
+ };
855
+
856
+ /**
857
+ * Build an {@link IRFilterCallExpr} expression-position filter
858
+ * invocation. Distinct from {@link exports.filterCall} — this is a
859
+ * first-class IRExpr that wraps its own input, so filters can appear
860
+ * mid-expression (`a + b|upper`, `foo[bar|upper]`) and chain
861
+ * (`a|upper|reverse`).
862
+ *
863
+ * @param {string} name
864
+ * @param {IRExpr} input
865
+ * @param {IRExpr[]} [args]
866
+ * @param {IRLoc} [loc]
867
+ * @return {IRFilterCallExpr}
868
+ */
869
+ exports.filterCallExpr = function (name, input, args, loc) {
870
+ var node = { type: 'FilterCall', name: name, input: input };
871
+ if (args !== undefined) { node.args = args; }
872
+ return withLoc(node, loc);
873
+ };