@frontiers-labs/argon 0.4.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/annotations.d.ts +23 -0
  4. package/dist/annotations.d.ts.map +1 -0
  5. package/dist/annotations.js +23 -0
  6. package/dist/annotations.js.map +1 -0
  7. package/dist/codegen/client.d.ts +14 -0
  8. package/dist/codegen/client.d.ts.map +1 -0
  9. package/dist/codegen/client.js +964 -0
  10. package/dist/codegen/client.js.map +1 -0
  11. package/dist/codegen/dts.d.ts +3 -0
  12. package/dist/codegen/dts.d.ts.map +1 -0
  13. package/dist/codegen/dts.js +82 -0
  14. package/dist/codegen/dts.js.map +1 -0
  15. package/dist/codegen/js.d.ts +3 -0
  16. package/dist/codegen/js.d.ts.map +1 -0
  17. package/dist/codegen/js.js +278 -0
  18. package/dist/codegen/js.js.map +1 -0
  19. package/dist/codegen/rust.d.ts +6 -0
  20. package/dist/codegen/rust.d.ts.map +1 -0
  21. package/dist/codegen/rust.js +1076 -0
  22. package/dist/codegen/rust.js.map +1 -0
  23. package/dist/component.d.ts +35 -0
  24. package/dist/component.d.ts.map +1 -0
  25. package/dist/component.js +120 -0
  26. package/dist/component.js.map +1 -0
  27. package/dist/diagnostics.d.ts +20 -0
  28. package/dist/diagnostics.d.ts.map +1 -0
  29. package/dist/diagnostics.js +40 -0
  30. package/dist/diagnostics.js.map +1 -0
  31. package/dist/docs.d.ts +3 -0
  32. package/dist/docs.d.ts.map +1 -0
  33. package/dist/docs.js +42 -0
  34. package/dist/docs.js.map +1 -0
  35. package/dist/error-catalog.d.ts +11 -0
  36. package/dist/error-catalog.d.ts.map +1 -0
  37. package/dist/error-catalog.js +92 -0
  38. package/dist/error-catalog.js.map +1 -0
  39. package/dist/html-attrs.d.ts +2 -0
  40. package/dist/html-attrs.d.ts.map +1 -0
  41. package/dist/html-attrs.js +37 -0
  42. package/dist/html-attrs.js.map +1 -0
  43. package/dist/html.d.ts +41 -0
  44. package/dist/html.d.ts.map +1 -0
  45. package/dist/html.js +188 -0
  46. package/dist/html.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +139 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/ir.d.ts +321 -0
  52. package/dist/ir.d.ts.map +1 -0
  53. package/dist/ir.js +243 -0
  54. package/dist/ir.js.map +1 -0
  55. package/dist/jsx.d.ts +31 -0
  56. package/dist/jsx.d.ts.map +1 -0
  57. package/dist/jsx.js +242 -0
  58. package/dist/jsx.js.map +1 -0
  59. package/dist/lower.d.ts +6 -0
  60. package/dist/lower.d.ts.map +1 -0
  61. package/dist/lower.js +1341 -0
  62. package/dist/lower.js.map +1 -0
  63. package/dist/parser.d.ts +3 -0
  64. package/dist/parser.d.ts.map +1 -0
  65. package/dist/parser.js +714 -0
  66. package/dist/parser.js.map +1 -0
  67. package/dist/ssr-subset.d.ts +29 -0
  68. package/dist/ssr-subset.d.ts.map +1 -0
  69. package/dist/ssr-subset.js +119 -0
  70. package/dist/ssr-subset.js.map +1 -0
  71. package/dist/template.d.ts +32 -0
  72. package/dist/template.d.ts.map +1 -0
  73. package/dist/template.js +90 -0
  74. package/dist/template.js.map +1 -0
  75. package/package.json +67 -0
package/dist/lower.js ADDED
@@ -0,0 +1,1341 @@
1
+ // Lowering: TypeScript AST → Argon IR.
2
+ //
3
+ // This is the single place that understands the source language. Value
4
+ // expressions (template interpolations, setup statements, helper functions)
5
+ // are lowered into typed IR with inference over the SSR-safe subset. Anything
6
+ // outside the subset becomes an `opaque` node carrying the source text and a
7
+ // ready-made diagnostic; event handlers are opaque by construction.
8
+ import * as ts from "typescript";
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { componentSignatures, findComponents } from "./component.js";
12
+ import { processTemplate } from "./template.js";
13
+ import { ArgonCompileError, diagnosticForNode } from "./diagnostics.js";
14
+ import { containsJsx, isJsxRoot, jsxToTemplate, } from "./jsx.js";
15
+ import { BOOLEAN, isMarkup, NUMBER, STRING, UNKNOWN, } from "./ir.js";
16
+ import { ARRAY_METHODS, MATH_FNS, SSR_HINT, STRING_METHODS } from "./ssr-subset.js";
17
+ export { SSR_HINT } from "./ssr-subset.js";
18
+ class LowerError extends Error {
19
+ diagnostic;
20
+ constructor(diagnostic) {
21
+ super(diagnostic.message);
22
+ this.diagnostic = diagnostic;
23
+ }
24
+ }
25
+ export function lowerProgram(source, sourceText) {
26
+ return new Lowerer(source, sourceText).lower();
27
+ }
28
+ class Lowerer {
29
+ source;
30
+ sourceText;
31
+ structs = new Map();
32
+ tables = new Map();
33
+ fns = new Map();
34
+ cssConsts = new Set();
35
+ opaqueDecls = new Set();
36
+ decls = [];
37
+ scopes = [];
38
+ signatures = new Map();
39
+ namedImports = new Map();
40
+ resolvedComponents = new Map();
41
+ composedImports = new Set();
42
+ // Composition is a template feature; nested JSX outside component bodies
43
+ // (module helpers) cannot compose.
44
+ composing = false;
45
+ constructor(source, sourceText) {
46
+ this.source = source;
47
+ this.sourceText = sourceText;
48
+ }
49
+ lower() {
50
+ const imports = this.readImports();
51
+ this.signatures = componentSignatures(this.source);
52
+ const componentNames = new Set(this.signatures.keys());
53
+ // Pass 1: hoisted facts — interfaces and function signatures.
54
+ ts.forEachChild(this.source, node => {
55
+ if (ts.isInterfaceDeclaration(node)) {
56
+ try {
57
+ this.registerStruct(node.name.text, node.members);
58
+ }
59
+ catch (error) {
60
+ if (!(error instanceof LowerError))
61
+ throw error;
62
+ }
63
+ }
64
+ else if (ts.isFunctionDeclaration(node) && node.name && !componentNames.has(node.name.text)) {
65
+ this.fns.set(node.name.text, this.fnSignature(node));
66
+ }
67
+ });
68
+ // Pass 2: declaration bodies in source order.
69
+ ts.forEachChild(this.source, node => {
70
+ if (ts.isFunctionDeclaration(node) && node.name && !componentNames.has(node.name.text)) {
71
+ this.lowerFn(node);
72
+ }
73
+ else if (ts.isVariableStatement(node)) {
74
+ for (const decl of node.declarationList.declarations)
75
+ this.lowerConst(decl);
76
+ }
77
+ });
78
+ // Templates convert last, so composed components resolve against the
79
+ // full module context (structs, consts, helpers).
80
+ const components = findComponents(this.source, this.composeResolver);
81
+ const lowered = components.map(c => this.lowerComponent(c));
82
+ return {
83
+ source: this.source,
84
+ sourceText: this.sourceText,
85
+ imports,
86
+ composedImports: [...this.composedImports],
87
+ structs: this.structs,
88
+ decls: this.decls,
89
+ components: lowered,
90
+ };
91
+ }
92
+ readImports() {
93
+ const imports = [];
94
+ ts.forEachChild(this.source, node => {
95
+ if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier))
96
+ return;
97
+ const bindings = node.importClause?.namedBindings;
98
+ if (bindings && ts.isNamedImports(bindings)) {
99
+ for (const element of bindings.elements) {
100
+ this.namedImports.set(element.name.text, node.moduleSpecifier.text);
101
+ }
102
+ }
103
+ imports.push({
104
+ module: node.moduleSpecifier.text,
105
+ isArgon: node.moduleSpecifier.text === "@frontiers-labs/argon",
106
+ span: { start: node.getStart(this.source), end: node.getEnd() },
107
+ });
108
+ });
109
+ return imports;
110
+ }
111
+ // ── Component composition ───────────────────────────────────────────────────
112
+ composeResolver = (name, node) => {
113
+ const cached = this.resolvedComponents.get(name);
114
+ if (cached)
115
+ return cached;
116
+ const resolved = this.resolveComponent(name, node);
117
+ this.resolvedComponents.set(name, resolved);
118
+ return resolved;
119
+ };
120
+ resolveComponent(name, node) {
121
+ const composeFail = (message, hint) => {
122
+ throw new ArgonCompileError([diagnosticForNode("ARGON124", "all", message, node, this.source, hint)], this.source);
123
+ };
124
+ let sig = this.signatures.get(name);
125
+ let module;
126
+ if (!sig) {
127
+ const spec = this.namedImports.get(name);
128
+ if (spec === undefined) {
129
+ composeFail(`Unknown component '<${name}>'.`, "Declare it in this file, or import it from the module that declares it.");
130
+ }
131
+ if (!spec.startsWith(".")) {
132
+ composeFail(`'<${name}>' is imported from '${spec}'; composition needs a relative import pointing at the component's source file.`);
133
+ }
134
+ const file = resolveSourceFile(path.resolve(path.dirname(this.source.fileName), spec));
135
+ if (!file) {
136
+ composeFail(`Cannot find the source of '<${name}>' (imported from '${spec}').`, "Composition resolves './x.js' to './x.tsx' or './x.ts' relative to this file.");
137
+ }
138
+ const childText = fs.readFileSync(file, "utf8");
139
+ const childSource = ts.createSourceFile(file, childText, ts.ScriptTarget.Latest, true);
140
+ sig = componentSignatures(childSource).get(name);
141
+ if (!sig)
142
+ composeFail(`'${file}' does not declare an Argon component named '${name}'.`);
143
+ // Rust convention: each compiled file is a sibling module named after
144
+ // its source file, so cross-file children resolve as super::module::T.
145
+ module = path.basename(file, path.extname(file)).replace(/-/g, "_");
146
+ this.composedImports.add(spec);
147
+ }
148
+ const sameFile = module === undefined;
149
+ const props = sig.props.map(p => {
150
+ let base = UNKNOWN;
151
+ if (p.typeNode) {
152
+ try {
153
+ base = this.typeOf(p.typeNode, pascal(p.name));
154
+ }
155
+ catch (error) {
156
+ if (!(error instanceof LowerError))
157
+ throw error;
158
+ composeFail(`Prop '${p.name}' of '<${name}>' has a type this file cannot encode.`, sameFile ? undefined : "Cross-file composition supports scalar props and arrays of scalars; declare the child in this file to pass interface types.");
159
+ }
160
+ }
161
+ let def;
162
+ if (p.defaultExpr) {
163
+ // Cross-file defaults lower from the child's AST, so only literals
164
+ // are safe; anything richer makes the prop required for the parent.
165
+ def = sameFile ? this.tryExpr(p.defaultExpr) : literalExpr(p.defaultExpr);
166
+ if (def && def.type.kind === "unknown")
167
+ def = { ...def, type: base };
168
+ if (def && base.kind === "unknown")
169
+ base = def.type;
170
+ }
171
+ const optional = p.optional && !p.defaultExpr;
172
+ return {
173
+ name: p.name,
174
+ type: optional ? { kind: "option", inner: base } : base,
175
+ default: def,
176
+ optional,
177
+ required: !optional && def === undefined,
178
+ };
179
+ });
180
+ return { className: sig.className, tagName: sig.tagName, module, props };
181
+ }
182
+ tryExpr(node) {
183
+ try {
184
+ return this.expr(node);
185
+ }
186
+ catch (error) {
187
+ if (!(error instanceof LowerError))
188
+ throw error;
189
+ return undefined;
190
+ }
191
+ }
192
+ // Lower a marker that plays a composition role: a child prop value in
193
+ // attribute position, or the construction site of the child's shadow DOM.
194
+ composedExpr(meta, node) {
195
+ if (meta.kind === "propattr") {
196
+ const propType = meta.prop.type.kind === "option" ? meta.prop.type.inner : meta.prop.type;
197
+ let value;
198
+ try {
199
+ value = this.value(node, propType);
200
+ }
201
+ catch (error) {
202
+ if (!(error instanceof LowerError))
203
+ throw error;
204
+ value = this.opaqueExpr(node, error.diagnostic);
205
+ }
206
+ return { kind: "propattr", value, propType, type: STRING };
207
+ }
208
+ const props = meta.component.props.map(p => {
209
+ const provided = meta.provided.get(p.name);
210
+ let value;
211
+ if (provided === true) {
212
+ value = { kind: "bool", value: true, type: BOOLEAN };
213
+ }
214
+ else if (provided) {
215
+ try {
216
+ value = this.value(provided, p.type);
217
+ }
218
+ catch (error) {
219
+ if (!(error instanceof LowerError))
220
+ throw error;
221
+ value = this.opaqueExpr(provided, error.diagnostic);
222
+ }
223
+ }
224
+ else {
225
+ // Validated by the JSX pass: only optional or defaulted props omit.
226
+ value = p.default;
227
+ }
228
+ return { name: p.name, type: p.type, value };
229
+ });
230
+ return { kind: "construct", className: meta.component.className, module: meta.component.module, props, type: STRING };
231
+ }
232
+ // ── Diagnostics ────────────────────────────────────────────────────────────
233
+ fail(node, message, hint = SSR_HINT) {
234
+ throw new LowerError(diagnosticForNode("ARGON199", "ssr", message, node, this.source, hint));
235
+ }
236
+ opaqueExpr(node, diagnostic) {
237
+ return {
238
+ kind: "opaque",
239
+ text: node.getText(this.source),
240
+ freeIds: [...freeIdentifiers(node)],
241
+ diagnostic,
242
+ hasJsx: containsJsx(node),
243
+ type: UNKNOWN,
244
+ };
245
+ }
246
+ // ── Types ────────────────────────────────────────────────────────────────────
247
+ registerStruct(name, members) {
248
+ const existing = this.structs.get(name);
249
+ if (existing)
250
+ return existing;
251
+ const struct = { name, fields: [] };
252
+ this.structs.set(name, struct);
253
+ try {
254
+ for (const m of members) {
255
+ if (!ts.isPropertySignature(m) || !m.type || !ts.isIdentifier(m.name))
256
+ continue;
257
+ const type = this.typeOf(m.type, pascal(m.name.text));
258
+ struct.fields.push({ name: m.name.text, type: m.questionToken ? { kind: "option", inner: type } : type });
259
+ }
260
+ }
261
+ catch (error) {
262
+ // A field outside the supported type set: drop the struct, so uses of
263
+ // it surface as unsupported types instead of crashing the compile.
264
+ if (!(error instanceof LowerError))
265
+ throw error;
266
+ this.structs.delete(name);
267
+ throw error;
268
+ }
269
+ return struct;
270
+ }
271
+ typeOf(node, nameHint) {
272
+ if (node.kind === ts.SyntaxKind.StringKeyword)
273
+ return STRING;
274
+ if (node.kind === ts.SyntaxKind.NumberKeyword)
275
+ return NUMBER;
276
+ if (node.kind === ts.SyntaxKind.BooleanKeyword)
277
+ return BOOLEAN;
278
+ if (ts.isArrayTypeNode(node)) {
279
+ return { kind: "array", elem: this.typeOf(node.elementType, nameHint) };
280
+ }
281
+ if (ts.isTupleTypeNode(node)) {
282
+ const elems = node.elements.map(e => this.typeOf(e, nameHint));
283
+ if (elems.length > 0 && elems.every(e => sameType(e, elems[0]))) {
284
+ return { kind: "fixed", elem: elems[0], size: elems.length };
285
+ }
286
+ return { kind: "tuple", elems };
287
+ }
288
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
289
+ const struct = this.structs.get(node.typeName.text);
290
+ if (struct)
291
+ return { kind: "struct", name: struct.name };
292
+ }
293
+ if (ts.isTypeLiteralNode(node)) {
294
+ return { kind: "struct", name: this.registerStruct(nameHint, node.members).name };
295
+ }
296
+ this.fail(node, `unsupported Rust SSR type: ${node.getText(this.source)}`);
297
+ }
298
+ fieldType(type, name) {
299
+ if (type.kind !== "struct")
300
+ return UNKNOWN;
301
+ return this.structs.get(type.name)?.fields.find(f => f.name === name)?.type ?? UNKNOWN;
302
+ }
303
+ // ── Scopes ─────────────────────────────────────────────────────────────────
304
+ bind(name, binding) {
305
+ this.scopes[this.scopes.length - 1].bindings.set(name, binding);
306
+ }
307
+ resolve(name) {
308
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
309
+ const found = this.scopes[i].bindings.get(name);
310
+ if (found)
311
+ return found;
312
+ }
313
+ return undefined;
314
+ }
315
+ scoped(fn) {
316
+ this.scopes.push({ bindings: new Map(), narrows: new Set() });
317
+ try {
318
+ return fn();
319
+ }
320
+ finally {
321
+ this.scopes.pop();
322
+ }
323
+ }
324
+ // ── Flow narrowing ─────────────────────────────────────────────────────────
325
+ //
326
+ // Within a branch guarded by a presence test, the tested optional place is
327
+ // proven present, so reads of it unwrap instead of demanding a fallback.
328
+ narrowedBy(cond, taken, fn) {
329
+ const paths = [];
330
+ collectPresent(cond, taken, paths);
331
+ if (paths.length === 0)
332
+ return fn();
333
+ return this.scoped(() => {
334
+ const narrows = this.scopes[this.scopes.length - 1].narrows;
335
+ for (const path of paths)
336
+ narrows.add(path);
337
+ return fn();
338
+ });
339
+ }
340
+ isNarrowed(path) {
341
+ const root = path.split(".")[0];
342
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
343
+ if (this.scopes[i].narrows.has(path))
344
+ return true;
345
+ // A rebinding of the root (map items, params) shadows outer proofs.
346
+ if (this.scopes[i].bindings.has(root))
347
+ return false;
348
+ }
349
+ return false;
350
+ }
351
+ maybeUnwrap(expr) {
352
+ if (expr.type.kind !== "option")
353
+ return expr;
354
+ const path = placePath(expr);
355
+ if (path === undefined || !this.isNarrowed(path))
356
+ return expr;
357
+ return { kind: "unwrap", object: expr, type: expr.type.inner };
358
+ }
359
+ // ── Module declarations ────────────────────────────────────────────────────
360
+ fnSignature(fn) {
361
+ try {
362
+ const params = fn.parameters.map(p => ({
363
+ name: p.name.text,
364
+ type: p.type ? this.typeOf(p.type, pascal(p.name.text)) : NUMBER,
365
+ }));
366
+ const ret = fn.type ? this.typeOf(fn.type, "Ret") : NUMBER;
367
+ return { params, ret };
368
+ }
369
+ catch {
370
+ return { params: [], ret: UNKNOWN };
371
+ }
372
+ }
373
+ lowerFn(fn) {
374
+ const name = fn.name.text;
375
+ const sig = this.fns.get(name);
376
+ try {
377
+ if (!fn.body)
378
+ this.fail(fn, `function '${name}' has no body`);
379
+ const body = this.scoped(() => {
380
+ for (const param of sig.params)
381
+ this.bind(param.name, { space: "param", type: param.type });
382
+ return fn.body.statements.flatMap(s => this.stmt(s, sig.ret));
383
+ });
384
+ this.decls.push({
385
+ kind: "fn",
386
+ name,
387
+ params: sig.params,
388
+ ret: sig.ret,
389
+ body,
390
+ span: { start: fn.getStart(this.source), end: fn.getEnd() },
391
+ hasJsx: containsJsx(fn),
392
+ });
393
+ }
394
+ catch (error) {
395
+ if (!(error instanceof LowerError))
396
+ throw error;
397
+ this.opaqueDecls.add(name);
398
+ this.decls.push({ kind: "opaque", name, diagnostic: error.diagnostic, hasJsx: containsJsx(fn) });
399
+ }
400
+ }
401
+ lowerConst(decl) {
402
+ if (!ts.isIdentifier(decl.name) || !decl.initializer)
403
+ return;
404
+ const name = decl.name.text;
405
+ const init = decl.initializer;
406
+ if (ts.isTaggedTemplateExpression(init) && ts.isIdentifier(init.tag) && init.tag.text === "css") {
407
+ this.cssConsts.add(name);
408
+ this.decls.push({ kind: "css", name, text: templateText(init.template) });
409
+ return;
410
+ }
411
+ if (ts.isObjectLiteralExpression(init) && decl.type && isRecordType(decl.type)) {
412
+ try {
413
+ const valueType = this.typeOf(decl.type.typeArguments[1], "Value");
414
+ const entries = [];
415
+ for (const prop of init.properties) {
416
+ if (!ts.isPropertyAssignment(prop))
417
+ continue;
418
+ const key = ts.isStringLiteral(prop.name) || ts.isIdentifier(prop.name) ? prop.name.text : undefined;
419
+ if (key === undefined)
420
+ continue;
421
+ entries.push({ key, value: this.expr(prop.initializer, valueType) });
422
+ }
423
+ this.tables.set(name, valueType);
424
+ this.decls.push({
425
+ kind: "table",
426
+ name,
427
+ valueType,
428
+ entries,
429
+ span: { start: decl.getStart(this.source), end: decl.getEnd() },
430
+ hasJsx: containsJsx(init),
431
+ });
432
+ }
433
+ catch (error) {
434
+ if (!(error instanceof LowerError))
435
+ throw error;
436
+ this.opaqueDecls.add(name);
437
+ this.decls.push({ kind: "opaque", name, diagnostic: error.diagnostic, hasJsx: containsJsx(init) });
438
+ }
439
+ return;
440
+ }
441
+ this.opaqueDecls.add(name);
442
+ this.decls.push({
443
+ kind: "opaque",
444
+ name,
445
+ diagnostic: diagnosticForNode("ARGON199", "ssr", `unsupported Rust SSR const: ${name}`, decl, this.source, SSR_HINT),
446
+ hasJsx: containsJsx(decl),
447
+ });
448
+ }
449
+ // ── Components ─────────────────────────────────────────────────────────────
450
+ lowerComponent(component) {
451
+ this.composing = true;
452
+ try {
453
+ return this.lowerComponentBody(component);
454
+ }
455
+ finally {
456
+ this.composing = false;
457
+ }
458
+ }
459
+ lowerComponentBody(component) {
460
+ return this.scoped(() => {
461
+ const props = component.props.map(p => {
462
+ let type = UNKNOWN;
463
+ if (p.typeNode) {
464
+ try {
465
+ type = this.typeOf(p.typeNode, pascal(p.name));
466
+ // `name?:` with a default is always filled; without one the
467
+ // value may genuinely be absent.
468
+ if (p.optional && !p.defaultExpr)
469
+ type = { kind: "option", inner: type };
470
+ }
471
+ catch (error) {
472
+ if (!(error instanceof LowerError))
473
+ throw error;
474
+ }
475
+ }
476
+ let def;
477
+ if (p.defaultExpr) {
478
+ try {
479
+ def = this.expr(p.defaultExpr, type);
480
+ if (type.kind === "unknown")
481
+ type = def.type;
482
+ }
483
+ catch (error) {
484
+ if (!(error instanceof LowerError))
485
+ throw error;
486
+ def = this.opaqueExpr(p.defaultExpr, error.diagnostic);
487
+ }
488
+ }
489
+ this.bind(p.name, { space: "prop", type });
490
+ return { name: p.name, type, default: def };
491
+ });
492
+ const states = [];
493
+ const setup = [];
494
+ for (const stmt of component.prelude) {
495
+ for (const lowered of this.setupStmt(stmt, states))
496
+ setup.push(lowered);
497
+ }
498
+ const refs = setup.flatMap(s => (s.kind === "refdecl" ? [s.name] : []));
499
+ const processed = processTemplate(component.template);
500
+ this.checkRefAttrs(processed.nodes, new Set(refs), component);
501
+ const eventIndices = new Set(processed.bindings.map(b => b.exprIndex));
502
+ const exprs = processed.expressionNodes.map((node, i) => {
503
+ if (eventIndices.has(i)) {
504
+ return this.opaqueExpr(node, diagnosticForNode("ARGON198", "ssr", "Event handlers are CSR-only and never evaluated during SSR.", node, this.source));
505
+ }
506
+ const meta = processed.meta.get(i);
507
+ if (meta)
508
+ return this.composedExpr(meta, node);
509
+ try {
510
+ // Rendered values must be definite; an optional interpolation gets
511
+ // the unwrap-first diagnostic.
512
+ return this.value(node);
513
+ }
514
+ catch (error) {
515
+ if (!(error instanceof LowerError))
516
+ throw error;
517
+ return this.opaqueExpr(node, error.diagnostic);
518
+ }
519
+ });
520
+ const template = {
521
+ nodes: processed.nodes,
522
+ events: processed.bindings,
523
+ textBindings: processed.textBindings,
524
+ attrBindings: processed.attrBindings,
525
+ exprs,
526
+ exprSpans: processed.expressionNodes.map(n => ({ start: n.getStart(this.source), end: n.getEnd() })),
527
+ };
528
+ return {
529
+ className: component.className,
530
+ tagName: component.tagName,
531
+ props,
532
+ states,
533
+ refs,
534
+ setup,
535
+ template,
536
+ span: {
537
+ start: component.func.getStart(component.source),
538
+ end: component.func.getEnd(),
539
+ },
540
+ };
541
+ });
542
+ }
543
+ // ref={name} attributes must point at a ref() declared in the setup;
544
+ // a typo would otherwise silently yield a ref that never resolves.
545
+ checkRefAttrs(nodes, refs, component) {
546
+ for (const node of nodes) {
547
+ if (node.type !== "element")
548
+ continue;
549
+ const attr = node.attrs.find(a => a.name === "data-argon-ref");
550
+ if (attr && (attr.value === null || !refs.has(attr.value))) {
551
+ throw new ArgonCompileError([{
552
+ code: "ARGON122",
553
+ target: "all",
554
+ message: `ref '${attr.value}' is not declared in this component.`,
555
+ span: { start: component.func.getStart(component.source), end: component.func.getEnd() },
556
+ hint: `Declare it in the component setup: const ${attr.value} = ref();`,
557
+ }], this.source);
558
+ }
559
+ this.checkRefAttrs(node.children, refs, component);
560
+ }
561
+ }
562
+ // onMount(cb) / effect(cb) — CSR-only lifecycle, component setup only.
563
+ // Malformed calls fail the whole compile: turned opaque they would pass
564
+ // through to the JS output, where onMount/effect do not exist at runtime.
565
+ hookStmt(stmt) {
566
+ if (!ts.isExpressionStatement(stmt) || !ts.isCallExpression(stmt.expression))
567
+ return undefined;
568
+ const call = stmt.expression;
569
+ if (!ts.isIdentifier(call.expression))
570
+ return undefined;
571
+ const name = call.expression.text;
572
+ if (name !== "onMount" && name !== "effect")
573
+ return undefined;
574
+ const cb = call.arguments[0];
575
+ if (call.arguments.length !== 1 || !cb || (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb))) {
576
+ throw new ArgonCompileError([diagnosticForNode("ARGON123", "all", `${name}() takes a single callback function.`, call, this.source)], this.source);
577
+ }
578
+ return {
579
+ kind: "hook",
580
+ hook: name === "onMount" ? "mount" : "effect",
581
+ text: cb.getText(this.source),
582
+ freeIds: [...freeIdentifiers(cb)],
583
+ span: { start: call.getStart(this.source), end: call.getEnd() },
584
+ hasJsx: containsJsx(cb),
585
+ };
586
+ }
587
+ setupStmt(stmt, states) {
588
+ const hook = this.hookStmt(stmt);
589
+ if (hook)
590
+ return [hook];
591
+ try {
592
+ const lowered = this.stmt(stmt, UNKNOWN, states);
593
+ return lowered;
594
+ }
595
+ catch (error) {
596
+ if (!(error instanceof LowerError))
597
+ throw error;
598
+ const names = declaredNames(stmt);
599
+ for (const name of names)
600
+ this.bind(name, { space: "local", type: UNKNOWN });
601
+ return [{
602
+ kind: "opaque",
603
+ text: stmt.getText(this.source),
604
+ names,
605
+ freeIds: [...freeIdentifiers(stmt)],
606
+ diagnostic: error.diagnostic,
607
+ hasJsx: containsJsx(stmt),
608
+ }];
609
+ }
610
+ }
611
+ // ── Statements ─────────────────────────────────────────────────────────────
612
+ stmt(node, expectedReturn, states) {
613
+ if (ts.isVariableStatement(node)) {
614
+ return node.declarationList.declarations.map(d => this.varDecl(d, states));
615
+ }
616
+ if (ts.isReturnStatement(node)) {
617
+ if (!node.expression)
618
+ this.fail(node, "Rust SSR requires a return value.");
619
+ return [{ kind: "return", value: this.value(node.expression, expectedReturn) }];
620
+ }
621
+ if (ts.isIfStatement(node) || ts.isSwitchStatement(node)) {
622
+ // `states` is only threaded through component setup; control flow there
623
+ // would bypass the reactive-binding gating, so it stays in helpers.
624
+ if (states) {
625
+ this.fail(node, "if/switch statements are only supported inside helper functions; use a ternary here, or extract a helper.");
626
+ }
627
+ return [ts.isIfStatement(node) ? this.ifStmt(node, expectedReturn) : this.switchStmt(node, expectedReturn)];
628
+ }
629
+ this.fail(node, `Rust SSR supports const/let setup statements, if/switch, and return; found ${ts.SyntaxKind[node.kind]}.`);
630
+ }
631
+ // Lower a statement or block into a scoped list of StmtIR.
632
+ branch(node, expectedReturn) {
633
+ return this.scoped(() => {
634
+ if (ts.isBlock(node))
635
+ return node.statements.flatMap(s => this.stmt(s, expectedReturn));
636
+ return this.stmt(node, expectedReturn);
637
+ });
638
+ }
639
+ ifStmt(node, expectedReturn) {
640
+ const cond = this.value(node.expression);
641
+ const then = this.narrowedBy(cond, true, () => this.branch(node.thenStatement, expectedReturn));
642
+ const els = node.elseStatement
643
+ ? this.narrowedBy(cond, false, () => this.branch(node.elseStatement, expectedReturn))
644
+ : undefined;
645
+ return { kind: "if", cond, then, else: els };
646
+ }
647
+ // switch (disc) { case k: …; default: … } desugars to an if/else chain over
648
+ // equality with each case label. Cases do not fall through: each must break
649
+ // or return, and a trailing break is dropped.
650
+ switchStmt(node, expectedReturn) {
651
+ const disc = this.value(node.expression);
652
+ const clauses = node.caseBlock.clauses;
653
+ const bodyOf = (stmts) => this.scoped(() => stmts.filter(s => !ts.isBreakStatement(s)).flatMap(s => this.stmt(s, expectedReturn)));
654
+ // Group consecutive labels that fall through to a shared body (`case a:
655
+ // case b: return x`), and pull out the default body.
656
+ const groups = [];
657
+ let defaultBody;
658
+ let pending = [];
659
+ for (const clause of clauses) {
660
+ if (ts.isDefaultClause(clause)) {
661
+ defaultBody = bodyOf(clause.statements);
662
+ continue;
663
+ }
664
+ pending.push({ kind: "binary", op: "==", left: disc, right: this.value(clause.expression, disc.type), type: BOOLEAN });
665
+ const meaningful = clause.statements.filter(s => !ts.isBreakStatement(s));
666
+ if (meaningful.length > 0) {
667
+ groups.push({ tests: pending, body: bodyOf(clause.statements) });
668
+ pending = [];
669
+ }
670
+ }
671
+ if (groups.length === 0 && !defaultBody)
672
+ this.fail(node, "switch needs at least one case with a body.");
673
+ // Fold into an if/else chain, ORing each group's labels; default is the tail.
674
+ let chain = defaultBody;
675
+ for (let i = groups.length - 1; i >= 0; i--) {
676
+ const cond = groups[i].tests.reduce((acc, t) => ({ kind: "binary", op: "||", left: acc, right: t, type: BOOLEAN }));
677
+ chain = [{ kind: "if", cond, then: groups[i].body, else: chain }];
678
+ }
679
+ return chain.length === 1 ? chain[0] : { kind: "if", cond: { kind: "bool", value: true, type: BOOLEAN }, then: chain };
680
+ }
681
+ varDecl(decl, states) {
682
+ if (!decl.initializer)
683
+ this.fail(decl, "Rust SSR declarations need an initializer.");
684
+ // const el = ref() — CSR-only DOM handle, component setup only.
685
+ if (states && ts.isIdentifier(decl.name) && isRefCall(decl.initializer)) {
686
+ return { kind: "refdecl", name: decl.name.text };
687
+ }
688
+ // let count = state(initial) — reactive state, component setup only. A
689
+ // CSR-only initializer (e.g. Date.now()) is not silently emitted as a
690
+ // verbatim `state(...)` call: it stays reactive state (the client
691
+ // materializes it at hydration) but carries an SSR diagnostic so Rust
692
+ // rejects it with an actionable message.
693
+ const stateInit = states && stateCallArgument(decl.initializer);
694
+ if (stateInit && ts.isIdentifier(decl.name)) {
695
+ let initial;
696
+ try {
697
+ initial = this.expr(stateInit);
698
+ }
699
+ catch (error) {
700
+ if (!(error instanceof LowerError))
701
+ throw error;
702
+ initial = this.opaqueExpr(stateInit, diagnosticForNode("ARGON126", "ssr", `state() initializer '${stateInit.getText(this.source)}' is CSR-only and cannot be server-rendered.`, stateInit, this.source, "Initialize with a server-renderable placeholder and assign the real value in onMount()."));
703
+ }
704
+ states.push({ name: decl.name.text, type: initial.type, initial });
705
+ this.bind(decl.name.text, { space: "state", type: initial.type });
706
+ return { kind: "let", names: [decl.name.text], tuple: false, init: initial, type: initial.type, isState: true };
707
+ }
708
+ const init = this.expr(decl.initializer);
709
+ if (ts.isArrayBindingPattern(decl.name)) {
710
+ const names = decl.name.elements.map(e => ts.isBindingElement(e) && ts.isIdentifier(e.name) ? e.name.text : "_");
711
+ const elems = tupleElemTypes(init.type, names.length);
712
+ names.forEach((name, i) => {
713
+ if (name !== "_")
714
+ this.bind(name, { space: "local", type: elems[i] ?? UNKNOWN });
715
+ });
716
+ return { kind: "let", names, tuple: true, init, type: init.type, isState: false };
717
+ }
718
+ if (!ts.isIdentifier(decl.name)) {
719
+ this.fail(decl, "Rust SSR does not support this binding pattern.");
720
+ }
721
+ this.bind(decl.name.text, { space: "local", type: init.type });
722
+ return { kind: "let", names: [decl.name.text], tuple: false, init, type: init.type, isState: false };
723
+ }
724
+ // ── Expressions ────────────────────────────────────────────────────────────
725
+ // Lower an expression that must produce a definite value: optionals are
726
+ // rejected with a targeted fix instead of leaking Option<T> into Rust.
727
+ value(node, expected = UNKNOWN) {
728
+ const expr = this.expr(node, expected);
729
+ if (expr.type.kind === "option") {
730
+ this.fail(node, "Optional value used where a definite value is required.", "Unwrap it first: apply a fallback with '?? <default>' or test presence with '!== undefined'.");
731
+ }
732
+ return expr;
733
+ }
734
+ expr(node, expected = UNKNOWN) {
735
+ // Values stored into an optional slot are lowered against the inner type;
736
+ // the Rust backend wraps them in Some(...) at the assignment site.
737
+ if (expected.kind === "option")
738
+ expected = expected.inner;
739
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node)) {
740
+ return this.expr(node.expression, expected);
741
+ }
742
+ if (ts.isNumericLiteral(node))
743
+ return { kind: "num", text: node.text, type: NUMBER };
744
+ if (ts.isStringLiteral(node))
745
+ return { kind: "str", value: node.text, type: STRING };
746
+ if (ts.isNoSubstitutionTemplateLiteral(node))
747
+ return { kind: "str", value: node.text, type: STRING };
748
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
749
+ return { kind: "bool", value: true, type: BOOLEAN };
750
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
751
+ return { kind: "bool", value: false, type: BOOLEAN };
752
+ if (ts.isIdentifier(node))
753
+ return this.identifier(node);
754
+ if (ts.isPrefixUnaryExpression(node)) {
755
+ if (node.operator === ts.SyntaxKind.ExclamationToken) {
756
+ return { kind: "unary", op: "!", operand: this.value(node.operand), type: BOOLEAN };
757
+ }
758
+ if (node.operator === ts.SyntaxKind.MinusToken) {
759
+ const operand = this.value(node.operand);
760
+ if (operand.kind === "num")
761
+ return { kind: "num", text: `-${operand.text}`, type: NUMBER };
762
+ return { kind: "unary", op: "-", operand, type: NUMBER };
763
+ }
764
+ this.fail(node, `Rust SSR does not support unary '${ts.tokenToString(node.operator)}'.`);
765
+ }
766
+ if (ts.isPropertyAccessExpression(node)) {
767
+ if (node.name.text === "length") {
768
+ return { kind: "length", object: this.value(node.expression), type: NUMBER };
769
+ }
770
+ const object = this.value(node.expression);
771
+ return this.maybeUnwrap({
772
+ kind: "member",
773
+ object,
774
+ name: node.name.text,
775
+ type: this.fieldType(object.type, node.name.text),
776
+ });
777
+ }
778
+ if (ts.isElementAccessExpression(node)) {
779
+ if (ts.isIdentifier(node.expression) && this.tables.has(node.expression.text)) {
780
+ return {
781
+ kind: "lookup",
782
+ table: node.expression.text,
783
+ key: this.value(node.argumentExpression),
784
+ type: this.tables.get(node.expression.text),
785
+ };
786
+ }
787
+ const object = this.value(node.expression);
788
+ const index = this.value(node.argumentExpression);
789
+ return { kind: "index", object, index, type: indexedType(object.type, index) };
790
+ }
791
+ if (ts.isConditionalExpression(node)) {
792
+ const cond = this.value(node.condition);
793
+ const whenTrue = this.narrowedBy(cond, true, () => this.value(node.whenTrue, expected));
794
+ const whenFalse = this.narrowedBy(cond, false, () => this.value(node.whenFalse, expected));
795
+ return {
796
+ kind: "cond",
797
+ cond,
798
+ whenTrue,
799
+ whenFalse,
800
+ type: whenTrue.type.kind === "unknown" ? whenFalse.type : whenTrue.type,
801
+ };
802
+ }
803
+ if (ts.isBinaryExpression(node))
804
+ return this.binary(node);
805
+ if (ts.isTemplateExpression(node))
806
+ return this.template(node);
807
+ if (ts.isCallExpression(node))
808
+ return this.call(node);
809
+ if (ts.isArrayLiteralExpression(node)) {
810
+ const elems = node.elements.map(e => this.value(e));
811
+ const type = expected.kind === "fixed" || expected.kind === "array"
812
+ ? expected
813
+ : { kind: "tuple", elems: elems.map(e => e.type) };
814
+ return { kind: "arr", elems, type };
815
+ }
816
+ if (ts.isObjectLiteralExpression(node)) {
817
+ if (expected.kind !== "struct") {
818
+ this.fail(node, "untyped Rust SSR object literal", "Annotate the value with an interface type so the compiler can emit a struct.");
819
+ }
820
+ const struct = this.structs.get(expected.name);
821
+ const fields = node.properties.flatMap(prop => {
822
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
823
+ this.fail(prop, "Rust SSR does not support this object literal member.");
824
+ }
825
+ const fieldType = struct?.fields.find(f => f.name === prop.name.text)?.type ?? UNKNOWN;
826
+ // Optional fields accept optional values as-is; definite fields must
827
+ // receive definite values.
828
+ const value = fieldType.kind === "option"
829
+ ? this.expr(prop.initializer, fieldType)
830
+ : this.value(prop.initializer, fieldType);
831
+ return [{ name: prop.name.text, value }];
832
+ });
833
+ return { kind: "obj", struct: expected.name, fields, type: expected };
834
+ }
835
+ if (isJsxRoot(node))
836
+ return this.jsxTemplate(node);
837
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
838
+ this.fail(node, "Function values are CSR-only; Rust SSR cannot evaluate them in template interpolation.");
839
+ }
840
+ this.fail(node, `Rust SSR does not support ${ts.SyntaxKind[node.kind]}: ${node.getText(this.source)}`);
841
+ }
842
+ identifier(node) {
843
+ const binding = this.resolve(node.text);
844
+ if (binding)
845
+ return this.maybeUnwrap({ kind: "ref", name: node.text, space: binding.space, type: binding.type });
846
+ if (this.cssConsts.has(node.text))
847
+ return { kind: "ref", name: node.text, space: "global", type: STRING };
848
+ this.fail(node, `Rust SSR cannot evaluate '${node.text}' here.`);
849
+ }
850
+ // Nested JSX in a value position compiles to a string-producing template.
851
+ jsxTemplate(node) {
852
+ const tpl = jsxToTemplate(node, this.source, false, this.composing ? this.composeResolver : undefined);
853
+ const key = tpl.key ? this.value(tpl.key) : undefined;
854
+ if (tpl.expressionNodes.length === 0 && !key)
855
+ return { kind: "str", value: tpl.quasis[0], type: STRING, html: true };
856
+ return {
857
+ kind: "template",
858
+ quasis: tpl.quasis,
859
+ parts: tpl.expressionNodes.map((n, i) => {
860
+ const meta = tpl.meta.get(i);
861
+ return meta ? this.composedExpr(meta, n) : this.value(n);
862
+ }),
863
+ key,
864
+ type: STRING,
865
+ html: true,
866
+ };
867
+ }
868
+ binary(node) {
869
+ const ops = {
870
+ [ts.SyntaxKind.EqualsEqualsEqualsToken]: "==",
871
+ [ts.SyntaxKind.EqualsEqualsToken]: "==",
872
+ [ts.SyntaxKind.ExclamationEqualsEqualsToken]: "!=",
873
+ [ts.SyntaxKind.ExclamationEqualsToken]: "!=",
874
+ [ts.SyntaxKind.PlusToken]: "+",
875
+ [ts.SyntaxKind.MinusToken]: "-",
876
+ [ts.SyntaxKind.AsteriskToken]: "*",
877
+ [ts.SyntaxKind.SlashToken]: "/",
878
+ [ts.SyntaxKind.PercentToken]: "%",
879
+ [ts.SyntaxKind.LessThanToken]: "<",
880
+ [ts.SyntaxKind.GreaterThanToken]: ">",
881
+ [ts.SyntaxKind.LessThanEqualsToken]: "<=",
882
+ [ts.SyntaxKind.GreaterThanEqualsToken]: ">=",
883
+ [ts.SyntaxKind.AmpersandAmpersandToken]: "&&",
884
+ [ts.SyntaxKind.BarBarToken]: "||",
885
+ [ts.SyntaxKind.QuestionQuestionToken]: "??",
886
+ };
887
+ // Exponentiation lowers to a scalar Math.pow.
888
+ if (node.operatorToken.kind === ts.SyntaxKind.AsteriskAsteriskToken) {
889
+ return { kind: "math", fn: "pow", args: [this.value(node.left, NUMBER), this.value(node.right, NUMBER)], type: NUMBER };
890
+ }
891
+ const op = ops[node.operatorToken.kind];
892
+ if (!op) {
893
+ this.fail(node.operatorToken, `Rust SSR does not support '${ts.tokenToString(node.operatorToken.kind)}'.`);
894
+ }
895
+ // React semantics: `cond && value` renders nothing when falsy, instead of
896
+ // the string "false". This covers `<jsx/>` and any string-valued right
897
+ // side; emitting the raw `bool && String` would miscompile in Rust (E0308)
898
+ // and render "false" (never nothing) on the client. A boolean/numeric
899
+ // right side stays a real logical-AND.
900
+ if (op === "&&") {
901
+ const cond = this.value(node.left);
902
+ const whenTrue = this.narrowedBy(cond, true, () => isJsxRoot(node.right) ? this.jsxTemplate(node.right) : this.value(node.right));
903
+ if (whenTrue.type.kind === "string" || whenTrue.type.kind === "unknown") {
904
+ return {
905
+ kind: "cond",
906
+ cond,
907
+ whenTrue,
908
+ // The empty branch matches the right side's markup-ness: a markup
909
+ // right side stays wholly markup; a string right side stays text (so
910
+ // it is escaped whole, as a scalar value would be).
911
+ whenFalse: { kind: "str", value: "", type: STRING, html: isMarkup(whenTrue) },
912
+ type: STRING,
913
+ };
914
+ }
915
+ return { kind: "binary", op, left: cond, right: whenTrue, type: binaryType(op, cond, whenTrue) };
916
+ }
917
+ // Presence test of an optional: x !== undefined / x === undefined.
918
+ const isUndef = (n) => ts.isIdentifier(n) && n.text === "undefined";
919
+ if ((op === "==" || op === "!=") && (isUndef(node.left) || isUndef(node.right))) {
920
+ const object = this.expr(isUndef(node.left) ? node.right : node.left);
921
+ return { kind: "exists", object, present: op === "!=", type: BOOLEAN };
922
+ }
923
+ // ?? unwraps an optional left side; everything else needs definite values.
924
+ const left = op === "??" ? this.expr(node.left) : this.value(node.left);
925
+ // Short-circuit narrowing: the right side of || runs only when the left
926
+ // is false. (&& is handled above.)
927
+ const lowerRight = () => this.value(node.right);
928
+ const right = op === "||" ? this.narrowedBy(left, false, lowerRight) : lowerRight();
929
+ return { kind: "binary", op, left, right, type: binaryType(op, left, right) };
930
+ }
931
+ template(node) {
932
+ const quasis = [node.head.text];
933
+ const parts = [];
934
+ for (const span of node.templateSpans) {
935
+ parts.push(this.value(span.expression));
936
+ quasis.push(span.literal.text);
937
+ }
938
+ return { kind: "template", quasis, parts, type: STRING };
939
+ }
940
+ call(node) {
941
+ const callee = node.expression;
942
+ // state(x) outside a declaration evaluates to its initial value.
943
+ if (ts.isIdentifier(callee) && callee.text === "state") {
944
+ if (!node.arguments[0])
945
+ this.fail(node, "state() needs an initial value.");
946
+ return this.expr(node.arguments[0]);
947
+ }
948
+ // unsafeHtml(x): render x as raw markup instead of escaping it.
949
+ if (ts.isIdentifier(callee) && callee.text === "unsafeHtml") {
950
+ if (!node.arguments[0])
951
+ this.fail(node, "unsafeHtml() needs a string argument.");
952
+ return { kind: "raw", inner: this.value(node.arguments[0], STRING), type: STRING };
953
+ }
954
+ if (ts.isIdentifier(callee)) {
955
+ const sig = this.fns.get(callee.text);
956
+ if (!sig)
957
+ this.fail(node, `Rust SSR cannot call '${callee.text}'.`);
958
+ if (this.opaqueDecls.has(callee.text)) {
959
+ this.fail(node, `'${callee.text}' uses CSR-only code, so Rust SSR cannot call it.`);
960
+ }
961
+ const args = node.arguments.map((a, i) => this.value(a, sig.params[i]?.type ?? UNKNOWN));
962
+ return { kind: "call", fn: callee.text, args, type: sig.ret };
963
+ }
964
+ if (ts.isPropertyAccessExpression(callee)) {
965
+ const method = callee.name.text;
966
+ if (ts.isIdentifier(callee.expression) && callee.expression.text === "Math") {
967
+ // Spread min/max keeps the array-reducing form; everything else is a
968
+ // scalar Math call over definite numbers.
969
+ if ((method === "min" || method === "max") && node.arguments[0] && ts.isSpreadElement(node.arguments[0])) {
970
+ return { kind: "minmax", op: method, array: this.sequence(node.arguments[0].expression), type: NUMBER };
971
+ }
972
+ const spec = MATH_FNS[method];
973
+ if (!spec)
974
+ this.fail(node, `Rust SSR does not support Math.${method}.`);
975
+ const args = node.arguments.map(a => this.value(a, NUMBER));
976
+ const ok = spec.arity === "1+" ? args.length >= 1 : spec.arity === "2" ? args.length === 2 : args.length === spec.arity;
977
+ if (!ok) {
978
+ const want = spec.arity === "1+" ? "one or more arguments" : spec.arity === "2" ? "two arguments" : `${spec.arity} argument(s)`;
979
+ this.fail(node, `Math.${method} takes ${want}.`);
980
+ }
981
+ return { kind: "math", fn: method, args, type: NUMBER };
982
+ }
983
+ if (method === "toFixed") {
984
+ const digits = node.arguments[0];
985
+ return {
986
+ kind: "toFixed",
987
+ value: this.value(callee.expression),
988
+ digits: digits && ts.isNumericLiteral(digits) ? Number(digits.text) : 0,
989
+ type: STRING,
990
+ };
991
+ }
992
+ if (method === "map") {
993
+ return this.mapCall(node, callee);
994
+ }
995
+ if (method === "join") {
996
+ const sep = node.arguments[0];
997
+ const array = this.sequence(callee.expression);
998
+ return {
999
+ kind: "join",
1000
+ array,
1001
+ sep: sep && ts.isStringLiteral(sep) ? sep.text : ",",
1002
+ type: STRING,
1003
+ };
1004
+ }
1005
+ // Whitelisted string/array methods, dispatched by receiver type.
1006
+ if (STRING_METHODS[method] || ARRAY_METHODS[method]) {
1007
+ const object = this.sequence(callee.expression);
1008
+ if (object.type.kind === "string" && STRING_METHODS[method]) {
1009
+ return this.methodCall("strmethod", method, object, node, STRING_METHODS[method]);
1010
+ }
1011
+ if ((object.type.kind === "array" || object.type.kind === "fixed") && ARRAY_METHODS[method]) {
1012
+ return this.arrMethod(method, object, node);
1013
+ }
1014
+ }
1015
+ this.fail(node, `Rust SSR does not support '.${method}()' calls.`);
1016
+ }
1017
+ this.fail(node, `Rust SSR does not support this call: ${node.getText(this.source)}`);
1018
+ }
1019
+ // Lower a whitelisted method call: validate arity, lower each argument
1020
+ // against its declared kind, and tag the result type.
1021
+ methodCall(kind, method, object, node, spec) {
1022
+ const [min, max] = spec.args;
1023
+ if (node.arguments.length < min || node.arguments.length > max) {
1024
+ const want = min === max ? `${min}` : `${min}–${max}`;
1025
+ this.fail(node, `.${method}() takes ${want} argument(s).`);
1026
+ }
1027
+ const args = node.arguments.map((a, i) => this.value(a, spec.argKinds[i] === "number" ? NUMBER : STRING));
1028
+ const type = spec.result === "boolean" ? BOOLEAN : spec.result === "number" ? NUMBER : STRING;
1029
+ return { kind, method, object, args, type };
1030
+ }
1031
+ // Whitelisted array methods. Predicate methods bind a single element param
1032
+ // and lower the arrow body within its scope.
1033
+ arrMethod(method, object, node) {
1034
+ const elem = elemType(object.type);
1035
+ const spec = ARRAY_METHODS[method];
1036
+ if (spec.kind === "predicate") {
1037
+ const cb = node.arguments[0];
1038
+ if (!cb || !ts.isArrowFunction(cb))
1039
+ this.fail(node, `.${method}() requires an arrow function predicate.`);
1040
+ const { param, body } = this.predicate(cb, elem);
1041
+ const type = method === "filter" ? { kind: "array", elem }
1042
+ : method === "find" ? { kind: "option", inner: elem }
1043
+ : BOOLEAN;
1044
+ return { kind: "arrmethod", method, object, param, body, args: [], type };
1045
+ }
1046
+ if (spec.kind === "value") {
1047
+ const v = node.arguments[0];
1048
+ if (!v)
1049
+ this.fail(node, `.${method}() needs a value argument.`);
1050
+ const arg = this.value(v, elem);
1051
+ return { kind: "arrmethod", method, object, args: [arg], type: method === "includes" ? BOOLEAN : NUMBER };
1052
+ }
1053
+ // slice
1054
+ const args = node.arguments.map(a => this.value(a, NUMBER));
1055
+ if (args.length < 1 || args.length > 2)
1056
+ this.fail(node, ".slice() takes 1–2 arguments.");
1057
+ return { kind: "arrmethod", method, object, args, type: { kind: "array", elem } };
1058
+ }
1059
+ predicate(cb, elem) {
1060
+ const first = cb.parameters[0];
1061
+ if (!first || !ts.isIdentifier(first.name)) {
1062
+ this.fail(cb, "Predicate callbacks need a single named parameter.");
1063
+ }
1064
+ const param = first.name.text;
1065
+ return this.scoped(() => {
1066
+ this.bind(param, { space: "local", type: elem });
1067
+ let body;
1068
+ if (ts.isBlock(cb.body)) {
1069
+ const ret = cb.body.statements.find(ts.isReturnStatement);
1070
+ if (!ret || !ret.expression)
1071
+ this.fail(cb, "Predicate callbacks must return a value.");
1072
+ body = this.value(ret.expression);
1073
+ }
1074
+ else {
1075
+ body = this.value(cb.body);
1076
+ }
1077
+ return { param, body };
1078
+ });
1079
+ }
1080
+ // An array literal in a sequence position (map/join/spread) is a fixed
1081
+ // array, not the tuple it would be as a standalone value.
1082
+ sequence(node) {
1083
+ const expr = this.value(node);
1084
+ if (expr.kind === "arr" && expr.type.kind === "tuple" && expr.elems.length > 0) {
1085
+ const elems = expr.type.elems;
1086
+ if (elems.every(e => sameType(e, elems[0]))) {
1087
+ return { ...expr, type: { kind: "fixed", elem: elems[0], size: elems.length } };
1088
+ }
1089
+ }
1090
+ return expr;
1091
+ }
1092
+ mapCall(node, callee) {
1093
+ const array = this.sequence(callee.expression);
1094
+ const cb = node.arguments[0];
1095
+ if (!cb || !ts.isArrowFunction(cb)) {
1096
+ this.fail(node, "Rust SSR map() requires an arrow function callback.");
1097
+ }
1098
+ const elem = elemType(array.type);
1099
+ return this.scoped(() => {
1100
+ let item;
1101
+ const first = cb.parameters[0];
1102
+ if (first && ts.isArrayBindingPattern(first.name)) {
1103
+ const names = first.name.elements.map(e => ts.isBindingElement(e) && ts.isIdentifier(e.name) ? e.name.text : "_");
1104
+ const elems = tupleElemTypes(elem, names.length);
1105
+ names.forEach((name, i) => {
1106
+ if (name !== "_")
1107
+ this.bind(name, { space: "local", type: elems[i] ?? UNKNOWN });
1108
+ });
1109
+ item = names;
1110
+ }
1111
+ else if (first && ts.isIdentifier(first.name)) {
1112
+ this.bind(first.name.text, { space: "local", type: elem });
1113
+ item = first.name.text;
1114
+ }
1115
+ else {
1116
+ this.fail(cb, "Rust SSR map() callbacks need a named element parameter.");
1117
+ }
1118
+ let indexVar;
1119
+ const second = cb.parameters[1];
1120
+ if (second && ts.isIdentifier(second.name)) {
1121
+ indexVar = second.name.text;
1122
+ this.bind(indexVar, { space: "local", type: NUMBER });
1123
+ }
1124
+ const stmts = [];
1125
+ let result;
1126
+ if (ts.isBlock(cb.body)) {
1127
+ for (const stmt of cb.body.statements) {
1128
+ if (ts.isReturnStatement(stmt)) {
1129
+ if (!stmt.expression)
1130
+ this.fail(stmt, "Rust SSR map() callbacks must return a value.");
1131
+ result = this.value(stmt.expression);
1132
+ }
1133
+ else {
1134
+ for (const s of this.stmt(stmt, UNKNOWN))
1135
+ stmts.push(s);
1136
+ }
1137
+ }
1138
+ if (!result)
1139
+ this.fail(cb, "Rust SSR map() callbacks must return a value.");
1140
+ }
1141
+ else {
1142
+ result = this.value(cb.body);
1143
+ }
1144
+ return {
1145
+ kind: "map",
1146
+ array,
1147
+ item,
1148
+ indexVar,
1149
+ body: { stmts, result },
1150
+ type: { kind: "array", elem: result.type },
1151
+ };
1152
+ });
1153
+ }
1154
+ }
1155
+ // ── Narrowing helpers ────────────────────────────────────────────────────────
1156
+ // Paths proven present when `cond` evaluates to `taken`, flowing through
1157
+ // `!`, `&&` (taken branch) and `||` (not-taken branch).
1158
+ function collectPresent(cond, taken, out) {
1159
+ if (cond.kind === "exists") {
1160
+ if (cond.present === taken && cond.object.type.kind === "option") {
1161
+ const path = placePath(cond.object);
1162
+ if (path !== undefined)
1163
+ out.push(path);
1164
+ }
1165
+ }
1166
+ else if (cond.kind === "unary" && cond.op === "!") {
1167
+ collectPresent(cond.operand, !taken, out);
1168
+ }
1169
+ else if (cond.kind === "binary" && ((cond.op === "&&" && taken) || (cond.op === "||" && !taken))) {
1170
+ collectPresent(cond.left, taken, out);
1171
+ collectPresent(cond.right, taken, out);
1172
+ }
1173
+ }
1174
+ // Canonical path of a narrowable place: refs and member chains only.
1175
+ function placePath(e) {
1176
+ if (e.kind === "ref")
1177
+ return e.name;
1178
+ if (e.kind === "unwrap")
1179
+ return placePath(e.object);
1180
+ if (e.kind === "member") {
1181
+ const base = placePath(e.object);
1182
+ return base === undefined ? undefined : `${base}.${e.name}`;
1183
+ }
1184
+ return undefined;
1185
+ }
1186
+ // ── Type helpers ─────────────────────────────────────────────────────────────
1187
+ function sameType(a, b) {
1188
+ if (a.kind !== b.kind)
1189
+ return false;
1190
+ if (a.kind === "struct" && b.kind === "struct")
1191
+ return a.name === b.name;
1192
+ if ((a.kind === "array" || a.kind === "fixed") && (b.kind === "array" || b.kind === "fixed")) {
1193
+ return a.kind === b.kind && sameType(a.elem, b.elem);
1194
+ }
1195
+ return true;
1196
+ }
1197
+ function elemType(type) {
1198
+ if (type.kind === "array" || type.kind === "fixed")
1199
+ return type.elem;
1200
+ return UNKNOWN;
1201
+ }
1202
+ function tupleElemTypes(type, count) {
1203
+ if (type.kind === "tuple")
1204
+ return type.elems;
1205
+ if (type.kind === "fixed" || type.kind === "array")
1206
+ return Array(count).fill(type.elem);
1207
+ return Array(count).fill(UNKNOWN);
1208
+ }
1209
+ function indexedType(object, index) {
1210
+ if (object.kind === "array" || object.kind === "fixed")
1211
+ return object.elem;
1212
+ if (object.kind === "tuple" && index.kind === "num")
1213
+ return object.elems[Number(index.text)] ?? UNKNOWN;
1214
+ return UNKNOWN;
1215
+ }
1216
+ function binaryType(op, left, right) {
1217
+ if (op === "==" || op === "!=" || op === "<" || op === ">" || op === "<=" || op === ">=" || op === "&&") {
1218
+ return BOOLEAN;
1219
+ }
1220
+ if (op === "||")
1221
+ return left.type.kind === "boolean" ? BOOLEAN : left.type;
1222
+ // The right side is policed to be definite, so ?? always yields a value.
1223
+ if (op === "??")
1224
+ return left.type.kind === "option" ? right.type : left.type;
1225
+ if (op === "+") {
1226
+ if (left.type.kind === "string" || right.type.kind === "string")
1227
+ return STRING;
1228
+ return NUMBER;
1229
+ }
1230
+ return NUMBER;
1231
+ }
1232
+ // ── AST helpers ──────────────────────────────────────────────────────────────
1233
+ function pascal(name) {
1234
+ return name.charAt(0).toUpperCase() + name.slice(1);
1235
+ }
1236
+ function resolveSourceFile(base) {
1237
+ const stripped = base.replace(/\.(js|tsx|ts)$/, "");
1238
+ for (const ext of [".tsx", ".ts"]) {
1239
+ if (fs.existsSync(stripped + ext))
1240
+ return stripped + ext;
1241
+ }
1242
+ return undefined;
1243
+ }
1244
+ // The literal subset safe to lower from another file's AST (cross-file
1245
+ // defaults): diagnostics and source text must never reference foreign nodes.
1246
+ function literalExpr(node) {
1247
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
1248
+ return { kind: "str", value: node.text, type: STRING };
1249
+ }
1250
+ if (ts.isNumericLiteral(node))
1251
+ return { kind: "num", text: node.text, type: NUMBER };
1252
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
1253
+ return { kind: "bool", value: true, type: BOOLEAN };
1254
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
1255
+ return { kind: "bool", value: false, type: BOOLEAN };
1256
+ if (ts.isPrefixUnaryExpression(node) &&
1257
+ node.operator === ts.SyntaxKind.MinusToken &&
1258
+ ts.isNumericLiteral(node.operand)) {
1259
+ return { kind: "num", text: `-${node.operand.text}`, type: NUMBER };
1260
+ }
1261
+ if (ts.isArrayLiteralExpression(node)) {
1262
+ const elems = [];
1263
+ for (const element of node.elements) {
1264
+ const lowered = literalExpr(element);
1265
+ if (!lowered)
1266
+ return undefined;
1267
+ elems.push(lowered);
1268
+ }
1269
+ return { kind: "arr", elems, type: UNKNOWN };
1270
+ }
1271
+ return undefined;
1272
+ }
1273
+ function isRefCall(init) {
1274
+ return (ts.isCallExpression(init) &&
1275
+ ts.isIdentifier(init.expression) &&
1276
+ init.expression.text === "ref" &&
1277
+ init.arguments.length === 0);
1278
+ }
1279
+ function stateCallArgument(init) {
1280
+ if (ts.isCallExpression(init) &&
1281
+ ts.isIdentifier(init.expression) &&
1282
+ init.expression.text === "state" &&
1283
+ init.arguments[0]) {
1284
+ return init.arguments[0];
1285
+ }
1286
+ return undefined;
1287
+ }
1288
+ function declaredNames(stmt) {
1289
+ const names = [];
1290
+ if (ts.isVariableStatement(stmt)) {
1291
+ for (const decl of stmt.declarationList.declarations) {
1292
+ if (ts.isIdentifier(decl.name))
1293
+ names.push(decl.name.text);
1294
+ else if (ts.isArrayBindingPattern(decl.name)) {
1295
+ for (const el of decl.name.elements) {
1296
+ if (ts.isBindingElement(el) && ts.isIdentifier(el.name))
1297
+ names.push(el.name.text);
1298
+ }
1299
+ }
1300
+ }
1301
+ }
1302
+ return names;
1303
+ }
1304
+ function templateText(tpl) {
1305
+ if (ts.isNoSubstitutionTemplateLiteral(tpl))
1306
+ return tpl.text;
1307
+ let out = tpl.head.text;
1308
+ for (const span of tpl.templateSpans)
1309
+ out += span.literal.text;
1310
+ return out;
1311
+ }
1312
+ function isRecordType(type) {
1313
+ return (ts.isTypeReferenceNode(type) &&
1314
+ ts.isIdentifier(type.typeName) &&
1315
+ type.typeName.text === "Record" &&
1316
+ type.typeArguments?.length === 2);
1317
+ }
1318
+ export function freeIdentifiers(node) {
1319
+ const ids = new Set();
1320
+ const visit = (n) => {
1321
+ if (ts.isIdentifier(n)) {
1322
+ if (ts.isPropertyAccessExpression(n.parent) && n.parent.name === n)
1323
+ return;
1324
+ if (ts.isPropertyAssignment(n.parent) && n.parent.name === n)
1325
+ return;
1326
+ if (ts.isBindingElement(n.parent) && n.parent.name === n)
1327
+ return;
1328
+ if (ts.isVariableDeclaration(n.parent) && n.parent.name === n)
1329
+ return;
1330
+ if (ts.isParameter(n.parent) && n.parent.name === n)
1331
+ return;
1332
+ if (ts.isFunctionDeclaration(n.parent) && n.parent.name === n)
1333
+ return;
1334
+ ids.add(n.text);
1335
+ }
1336
+ ts.forEachChild(n, visit);
1337
+ };
1338
+ visit(node);
1339
+ return ids;
1340
+ }
1341
+ //# sourceMappingURL=lower.js.map