@lango-project/lango-js 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4226 @@
1
+ import path from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { Decimal, runtimeSource } from "./runtime.mjs";
4
+
5
+ export class LangoError extends Error {
6
+ constructor(code, line, message, sourceName = null, column = 1, endColumn = column) {
7
+ super(`${code} at line ${line}: ${message}`);
8
+ this.code = code;
9
+ this.severity = "error";
10
+ this.sourceName = sourceName;
11
+ this.span = {
12
+ start: { line, column },
13
+ end: { line, column: endColumn },
14
+ };
15
+ }
16
+ }
17
+
18
+ export function formatDiagnostics(error) {
19
+ const diagnostics = error instanceof LangoError
20
+ ? error.diagnostics ?? [error]
21
+ : [error];
22
+ return diagnostics.map((diagnostic) => {
23
+ if (!(diagnostic instanceof LangoError)) return String(diagnostic);
24
+ const source = diagnostic.sourceName ?? "<source>";
25
+ const { line, column } = diagnostic.span.start;
26
+ const related = (diagnostic.related ?? []).map((item) =>
27
+ `\n related ${source}:${item.span.start.line}:${item.span.start.column}: ${item.message}`,
28
+ ).join("");
29
+ return `${source}:${line}:${column}: ${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}${related}`;
30
+ }).join("\n");
31
+ }
32
+
33
+ export async function buildFile(inputPath, outputPath, seen = new Set(), includeTests = false, profile = {}) {
34
+ const decimalProfile = validateDecimalProfile(profile);
35
+ if (seen.size === 0)
36
+ await validateModuleInitializerCycles(inputPath, outputPath);
37
+ if (seen.has(inputPath)) return;
38
+ seen.add(inputPath);
39
+
40
+ const source = await readFile(inputPath, "utf8");
41
+ const imports = collectImports(source, inputPath, outputPath);
42
+ for (const item of imports)
43
+ await buildFile(item.sourcePath, item.outputPath, seen, includeTests, decimalProfile);
44
+ for (const item of imports) {
45
+ const importedProgram = new Parser(
46
+ await readFile(item.sourcePath, "utf8"),
47
+ item.sourcePath,
48
+ ).parse();
49
+ for (const selection of item.selections) {
50
+ const target =
51
+ selection.kind === "class"
52
+ ? importedProgram.classes.find(
53
+ (candidate) =>
54
+ candidate.name === selection.sourceName && candidate.exported,
55
+ )
56
+ : selection.kind === "function"
57
+ ? importedProgram.functions.find(
58
+ (candidate) =>
59
+ candidate.name === selection.sourceName && candidate.exported,
60
+ )
61
+ : importedProgram.globals.find(
62
+ (candidate) =>
63
+ candidate.name === selection.sourceName && candidate.exported,
64
+ );
65
+ if (!target)
66
+ throw new LangoError(
67
+ "LNG1227",
68
+ 1,
69
+ `${inputPath}: imported ${selection.kind} ${selection.sourceName} is not exported by ${item.sourcePath}`,
70
+ );
71
+ if (selection.kind === "function" && !sameParameters(selection.parameters, target.parameters))
72
+ throw new LangoError(
73
+ "LNG1230",
74
+ 1,
75
+ `${inputPath}: import signature for ${selection.sourceName} does not match ${item.sourcePath}`,
76
+ );
77
+ if (selection.kind === "class")
78
+ validateImportedClassInterface(
79
+ selection,
80
+ target,
81
+ importedProgram,
82
+ inputPath,
83
+ item.sourcePath,
84
+ );
85
+ }
86
+ }
87
+
88
+ const compiled = compile(source, inputPath, imports, includeTests, decimalProfile);
89
+ const output = compiled.javascript.replace(
90
+ "//# sourceMappingURL=main.mjs.map",
91
+ `//# sourceMappingURL=${path.basename(outputPath)}.map`,
92
+ );
93
+ const directory = path.dirname(outputPath);
94
+
95
+ await mkdir(path.join(directory, ".lango"), { recursive: true });
96
+ await writeFile(outputPath, output, "utf8");
97
+ await writeFile(
98
+ `${outputPath}.map`,
99
+ JSON.stringify({
100
+ version: 3,
101
+ file: path.basename(outputPath),
102
+ sources: [path.basename(inputPath)],
103
+ sourcesContent: [source],
104
+ names: [],
105
+ mappings: sourceMapMappings(compiled.program, output),
106
+ }) + "\n",
107
+ );
108
+ await writeFile(path.join(directory, ".lango/runtime.mjs"), runtimeSource(decimalProfile));
109
+ await writeFile(
110
+ path.join(directory, ".lango/bignumber.mjs"),
111
+ await readFile(new URL("./vendor/bignumber.mjs", import.meta.url), "utf8"),
112
+ "utf8",
113
+ );
114
+ }
115
+
116
+ export function compile(source, sourceName = "<source>", imports = [], includeTests = false, profile = {}) {
117
+ validateDecimalProfile(profile);
118
+ const tabs = tabDiagnostics(source, sourceName);
119
+ if (tabs.length) throw tabs[0];
120
+ const parser = new Parser(source, sourceName);
121
+ const program = parser.parse();
122
+ try {
123
+ validate(program, imports);
124
+ } catch (error) {
125
+ if (error instanceof LangoError && error.sourceName === null)
126
+ error.sourceName = sourceName;
127
+ if (error instanceof LangoError && error.diagnostics)
128
+ error.diagnostics.forEach((diagnostic) => {
129
+ if (diagnostic.sourceName === null) diagnostic.sourceName = sourceName;
130
+ });
131
+ throw error;
132
+ }
133
+ return { javascript: emit(program, imports, includeTests), program };
134
+ }
135
+
136
+ export function diagnose(source, sourceName = "<source>", imports = [], includeTests = false, profile = {}) {
137
+ const tabs = tabDiagnostics(source, sourceName);
138
+ if (tabs.length) return tabs;
139
+ try {
140
+ compile(source, sourceName, imports, includeTests, profile);
141
+ return [];
142
+ } catch (error) {
143
+ if (error instanceof LangoError) return error.diagnostics ?? [error];
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ function tabDiagnostics(source, sourceName) {
149
+ const diagnostics = [];
150
+ source.split(/\r?\n/).forEach((line, index) => {
151
+ for (let column = line.indexOf("\t"); column !== -1; column = line.indexOf("\t", column + 1))
152
+ diagnostics.push(new LangoError(
153
+ "LNG1001",
154
+ index + 1,
155
+ "TAB characters are not allowed",
156
+ sourceName,
157
+ column + 1,
158
+ column + 2,
159
+ ));
160
+ });
161
+ return diagnostics;
162
+ }
163
+
164
+ function validateDecimalProfile(profile) {
165
+ const decimal_division_scale = profile.decimal_division_scale ?? 6;
166
+ const decimal_division_rounding = profile.decimal_division_rounding ?? Decimal.Rounding.HalfEven;
167
+ if (!Number.isInteger(decimal_division_scale) || decimal_division_scale < 0)
168
+ throw new LangoError("LNG1262", 1, "decimal_division_scale must be a non-negative integer");
169
+ if (!Object.values(Decimal.Rounding).includes(decimal_division_rounding))
170
+ throw new LangoError("LNG1263", 1, `unknown decimal_division_rounding ${decimal_division_rounding}`);
171
+ return { decimal_division_scale, decimal_division_rounding };
172
+ }
173
+
174
+ const BASE64_VLQ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
175
+
176
+ function encodeVlq(value) {
177
+ let remaining = value < 0 ? ((-value) << 1) | 1 : value << 1;
178
+ let result = "";
179
+ do {
180
+ let digit = remaining & 31;
181
+ remaining >>>= 5;
182
+ if (remaining) digit |= 32;
183
+ result += BASE64_VLQ[digit];
184
+ } while (remaining);
185
+ return result;
186
+ }
187
+
188
+ function sourceMapMappings(program, javascript) {
189
+ const sourceLines = new Map([
190
+ ...program.functions.map((item) => [`function:${item.name}`, item.line]),
191
+ ...program.classes.map((item) => [`class:${item.name}`, item.line]),
192
+ ...program.globals.map((item) => [`global:${item.name}`, item.line]),
193
+ ]);
194
+ let priorSourceLine = 0;
195
+ return javascript.split("\n").map((line) => {
196
+ const functionMatch = /^(?:export )?function ([A-Za-z_]\w*)\(/.exec(line);
197
+ const classMatch = /^(?:export )?class ([A-Za-z_]\w*)\b/.exec(line);
198
+ const globalMatch = /^(?:export )?let (\$?[A-Za-z_]\w*)\b/.exec(line);
199
+ const sourceLine = sourceLines.get(
200
+ functionMatch ? `function:${functionMatch[1]}`
201
+ : classMatch ? `class:${classMatch[1]}`
202
+ : globalMatch ? `global:${globalMatch[1]}`
203
+ : "",
204
+ );
205
+ if (!sourceLine) return "";
206
+ const segment = `${encodeVlq(0)}${encodeVlq(0)}${encodeVlq(sourceLine - 1 - priorSourceLine)}${encodeVlq(0)}`;
207
+ priorSourceLine = sourceLine - 1;
208
+ return segment;
209
+ }).join(";");
210
+ }
211
+
212
+ export function formatSource(source, sourceName = "<source>") {
213
+ const tabs = tabDiagnostics(source, sourceName);
214
+ if (tabs.length) throw tabs[0];
215
+ new Parser(source, sourceName).parse();
216
+ const lines = source.split(/\r?\n/).map((line) => formatLine(line));
217
+ while (lines[0] === "") lines.shift();
218
+ while (lines.at(-1) === "") lines.pop();
219
+ const formatted = [];
220
+ for (const line of lines)
221
+ if (line !== "" || formatted.at(-1) !== "") formatted.push(line);
222
+ return formatted.length ? `${formatted.join("\n")}\n` : "";
223
+ }
224
+
225
+ function formatLine(line) {
226
+ const trimmed = line.trimEnd();
227
+ const code = stripComment(trimmed);
228
+ if (code.length === trimmed.length || code.trim() === "") return trimmed;
229
+ return `${code.trimEnd()} ${trimmed.slice(code.length).trim()}`;
230
+ }
231
+
232
+ class Parser {
233
+ constructor(source, name) {
234
+ this.lines = source.split(/\r?\n/).map((line) => stripComment(line).trimEnd());
235
+ this.name = name;
236
+ this.i = 0;
237
+ }
238
+ parse() {
239
+ const functions = [];
240
+ const classes = [];
241
+ const exports = [];
242
+ const globals = [];
243
+ const tests = [];
244
+ const jsImports = [];
245
+ let declarationSeen = false;
246
+ let exportedClassSeen = false;
247
+ this.blank();
248
+ while (this.i < this.lines.length) {
249
+ const line = this.line();
250
+ if (/^import "\.[^"]+"$/.test(line.text)) {
251
+ while (this.peek()?.startsWith(" ")) this.i += 1;
252
+ this.blank();
253
+ continue;
254
+ }
255
+ const jsImport = /^import js "([^"]+)" as ([A-Za-z_]\w*)$/.exec(line.text);
256
+ if (jsImport) {
257
+ if (declarationSeen)
258
+ this.fail(line, "LNG1121", "imports must precede class and function declarations");
259
+ if (jsImports.some((item) => item.name === jsImport[2]))
260
+ this.fail(line, "LNG1122", `duplicate JavaScript import name ${jsImport[2]}`);
261
+ jsImports.push({ specifier: jsImport[1], name: jsImport[2] });
262
+ this.blank();
263
+ continue;
264
+ }
265
+ const globalDeclaration = /^global (\$?[A-Za-z_]\w*) = (.+)$/.exec(
266
+ line.text,
267
+ );
268
+ if (globalDeclaration) {
269
+ if (declarationSeen)
270
+ this.fail(line, "LNG1119", "globals must precede class and function declarations");
271
+ globals.push({
272
+ name: globalDeclaration[1],
273
+ value: this.expression(globalDeclaration[2], line),
274
+ exported: false,
275
+ line: line.number,
276
+ });
277
+ this.blank();
278
+ continue;
279
+ }
280
+ const testHeader = /^test(?: "([^"]*)")?$/.exec(line.text);
281
+ if (testHeader) {
282
+ tests.push({
283
+ name: testHeader[1] ?? `${this.name}:${line.number}`,
284
+ line: line.number,
285
+ body: this.block(4),
286
+ });
287
+ this.blank();
288
+ continue;
289
+ }
290
+ const classHeader =
291
+ /^(export )?class ([A-Za-z_]\w*)(?: : ([A-Za-z_]\w*))?$/.exec(
292
+ line.text,
293
+ );
294
+ if (classHeader) {
295
+ declarationSeen = true;
296
+ if (classHeader[1] && exportedClassSeen)
297
+ this.fail(line, "LNG1114", "a file may export one class");
298
+ exportedClassSeen ||= Boolean(classHeader[1]);
299
+ const classNode = {
300
+ name: classHeader[2],
301
+ ancestor: classHeader[3] ?? null,
302
+ exported: Boolean(classHeader[1]),
303
+ methods: [],
304
+ publicMethods: [],
305
+ publicProperties: [],
306
+ staticValues: [],
307
+ enumValues: [],
308
+ line: line.number,
309
+ };
310
+ classes.push(classNode);
311
+ while (
312
+ this.peek() === " properties" ||
313
+ this.peek() === " methods" ||
314
+ this.peek() === " static" ||
315
+ this.peek() === " enum"
316
+ ) {
317
+ const section = this.line().text.trim();
318
+ if (section === "properties") {
319
+ while (/^ \$?[A-Za-z_]\w*$/.test(this.peek() ?? ""))
320
+ classNode.publicProperties.push(this.lines[this.i++].trim());
321
+ } else if (section === "methods") {
322
+ while (true) {
323
+ const methodLine = this.peek() ?? "";
324
+ const method = /^ ([A-Za-z_]\w*)\(([^)]*)\)$/.exec(
325
+ methodLine,
326
+ );
327
+ if (!method) break;
328
+ classNode.publicMethods.push({
329
+ name: method[1],
330
+ static: false,
331
+ parameters: this.parameters(method[2], {
332
+ number: this.i + 1,
333
+ }),
334
+ });
335
+ this.i += 1;
336
+ }
337
+ } else if (section === "static") {
338
+ while (true) {
339
+ const valueLine = this.peek() ?? "";
340
+ const value = /^ (\$?[A-Za-z_]\w*) = (.+)$/.exec(valueLine);
341
+ const method = /^ ([A-Za-z_]\w*)\(([^)]*)\)$/.exec(valueLine);
342
+ if (!value && !method) break;
343
+ if (value) {
344
+ const line = this.line();
345
+ classNode.staticValues.push({
346
+ name: value[1],
347
+ value: expr(value[2], line, this),
348
+ line: line.number,
349
+ });
350
+ } else {
351
+ classNode.publicMethods.push({
352
+ name: method[1],
353
+ static: true,
354
+ parameters: this.parameters(method[2], { number: this.i + 1 }),
355
+ });
356
+ this.i += 1;
357
+ }
358
+ }
359
+ } else {
360
+ while(/^ \$?[A-Za-z_]\w* = .+$/.test(this.peek() ?? "")) {
361
+ const valueLine = this.line();
362
+ const [, name, value] = /^ (\$?[A-Za-z_]\w*) = (.+)$/.exec(
363
+ valueLine.text,
364
+ );
365
+ classNode[
366
+ section === "enum" ? "enumValues" : "staticValues"
367
+ ].push({ name, value: expr(value, valueLine, this), line: valueLine.number });
368
+ }
369
+ }
370
+ }
371
+ this.blank();
372
+ continue;
373
+ }
374
+ const exportHeader = /^export ([A-Za-z_]\w*)\(([^)]*)\)$/.exec(line.text);
375
+ const exportGlobal = /^export global (\$?[A-Za-z_]\w*)$/.exec(line.text);
376
+ if (exportGlobal) {
377
+ if (declarationSeen)
378
+ this.fail(
379
+ line,
380
+ "LNG1113",
381
+ "exports must precede class and function declarations",
382
+ );
383
+ exports.push({ kind: "global", name: exportGlobal[1], line: line.number });
384
+ this.blank();
385
+ continue;
386
+ }
387
+ if (exportHeader) {
388
+ if (declarationSeen)
389
+ this.fail(
390
+ line,
391
+ "LNG1113",
392
+ "exports must precede class and function declarations",
393
+ );
394
+ exports.push({
395
+ kind: "function",
396
+ name: exportHeader[1],
397
+ parameters: this.parameters(exportHeader[2], line),
398
+ line: line.number,
399
+ });
400
+ this.blank();
401
+ continue;
402
+ }
403
+ const staticMethodHeader =
404
+ /^([A-Za-z_]\w*)::([A-Za-z_]\w*)\(([^)]*)\)$/.exec(line.text);
405
+ const methodHeader = /^([A-Za-z_]\w*):([A-Za-z_]\w*)\(([^)]*)\)$/.exec(
406
+ line.text,
407
+ );
408
+ const header =
409
+ staticMethodHeader ??
410
+ methodHeader ??
411
+ /^(export )?([A-Za-z_]\w*)\(([^)]*)\)$/.exec(line.text);
412
+ if (!header)
413
+ this.fail(line, "LNG1101", "expected a function declaration");
414
+ declarationSeen = true;
415
+ const parameters = this.parameters(
416
+ (staticMethodHeader ?? methodHeader) ? header[3] : header[3],
417
+ line,
418
+ );
419
+ if (
420
+ !staticMethodHeader &&
421
+ !methodHeader &&
422
+ header[1] &&
423
+ header[2] !== "main"
424
+ )
425
+ this.fail(line, "LNG1103", "Faz 2 exports only main(args)");
426
+ const owner =
427
+ (staticMethodHeader ?? methodHeader)
428
+ ? classes.find((item) => item.name === header[1])
429
+ : classes.find((item) => item.name === header[2]);
430
+ const body = this.block(4);
431
+ if (owner) {
432
+ const isStatic = Boolean(staticMethodHeader);
433
+ const name =
434
+ (staticMethodHeader ?? methodHeader) ? header[2] : "constructor";
435
+ owner.methods.push({
436
+ name,
437
+ static: isStatic,
438
+ public: owner.publicMethods.some(
439
+ (item) => item.name === name && item.static === isStatic,
440
+ ),
441
+ parameters,
442
+ line: line.number,
443
+ body,
444
+ });
445
+ } else
446
+ functions.push({
447
+ name: header[2],
448
+ parameters,
449
+ exported: false,
450
+ line: line.number,
451
+ body,
452
+ });
453
+ this.blank();
454
+ }
455
+ for (const exported of exports) {
456
+ if (exported.kind === "global") {
457
+ const target = globals.find((item) => item.name === exported.name);
458
+ if (!target)
459
+ throw new LangoError(
460
+ "LNG1104",
461
+ exported.line,
462
+ `${this.name}: export does not match a global declaration`,
463
+ );
464
+ target.exported = true;
465
+ continue;
466
+ }
467
+ const target = functions.find(
468
+ (item) =>
469
+ item.name === exported.name &&
470
+ item.parameters.map((p) => p.name).join("\0") ===
471
+ exported.parameters.map((p) => p.name).join("\0"),
472
+ );
473
+ if (!target)
474
+ throw new LangoError(
475
+ "LNG1104",
476
+ exported.line,
477
+ `${this.name}: export does not match a function declaration`,
478
+ );
479
+ target.exported = true;
480
+ }
481
+ const mains = functions.filter(
482
+ (item) =>
483
+ item.exported &&
484
+ item.name === "main" &&
485
+ item.parameters.length === 1 &&
486
+ item.parameters[0].name === "args",
487
+ );
488
+ if (mains.length > 1)
489
+ throw new LangoError(
490
+ "LNG1105",
491
+ 1,
492
+ `${this.name}: main(args) may be exported once`,
493
+ );
494
+ const declarations = new Set([
495
+ ...functions.map((item) => item.name),
496
+ ...classes.map((item) => item.name),
497
+ ...globals.map((item) => item.name),
498
+ ]);
499
+ for (const jsImport of jsImports)
500
+ if (declarations.has(jsImport.name))
501
+ throw new LangoError(
502
+ "LNG1123",
503
+ 1,
504
+ `${this.name}: JavaScript import name ${jsImport.name} conflicts with a Lango declaration`,
505
+ );
506
+ return { functions, classes, globals, tests, jsImports };
507
+ }
508
+ block(indent) {
509
+ const body = [];
510
+ while (true) {
511
+ this.blank();
512
+ const next = this.peek();
513
+ if (!next || spaces(next) < indent) break;
514
+ if (spaces(next) !== indent)
515
+ this.fail(
516
+ { text: next, number: this.i + 1 },
517
+ "LNG1105",
518
+ `expected ${indent} spaces`,
519
+ );
520
+ const line = this.line();
521
+ const text = line.text.slice(indent);
522
+ if (text.startsWith("assert ")) {
523
+ body.push({
524
+ type: "assert",
525
+ value: expr(text.slice(7), line, this),
526
+ line: line.number,
527
+ source: text.slice(7),
528
+ });
529
+ } else if (/^global (\$?[A-Za-z_]\w*)$/.test(text)) {
530
+ const [, name] = /^global (\$?[A-Za-z_]\w*)$/.exec(text);
531
+ body.push({ type: "globalDeclare", name, line: line.number });
532
+ } else if (/^context ([A-Za-z_]\w*) = (.+)$/.test(text)) {
533
+ const [, name, value] = /^context ([A-Za-z_]\w*) = (.+)$/.exec(text);
534
+ body.push({ type: "contextSet", name, value: expr(value, line, this) });
535
+ } else if (/^context ([A-Za-z_]\w*)$/.test(text)) {
536
+ const [, name] = /^context ([A-Za-z_]\w*)$/.exec(text);
537
+ body.push({ type: "contextDeclare", name, line: line.number });
538
+ } else if (text.startsWith("if ")) {
539
+ const thenBody = this.block(indent + 4);
540
+ this.blank();
541
+ let elseBody = null;
542
+ if (this.peek() === `${" ".repeat(indent + 2)}else`) {
543
+ this.i += 1;
544
+ elseBody = this.block(indent + 6);
545
+ }
546
+ body.push({
547
+ type: "if",
548
+ condition: expr(text.slice(3), line, this),
549
+ thenBody,
550
+ elseBody,
551
+ });
552
+ } else if (text === "switch" || text.startsWith("switch ")) {
553
+ const value =
554
+ text === "switch" ? null : expr(text.slice(7), line, this);
555
+ const cases = [];
556
+ let elseBody = null;
557
+ while (true) {
558
+ this.blank();
559
+ const label = this.peek();
560
+ if (!label || spaces(label) !== indent + 2) break;
561
+ const labelText = label.trim();
562
+ if (labelText === "else") {
563
+ this.i += 1;
564
+ elseBody = this.block(indent + 6);
565
+ break;
566
+ }
567
+ if (!labelText.startsWith("case "))
568
+ this.fail(
569
+ { text: label, number: this.i + 1 },
570
+ "LNG1116",
571
+ "expected case or else",
572
+ );
573
+ const tests = [];
574
+ while (
575
+ spaces(this.peek() ?? "") === indent + 2 &&
576
+ (this.peek() ?? "").trim().startsWith("case ")
577
+ ) {
578
+ const caseLine = this.line();
579
+ tests.push(expr(caseLine.text.trim().slice(5), caseLine, this));
580
+ this.blank();
581
+ }
582
+ cases.push({ tests, body: this.block(indent + 6) });
583
+ }
584
+ if (!cases.length) this.fail(line, "LNG1116", "switch requires a case");
585
+ body.push({ type: "switch", value, cases, elseBody });
586
+ } else if (text === "try") {
587
+ const tryBody = this.block(indent + 4);
588
+ this.blank();
589
+ let catchName = null;
590
+ let catchBody = null;
591
+ let finallyBody = null;
592
+ const caught = /^catch ([A-Za-z_]\w*)$/.exec(
593
+ (this.peek() ?? "").trimStart(),
594
+ );
595
+ if (caught) {
596
+ this.i += 1;
597
+ catchName = caught[1];
598
+ catchBody = this.block(indent + 4);
599
+ this.blank();
600
+ }
601
+ if ((this.peek() ?? "").trimStart() === "finally") {
602
+ this.i += 1;
603
+ finallyBody = this.block(indent + 4);
604
+ }
605
+ if (!catchBody && !finallyBody)
606
+ this.fail(line, "LNG1112", "try requires catch or finally");
607
+ body.push({ type: "try", tryBody, catchName, catchBody, finallyBody });
608
+ } else if (text === "loop") {
609
+ body.push({ type: "loop", condition: null, body: this.block(indent + 4) });
610
+ } else if (text.startsWith("loop while ")) {
611
+ body.push({
612
+ type: "loop",
613
+ condition: expr(text.slice(11), line, this),
614
+ body: this.block(indent + 4),
615
+ });
616
+ } else if (text.startsWith("loop until ")) {
617
+ body.push({
618
+ type: "loop",
619
+ condition: expr(text.slice(11), line, this),
620
+ until: true,
621
+ body: this.block(indent + 4),
622
+ });
623
+ } else if (/^for (.+) as ([A-Za-z_]\w*): ([A-Za-z_]\w*)$/.test(text)) {
624
+ const [, source, key, value] =
625
+ /^for (.+) as ([A-Za-z_]\w*): ([A-Za-z_]\w*)$/.exec(text);
626
+ body.push({
627
+ type: "forObject",
628
+ collection: this.expression(source, line),
629
+ key,
630
+ value,
631
+ body: this.block(indent + 4),
632
+ });
633
+ } else if (/^for (.+) as ([A-Za-z_]\w*)$/.test(text)) {
634
+ const [, source, item] =
635
+ /^for (.+) as ([A-Za-z_]\w*)$/.exec(text);
636
+ body.push({
637
+ type: "for",
638
+ collection: this.expression(source, line),
639
+ item,
640
+ body: this.block(indent + 4),
641
+ });
642
+ } else if (text === "return") body.push({ type: "return", value: null });
643
+ else if (/^return \(([^)]*)\)$/.test(text)) {
644
+ const [, rawParameters] = /^return \(([^)]*)\)$/.exec(text);
645
+ body.push({
646
+ type: "return",
647
+ value: {
648
+ type: "closure",
649
+ parameters: this.parameters(rawParameters, line),
650
+ captures: this.captures(),
651
+ line: line.number,
652
+ body: this.block(indent + 4),
653
+ },
654
+ });
655
+ } else if (text.startsWith("return ")) {
656
+ const value = text.slice(7);
657
+ const chain = /^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.exec(value);
658
+ if (!chain) body.push({ type: "return", value: this.expression(value, line) });
659
+ else {
660
+ const steps = [];
661
+ steps.push({
662
+ name: chain[2],
663
+ parameters: this.parameters(chain[3], line),
664
+ captures: this.captures(),
665
+ line: line.number,
666
+ body: this.block(indent + 4),
667
+ });
668
+ this.blank();
669
+ while (spaces(this.peek() ?? "") === indent) {
670
+ const continuation = /^\)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.exec((this.peek() ?? "").slice(indent));
671
+ if (!continuation) break;
672
+ const continuationLine = this.line();
673
+ steps.push({
674
+ name: continuation[1],
675
+ parameters: this.parameters(continuation[2], continuationLine),
676
+ captures: this.captures(),
677
+ line: continuationLine.number,
678
+ body: this.block(indent + 4),
679
+ });
680
+ this.blank();
681
+ }
682
+ if (this.peek() !== `${" ".repeat(indent)})`)
683
+ this.fail(line, "LNG1253", "Promise chain requires a closing parenthesis");
684
+ this.i += 1;
685
+ body.push({ type: "return", value: { type: "chain", base: expr(chain[1], line, this), steps } });
686
+ }
687
+ }
688
+ else if (text.startsWith("throw "))
689
+ body.push({ type: "throw", value: expr(text.slice(6), line, this) });
690
+ else if (/^super\((.*)\)$/.test(text)) {
691
+ const [, argumentsText] = /^super\((.*)\)$/.exec(text);
692
+ body.push({
693
+ type: "superConstructor",
694
+ arguments: argumentsText
695
+ ? splitTopLevel(argumentsText).map((item) => expr(item, line, this))
696
+ : [],
697
+ line: line.number,
698
+ });
699
+ } else if (/^(break|continue)(?: ([1-9]\d*))?$/.test(text)) {
700
+ const [, type, rawLevel] = /^(break|continue)(?: ([1-9]\d*))?$/.exec(text);
701
+ body.push({ type, level: Number(rawLevel ?? 1), line: line.number });
702
+ }
703
+ else if (/^(\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*) (\+|-|\*|\/|%|\*\*)= (.+)$/.test(text)) {
704
+ const [, name, operator, source] = /^(\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*) (\+|-|\*|\/|%|\*\*)= (.+)$/.exec(text);
705
+ body.push({
706
+ type: "assign",
707
+ name,
708
+ value: {
709
+ type: "binary",
710
+ operator,
711
+ left: { type: "raw", value: name },
712
+ right: this.expression(source, line),
713
+ },
714
+ });
715
+ }
716
+ else if (/^(\$[A-Za-z_]\w*|@{1,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*)(\+\+|--)$/.test(text)) {
717
+ const [, name, suffix] = /^(\$[A-Za-z_]\w*|@{1,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*)(\+\+|--)$/.exec(text);
718
+ body.push({
719
+ type: "assign",
720
+ name,
721
+ value: {
722
+ type: "binary",
723
+ operator: suffix === "++" ? "+" : "-",
724
+ left: { type: "raw", value: name },
725
+ right: { type: "raw", value: "1" },
726
+ },
727
+ });
728
+ }
729
+ else if (/^[A-Za-z_]\w*--$/.test(text))
730
+ body.push({
731
+ type: "assign",
732
+ name: text.slice(0, -2),
733
+ value: {
734
+ type: "binary",
735
+ operator: "-",
736
+ left: { type: "raw", value: text.slice(0, -2) },
737
+ right: { type: "raw", value: "1" },
738
+ },
739
+ });
740
+ else if (/^[A-Za-z_]\w*\+\+$/.test(text))
741
+ body.push({ type: "increment", name: text.slice(0, -2) });
742
+ else if (/^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.test(text)) {
743
+ const [, first, name, parameters] = /^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.exec(text);
744
+ const steps = [];
745
+ let step = { name, parameters: this.parameters(parameters, line), captures: this.captures(), line: line.number, body: this.block(indent + 4) };
746
+ steps.push(step);
747
+ this.blank();
748
+ while (spaces(this.peek() ?? "") === indent) {
749
+ const continuation = /^\)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.exec(
750
+ (this.peek() ?? "").slice(indent),
751
+ );
752
+ if (!continuation) break;
753
+ const continuationLine = this.line();
754
+ step = {
755
+ name: continuation[1],
756
+ parameters: this.parameters(continuation[2], continuationLine),
757
+ captures: this.captures(),
758
+ line: continuationLine.number,
759
+ body: this.block(indent + 4),
760
+ };
761
+ steps.push(step);
762
+ this.blank();
763
+ }
764
+ if (this.peek() !== `${" ".repeat(indent)})`)
765
+ this.fail(line, "LNG1253", "Promise chain requires a closing parenthesis");
766
+ this.i += 1;
767
+ body.push({ type: "chain", base: expr(first, line, this), steps });
768
+ }
769
+ else if (/^println\((.*)\)$/.test(text))
770
+ body.push({
771
+ type: "println",
772
+ value: expr(text.slice(8, -1), line, this),
773
+ });
774
+ else if (/^(\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*) = ([A-Za-z_]\w*.*(?:\]|\))(?:\?\.|\.)[A-Za-z_]\w*\(.*\))$/.test(text)) {
775
+ const [, name, source] = /^(\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*) = ([A-Za-z_]\w*.*(?:\]|\))(?:\?\.|\.)[A-Za-z_]\w*\(.*\))$/.exec(text);
776
+ body.push({ type: "assign", name, value: this.expression(source, line) });
777
+ }
778
+ else if (/^[A-Za-z_]\w*.*(?:\]|\))(?:\?\.|\.)[A-Za-z_]\w*\(.*\)$/.test(text))
779
+ body.push({ type: "expression", value: expr(text, line, this) });
780
+ else if (/^[A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w*)+\(.*\)$/.test(text))
781
+ body.push({ type: "expression", value: expr(text, line, this) });
782
+ else if (/^[A-Za-z_]\w*(?:\/[A-Za-z_]\w*)+\(.*\)$/.test(text))
783
+ body.push({ type: "expression", value: expr(text, line, this) });
784
+ else if (/^[A-Za-z_]\w*\(.*\)$/.test(text))
785
+ body.push({ type: "expression", value: expr(text, line, this) });
786
+ else if (/^([A-Za-z_]\w*) = \(([^)]*)\)$/.test(text)) {
787
+ const [, name, rawParameters] = /^([A-Za-z_]\w*) = \(([^)]*)\)$/.exec(
788
+ text,
789
+ );
790
+ body.push({
791
+ type: "closureAssign",
792
+ name,
793
+ parameters: this.parameters(rawParameters, line),
794
+ captures: this.captures(),
795
+ line: line.number,
796
+ body: this.block(indent + 4),
797
+ });
798
+ } else if (/^([A-Za-z_]\w*) = \{$/.test(text)) {
799
+ const [, name] = /^([A-Za-z_]\w*) = \{$/.exec(text);
800
+ body.push({ type: "assign", name, value: this.object(indent) });
801
+ } else if (/^([A-Za-z_]\w*) = SQL$/.test(text)) {
802
+ const [, name] = /^([A-Za-z_]\w*) = SQL$/.exec(text);
803
+ const sql = [];
804
+ while (this.i < this.lines.length) {
805
+ const sqlLine = this.peek();
806
+ if (sqlLine.trim() === "") { sql.push(""); this.i += 1; continue; }
807
+ if (spaces(sqlLine) <= indent) break;
808
+ if (spaces(sqlLine) < indent + 4)
809
+ this.fail({ text: sqlLine, number: this.i + 1 }, "LNG1255", "SQL body must be indented");
810
+ sql.push(sqlLine.slice(indent + 4));
811
+ this.i += 1;
812
+ }
813
+ if (!sql.some((item) => item.trim()))
814
+ this.fail(line, "LNG1255", "SQL body requires a statement");
815
+ const sqlText = sql.join("\n");
816
+ const parts = [];
817
+ let at = 0;
818
+ for (const match of sqlText.matchAll(/\$\{([^}]*)\}/g)) {
819
+ if (match.index > at) parts.push({ type: "text", value: sqlText.slice(at, match.index) });
820
+ const source = match[1].trim();
821
+ if (!source) this.fail(line, "LNG1257", "SQL interpolation requires an expression");
822
+ parts.push({ type: "expression", value: expr(source, line, this) });
823
+ at = match.index + match[0].length;
824
+ }
825
+ if (at < sqlText.length || !parts.length)
826
+ parts.push({ type: "text", value: sqlText.slice(at) });
827
+ const parameters = [...new Set(
828
+ parts
829
+ .filter((part) => part.type === "text")
830
+ .flatMap((part) => collectSqlParameters(part.value)),
831
+ )];
832
+ body.push({
833
+ type: "assign",
834
+ name,
835
+ value: { type: "sql", text: sqlText, parts, parameters },
836
+ });
837
+ } else if (/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\]\[([^\]]+)\] = (.+)$/.test(text)) {
838
+ const [, receiver, firstIndex, secondIndex, sourceValue] =
839
+ /^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\]\[([^\]]+)\] = (.+)$/.exec(text);
840
+ body.push({
841
+ type: "indexAssign",
842
+ receiver,
843
+ indexes: [this.expression(firstIndex, line), this.expression(secondIndex, line)],
844
+ value: this.expression(sourceValue, line),
845
+ operator: null,
846
+ });
847
+ } else if (/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\]\[([^\]]+)\] (\+|-|\*|\/|%|\*\*)= (.+)$/.test(text)) {
848
+ const [, receiver, firstIndex, secondIndex, operator, sourceValue] =
849
+ /^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\]\[([^\]]+)\] (\+|-|\*|\/|%|\*\*)= (.+)$/.exec(text);
850
+ body.push({
851
+ type: "indexAssign",
852
+ receiver,
853
+ indexes: [this.expression(firstIndex, line), this.expression(secondIndex, line)],
854
+ value: this.expression(sourceValue, line),
855
+ operator,
856
+ });
857
+ } else if (/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\]\[([^\]]+)\](\+\+|--)$/.test(text)) {
858
+ const [, receiver, firstIndex, secondIndex, suffix] =
859
+ /^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\]\[([^\]]+)\](\+\+|--)$/.exec(text);
860
+ body.push({
861
+ type: "indexAssign",
862
+ receiver,
863
+ indexes: [this.expression(firstIndex, line), this.expression(secondIndex, line)],
864
+ value: { type: "raw", value: "1" },
865
+ operator: suffix === "++" ? "+" : "-",
866
+ });
867
+ } else if (/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\] = (.+)$/.test(text)) {
868
+ const [, receiver, sourceIndex, sourceValue] =
869
+ /^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\] = (.+)$/.exec(text);
870
+ body.push({
871
+ type: "indexAssign",
872
+ receiver,
873
+ index: this.expression(sourceIndex, line),
874
+ value: this.expression(sourceValue, line),
875
+ operator: null,
876
+ });
877
+ } else if (/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\] (\+|-|\*|\/|%|\*\*)= (.+)$/.test(text)) {
878
+ const [, receiver, sourceIndex, operator, sourceValue] =
879
+ /^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\] (\+|-|\*|\/|%|\*\*)= (.+)$/.exec(text);
880
+ body.push({
881
+ type: "indexAssign",
882
+ receiver,
883
+ index: this.expression(sourceIndex, line),
884
+ value: this.expression(sourceValue, line),
885
+ operator,
886
+ });
887
+ } else if (/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\](\+\+|--)$/.test(text)) {
888
+ const [, receiver, sourceIndex, suffix] =
889
+ /^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\[([^\]]+)\](\+\+|--)$/.exec(text);
890
+ body.push({
891
+ type: "indexAssign",
892
+ receiver,
893
+ index: this.expression(sourceIndex, line),
894
+ value: { type: "raw", value: "1" },
895
+ operator: suffix === "++" ? "+" : "-",
896
+ });
897
+ } else {
898
+ const assignment = /^(\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*) = (.+)$/.exec(text);
899
+ if (!assignment)
900
+ this.fail(line, "LNG1106", `unsupported Faz 2 statement: ${text}`);
901
+ body.push({
902
+ type: "assign",
903
+ name: assignment[1],
904
+ value: this.expression(assignment[2], line),
905
+ });
906
+ }
907
+ }
908
+ if (!body.length)
909
+ this.fail(
910
+ { text: "", number: this.i + 1 },
911
+ "LNG1107",
912
+ "expected a statement",
913
+ );
914
+ return body;
915
+ }
916
+ parameters(raw, line) {
917
+ if (raw === "") return [];
918
+ return splitTopLevel(raw).map((item) => {
919
+ const match = /^(\$?[A-Za-z_]\w*)(?: = (.+))?$/.exec(item);
920
+ if (!match) this.fail(line, "LNG1102", "invalid parameter list");
921
+ return {
922
+ name: match[1],
923
+ defaultValue:
924
+ match[2] === undefined ? null : expr(match[2], line, this),
925
+ };
926
+ });
927
+ }
928
+ expression(source, line) {
929
+ if (!source.trimStart().startsWith("<")) return expr(source, line, this);
930
+
931
+ let xml = source;
932
+ while (true) {
933
+ const parsed = xmlLiteral(xml, line, this);
934
+ if (parsed !== null) return parsed;
935
+ if (this.i >= this.lines.length)
936
+ this.fail(line, "LNG1247", "unterminated XML literal");
937
+ xml += `\n${this.lines[this.i++]}`;
938
+ }
939
+ }
940
+ captures() {
941
+ if (!(this.peek() ?? "").trimStart().startsWith("capture ")) return [];
942
+ const line = this.line();
943
+ const names = splitTopLevel(line.text.trim().slice(8));
944
+ if (
945
+ names.some(
946
+ (name) =>
947
+ !/^(?:@{1,2}[A-Za-z_]\w*|[A-Za-z_]\w*)$/.test(name) ||
948
+ name.startsWith("@@_"),
949
+ )
950
+ )
951
+ this.fail(line, "LNG1117", "invalid capture list");
952
+ if (new Set(names).size !== names.length)
953
+ this.fail(line, "LNG1118", "duplicate capture name");
954
+ return names;
955
+ }
956
+ object(indent) {
957
+ const fields = [];
958
+ const keys = new Set();
959
+ while (true) {
960
+ this.blank();
961
+ if (this.peek() === `${" ".repeat(indent)}}`) {
962
+ this.i += 1;
963
+ break;
964
+ }
965
+ if (!this.peek() || spaces(this.peek()) !== indent + 4)
966
+ this.fail(
967
+ { number: this.i + 1 },
968
+ "LNG1110",
969
+ "expected an object field or closing brace",
970
+ );
971
+ const line = this.line();
972
+ const match = /^(\$?[A-Za-z_]\w*|['"][^'"]+['"]): (.+)$/.exec(line.text.slice(indent + 4));
973
+ if (!match) this.fail(line, "LNG1111", "expected `key: value`");
974
+ const key = match[1].startsWith("'") || match[1].startsWith('"')
975
+ ? match[1].slice(1, -1).replace(/\\(['"\\])/g, "$1")
976
+ : match[1];
977
+ if (keys.has(key)) this.fail(line, "LNG1258", `duplicate object key ${key}`);
978
+ keys.add(key);
979
+ fields.push({
980
+ key: match[1],
981
+ value:
982
+ match[2] === "{"
983
+ ? this.object(indent + 4)
984
+ : expr(match[2], line, this),
985
+ });
986
+ }
987
+ return { type: "object", fields };
988
+ }
989
+ blank() {
990
+ while (this.peek()?.trim() === "") this.i += 1;
991
+ }
992
+
993
+ peek() {
994
+ return this.lines[this.i];
995
+ }
996
+
997
+ line() {
998
+ return { text: this.lines[this.i++], number: this.i };
999
+ }
1000
+
1001
+ fail(line, code, message) {
1002
+ const column = (line.text.search(/\S/) >= 0 ? line.text.search(/\S/) : 0) + 1;
1003
+ throw new LangoError(
1004
+ code,
1005
+ line.number,
1006
+ `${this.name}: ${message}`,
1007
+ this.name,
1008
+ column,
1009
+ Math.max(column, line.text.length + 1),
1010
+ );
1011
+ }
1012
+ }
1013
+
1014
+ function collectSqlParameters(sql) {
1015
+ const names = [];
1016
+ let quote = null;
1017
+ let lineComment = false;
1018
+ let blockComment = false;
1019
+ for (let index = 0; index < sql.length; index += 1) {
1020
+ const character = sql[index];
1021
+ const next = sql[index + 1];
1022
+ if (lineComment) {
1023
+ if (character === "\n") lineComment = false;
1024
+ continue;
1025
+ }
1026
+ if (blockComment) {
1027
+ if (character === "*" && next === "/") { blockComment = false; index += 1; }
1028
+ continue;
1029
+ }
1030
+ if (quote !== null) {
1031
+ if (character === quote) {
1032
+ if (next === quote) { index += 1; continue; }
1033
+ quote = null;
1034
+ }
1035
+ continue;
1036
+ }
1037
+ if (character === "-" && next === "-") { lineComment = true; index += 1; continue; }
1038
+ if (character === "/" && next === "*") { blockComment = true; index += 1; continue; }
1039
+ if (character === "'" || character === '"') { quote = character; continue; }
1040
+ if (character !== ":" || sql[index - 1] === ":" || !/[A-Za-z_]/.test(next ?? "")) continue;
1041
+ let end = index + 2;
1042
+ while (/\w/.test(sql[end] ?? "")) end += 1;
1043
+ names.push(sql.slice(index + 1, end));
1044
+ index = end - 1;
1045
+ }
1046
+ return [...new Set(names)];
1047
+ }
1048
+
1049
+ function topLevelBinary(text, operators, rightAssociative = false) {
1050
+ let quote = null;
1051
+ let depth = 0;
1052
+ let match = null;
1053
+ for (let index = 0; index < text.length; index += 1) {
1054
+ const character = text[index];
1055
+ if (quote !== null) {
1056
+ if (character === "\\") { index += 1; continue; }
1057
+ if (character === quote) quote = null;
1058
+ continue;
1059
+ }
1060
+ if (character === "'" || character === '"') { quote = character; continue; }
1061
+ if ("([{".includes(character)) { depth += 1; continue; }
1062
+ if (")]}".includes(character)) { depth -= 1; continue; }
1063
+ if (depth !== 0) continue;
1064
+ for (const operator of operators) {
1065
+ if (text.startsWith(` ${operator} `, index)) {
1066
+ if (match === null || !rightAssociative) match = { index, operator };
1067
+ index += operator.length + 1;
1068
+ break;
1069
+ }
1070
+ }
1071
+ }
1072
+ return match;
1073
+ }
1074
+
1075
+ function splitTopLevel(text, separator = ", ") {
1076
+ const parts = [];
1077
+ let start = 0;
1078
+ let quote = null;
1079
+ let depth = 0;
1080
+ for (let index = 0; index < text.length; index += 1) {
1081
+ const character = text[index];
1082
+ if (quote !== null) {
1083
+ if (character === "\\") { index += 1; continue; }
1084
+ if (character === quote) quote = null;
1085
+ continue;
1086
+ }
1087
+ if (character === "'" || character === '"') { quote = character; continue; }
1088
+ if ("([{".includes(character)) { depth += 1; continue; }
1089
+ if (")]}".includes(character)) { depth -= 1; continue; }
1090
+ if (depth === 0 && text.startsWith(separator, index)) {
1091
+ parts.push(text.slice(start, index));
1092
+ start = index + separator.length;
1093
+ index += separator.length - 1;
1094
+ }
1095
+ }
1096
+ parts.push(text.slice(start));
1097
+ return parts;
1098
+ }
1099
+
1100
+ function parenthesizedExpression(text) {
1101
+ if (!text.startsWith("(") || !text.endsWith(")")) return null;
1102
+ let quote = null;
1103
+ let depth = 0;
1104
+ for (let index = 0; index < text.length; index += 1) {
1105
+ const character = text[index];
1106
+ if (quote !== null) {
1107
+ if (character === "\\") { index += 1; continue; }
1108
+ if (character === quote) quote = null;
1109
+ continue;
1110
+ }
1111
+ if (character === "'" || character === '"') { quote = character; continue; }
1112
+ if (character === "(") depth += 1;
1113
+ if (character === ")") {
1114
+ depth -= 1;
1115
+ if (depth === 0 && index !== text.length - 1) return null;
1116
+ }
1117
+ }
1118
+ return depth === 0 ? text.slice(1, -1) : null;
1119
+ }
1120
+
1121
+ function expr(source, line, parser) {
1122
+ const text = source.trim();
1123
+ const grouped = parenthesizedExpression(text);
1124
+ if (grouped !== null) return expr(grouped, line, parser);
1125
+ if (text.startsWith("<")) {
1126
+ const parsed = xmlLiteral(text, line, parser);
1127
+ if (parsed !== null) return parsed;
1128
+ parser.fail(line, "LNG1247", "unterminated XML literal");
1129
+ }
1130
+ if (text.startsWith("[") && text.endsWith("]"))
1131
+ return {
1132
+ type: "array",
1133
+ values:
1134
+ text.slice(1, -1) === ""
1135
+ ? []
1136
+ : splitTopLevel(text.slice(1, -1)).map((item) => expr(item, line, parser)),
1137
+ };
1138
+ if (text.startsWith("{") && text.endsWith("}")) {
1139
+ const fields = text.slice(1, -1).trim();
1140
+ if (fields === "") return { type: "object", fields: [] };
1141
+ const keys = new Set();
1142
+ return {
1143
+ type: "object",
1144
+ fields: splitTopLevel(fields).map((field) => {
1145
+ const match = /^(\$?[A-Za-z_]\w*|['"][^'"]+['"]): (.+)$/.exec(field);
1146
+ if (!match) parser.fail(line, "LNG1111", "expected `key: value`");
1147
+ const key = match[1].startsWith("'") || match[1].startsWith('"')
1148
+ ? match[1].slice(1, -1).replace(/\\(['"\\])/g, "$1")
1149
+ : match[1];
1150
+ if (keys.has(key)) parser.fail(line, "LNG1258", `duplicate object key ${key}`);
1151
+ keys.add(key);
1152
+ return { key: match[1], value: expr(match[2], line, parser) };
1153
+ }),
1154
+ };
1155
+ }
1156
+ for (const operators of [
1157
+ ["??"],
1158
+ ["or", "||"],
1159
+ ["and", "&&"],
1160
+ ["==", "!="],
1161
+ [">=", "<=", ">", "<", "not in", "in"],
1162
+ ["+", "-"],
1163
+ ["*", "/", "%"],
1164
+ ]) {
1165
+ const match = topLevelBinary(text, operators);
1166
+ if (match !== null)
1167
+ return {
1168
+ type: "binary",
1169
+ operator: match.operator,
1170
+ left: expr(text.slice(0, match.index), line, parser),
1171
+ right: expr(text.slice(match.index + match.operator.length + 2), line, parser),
1172
+ };
1173
+ }
1174
+ const exponent = topLevelBinary(text, ["**"], true);
1175
+ if (exponent !== null)
1176
+ return {
1177
+ type: "binary",
1178
+ operator: exponent.operator,
1179
+ left: expr(text.slice(0, exponent.index), line, parser),
1180
+ right: expr(text.slice(exponent.index + exponent.operator.length + 2), line, parser),
1181
+ };
1182
+ if (text.startsWith("not "))
1183
+ return {
1184
+ type: "unary",
1185
+ operator: "not",
1186
+ value: expr(text.slice(4), line, parser),
1187
+ };
1188
+ if (/^-[^\s]/.test(text))
1189
+ return {
1190
+ type: "unary",
1191
+ operator: "negate",
1192
+ value: expr(text.slice(1), line, parser),
1193
+ };
1194
+ if (text.startsWith("!"))
1195
+ return {
1196
+ type: "unary",
1197
+ operator: "not",
1198
+ value: expr(text.slice(1), line, parser),
1199
+ };
1200
+ const localize = /^_\((.*)\)$/.exec(text);
1201
+ if (localize)
1202
+ return { type: "translate", literal: literal(localize[1], line, parser) };
1203
+ const construct =
1204
+ /^new ([A-Za-z_]\w*(?:[./][A-Za-z_]\w*)*)\((.*)\)$/.exec(text);
1205
+ if (construct)
1206
+ return {
1207
+ type: "new",
1208
+ name: construct[1],
1209
+ arguments: construct[2]
1210
+ ? splitTopLevel(construct[2]).map((item) => expr(item, line, parser))
1211
+ : [],
1212
+ line: line.number,
1213
+ };
1214
+ const superMethodCall = /^super\.([A-Za-z_]\w*)\((.*)\)$/.exec(text);
1215
+ if (superMethodCall)
1216
+ return {
1217
+ type: "superMethodCall",
1218
+ name: superMethodCall[1],
1219
+ arguments: superMethodCall[2]
1220
+ ? splitTopLevel(superMethodCall[2]).map((item) => expr(item, line, parser))
1221
+ : [],
1222
+ line: line.number,
1223
+ };
1224
+ const nestedIndex =
1225
+ /^([A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w)*(?:\?\.)?\[.+\])(\?\.)?\[(.+)\]$/.exec(
1226
+ text,
1227
+ );
1228
+ if (nestedIndex)
1229
+ return {
1230
+ type: "index",
1231
+ receiver: expr(nestedIndex[1], line, parser),
1232
+ optional: nestedIndex[2] === "?.",
1233
+ index: expr(nestedIndex[3], line, parser),
1234
+ };
1235
+ const computedIndex =
1236
+ /^([A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w)*)(\?\.)?\[([^\]]+)\]$/.exec(
1237
+ text,
1238
+ );
1239
+ if (
1240
+ computedIndex &&
1241
+ !/^(?:\d+|[A-Za-z_]\w*|['"][^'"]*['"])$/.test(computedIndex[3])
1242
+ )
1243
+ return {
1244
+ type: "index",
1245
+ receiver: computedIndex[1],
1246
+ optional: computedIndex[2] === "?.",
1247
+ index: expr(computedIndex[3], line, parser),
1248
+ };
1249
+ const indexedProperty =
1250
+ /^([A-Za-z_]\w*.*\])(\?\.|\.)(\$?[A-Za-z_]\w*)$/.exec(text);
1251
+ if (
1252
+ indexedProperty &&
1253
+ !/^[A-Za-z_]\w*\?\.\[(?:\d+|[A-Za-z_]\w*|['"][^'"]*['"])\]$/.test(
1254
+ indexedProperty[1],
1255
+ )
1256
+ )
1257
+ return {
1258
+ type: "property",
1259
+ receiver: expr(indexedProperty[1], line, parser),
1260
+ optional: indexedProperty[2] === "?.",
1261
+ name: indexedProperty[3],
1262
+ };
1263
+ const indexedMethodCall =
1264
+ /^([A-Za-z_]\w*.*\])(\?\.|\.)([A-Za-z_]\w*)\((.*)\)$/.exec(text);
1265
+ if (indexedMethodCall)
1266
+ return {
1267
+ type: "methodCall",
1268
+ receiver: expr(indexedMethodCall[1], line, parser),
1269
+ optional: indexedMethodCall[2] === "?.",
1270
+ name: indexedMethodCall[3],
1271
+ arguments: indexedMethodCall[4]
1272
+ ? splitTopLevel(indexedMethodCall[4]).map((item) => expr(item, line, parser))
1273
+ : [],
1274
+ line: line.number,
1275
+ };
1276
+ const nestedCallResultIndex =
1277
+ /^([A-Za-z_]\w*.*\)\?\.\[.+\])(\?\.)?\[(.+)\]$/.exec(text);
1278
+ if (nestedCallResultIndex)
1279
+ return {
1280
+ type: "index",
1281
+ receiver: expr(nestedCallResultIndex[1], line, parser),
1282
+ optional: nestedCallResultIndex[2] === "?.",
1283
+ index: expr(nestedCallResultIndex[3], line, parser),
1284
+ };
1285
+ const callResultIndex =
1286
+ /^([A-Za-z_]\w*.*\))(\?\.)?\[(.+)\]$/.exec(text);
1287
+ if (callResultIndex)
1288
+ return {
1289
+ type: "index",
1290
+ receiver: expr(callResultIndex[1], line, parser),
1291
+ optional: callResultIndex[2] === "?.",
1292
+ index: expr(callResultIndex[3], line, parser),
1293
+ };
1294
+ const callResultProperty =
1295
+ /^([A-Za-z_]\w*.*\))(\?\.|\.)(\$?[A-Za-z_]\w*)$/.exec(text);
1296
+ if (callResultProperty)
1297
+ return {
1298
+ type: "property",
1299
+ receiver: expr(callResultProperty[1], line, parser),
1300
+ optional: callResultProperty[2] === "?.",
1301
+ name: callResultProperty[3],
1302
+ };
1303
+ const callResultMethod =
1304
+ /^([A-Za-z_]\w*.*\))(\?\.|\.)([A-Za-z_]\w*)\((.*)\)$/.exec(text);
1305
+ if (callResultMethod)
1306
+ return {
1307
+ type: "methodCall",
1308
+ receiver: expr(callResultMethod[1], line, parser),
1309
+ optional: callResultMethod[2] === "?.",
1310
+ name: callResultMethod[3],
1311
+ arguments: callResultMethod[4]
1312
+ ? splitTopLevel(callResultMethod[4]).map((item) => expr(item, line, parser))
1313
+ : [],
1314
+ line: line.number,
1315
+ };
1316
+ const methodCall =
1317
+ /^([A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w*)*?)(\?\.|\.)([A-Za-z_]\w*)\((.*)\)$/.exec(
1318
+ text,
1319
+ );
1320
+ const namespaceMethodCall =
1321
+ /^([A-Za-z_]\w*(?:\/[A-Za-z_]\w*)*)\/([A-Za-z_]\w*)\((.*)\)$/.exec(
1322
+ text,
1323
+ );
1324
+ if (namespaceMethodCall)
1325
+ return {
1326
+ type: "namespaceMethodCall",
1327
+ receiver: namespaceMethodCall[1],
1328
+ name: namespaceMethodCall[2],
1329
+ arguments: namespaceMethodCall[3]
1330
+ ? splitTopLevel(namespaceMethodCall[3]).map((item) => expr(item, line, parser))
1331
+ : [],
1332
+ line: line.number,
1333
+ };
1334
+ if (methodCall)
1335
+ return {
1336
+ type: "methodCall",
1337
+ receiver: methodCall[1],
1338
+ optional: methodCall[2] === "?.",
1339
+ name: methodCall[3],
1340
+ arguments: methodCall[4]
1341
+ ? splitTopLevel(methodCall[4]).map((item) => expr(item, line, parser))
1342
+ : [],
1343
+ line: line.number,
1344
+ };
1345
+ const call = /^([A-Za-z_]\w*)\((.*)\)$/.exec(text);
1346
+ if (call)
1347
+ return {
1348
+ type: "call",
1349
+ name: call[1],
1350
+ arguments: call[2]
1351
+ ? splitTopLevel(call[2]).map((item) => expr(item, line, parser))
1352
+ : [],
1353
+ line: line.number,
1354
+ };
1355
+ if (/^['"].*['"]$/.test(text))
1356
+ return { type: "string", literal: literal(text, line, parser) };
1357
+ if (/^\$(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text)) {
1358
+ if (!Number.isFinite(Number(text.slice(1))))
1359
+ parser.fail(line, "LNG1120", "numeric literal must be finite");
1360
+ return { type: "raw", value: text };
1361
+ }
1362
+ if (
1363
+ /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text) ||
1364
+ /^0[xX][0-9a-fA-F]+$/.test(text)
1365
+ ) {
1366
+ if (!Number.isFinite(Number(text)))
1367
+ parser.fail(line, "LNG1120", "numeric literal must be finite");
1368
+ return { type: "raw", value: text };
1369
+ }
1370
+ if (/^(true|false|null)$/.test(text)) return { type: "raw", value: text };
1371
+ if (/^[A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w*)*(?:\.|\?\.)_[A-Za-z_]\w*$/.test(text))
1372
+ parser.fail(
1373
+ line,
1374
+ "LNG1115",
1375
+ "protected properties are not accessible outside a class",
1376
+ );
1377
+ if (/^[A-Za-z_]\w*(?:(?:\.|\?\.)\$?[A-Za-z_]\w*|(?:\?\.)?\[(?:\d+|[A-Za-z_]\w*|['"][^'"]*['"])\])+$/.test(text))
1378
+ return { type: "raw", value: text };
1379
+ if (/^[A-Za-z_]\w*\[(?:\d+|[A-Za-z_]\w*|['"][^'"]*['"])\]$/.test(text))
1380
+ return { type: "raw", value: text };
1381
+ if (/^(?:\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*)$/.test(text)) return { type: "raw", value: text };
1382
+ parser.fail(line, "LNG1108", `unsupported Faz 2 expression: ${text}`);
1383
+ }
1384
+
1385
+ function xmlLiteral(source, line, parser) {
1386
+ const parts = [];
1387
+ const appendText = (text) => {
1388
+ if (text === "") return;
1389
+ const previous = parts.at(-1);
1390
+ if (previous?.type === "text") previous.value += text;
1391
+ else parts.push({ type: "text", value: text });
1392
+ };
1393
+ const appendInterpolations = (text, allowed) => {
1394
+ let at = 0;
1395
+ while (at < text.length) {
1396
+ const start = text.indexOf("${", at);
1397
+ if (start === -1) {
1398
+ appendText(text.slice(at));
1399
+ return true;
1400
+ }
1401
+ if (!allowed) parser.fail(line, "LNG1248", "XML interpolation is only valid in text or quoted attributes");
1402
+ const end = text.indexOf("}", start + 2);
1403
+ if (end === -1) return false;
1404
+ appendText(text.slice(at, start));
1405
+ const expression = text.slice(start + 2, end).trim();
1406
+ if (!expression)
1407
+ parser.fail(line, "LNG1248", "XML interpolation requires an expression");
1408
+ parts.push({ type: "expression", value: expr(expression, line, parser) });
1409
+ at = end + 1;
1410
+ }
1411
+ return true;
1412
+ };
1413
+ const validateEntities = (text) => {
1414
+ if (/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[\da-fA-F]+;)/.test(text))
1415
+ parser.fail(line, "LNG1249", "XML text and attributes must escape ampersands");
1416
+ };
1417
+ const appendOpenTag = (tag) => {
1418
+ if (tag.startsWith("<${"))
1419
+ parser.fail(line, "LNG1248", "XML interpolation is only valid in text or quoted attributes");
1420
+ const match = /^<([A-Za-z_][\w:.-]*)([\s\S]*)>$/.exec(tag);
1421
+ if (!match) parser.fail(line, "LNG1249", "invalid XML opening tag");
1422
+ let attributes = match[2];
1423
+ const selfClosing = /\/$/.test(attributes.trimEnd());
1424
+ if (selfClosing) attributes = attributes.trimEnd().slice(0, -1);
1425
+ let at = 0;
1426
+ const names = new Set();
1427
+ while (at < attributes.length) {
1428
+ const whitespace = /^[ \t\r\n]+/.exec(attributes.slice(at));
1429
+ if (!whitespace)
1430
+ parser.fail(line, "LNG1249", "XML attributes must be separated by whitespace");
1431
+ at += whitespace[0].length;
1432
+ if (at === attributes.length) break;
1433
+ const name = /^[A-Za-z_][\w:.-]*/.exec(attributes.slice(at));
1434
+ if (!name) parser.fail(line, "LNG1249", "invalid XML attribute name");
1435
+ if (names.has(name[0]))
1436
+ parser.fail(line, "LNG1249", `duplicate XML attribute ${name[0]}`);
1437
+ names.add(name[0]);
1438
+ at += name[0].length;
1439
+ if (attributes[at] !== "=")
1440
+ parser.fail(line, "LNG1249", "XML attributes require quoted values");
1441
+ at += 1;
1442
+ const quote = attributes[at];
1443
+ if (quote !== '"' && quote !== "'")
1444
+ parser.fail(line, "LNG1249", "XML attributes require quoted values");
1445
+ const end = attributes.indexOf(quote, at + 1);
1446
+ if (end === -1) return null;
1447
+ validateEntities(attributes.slice(at + 1, end).replace(/\$\{[^}]*\}/g, ""));
1448
+ at = end + 1;
1449
+ }
1450
+ let quote = null;
1451
+ let segmentStart = 0;
1452
+ for (let index = 0; index < tag.length; index += 1) {
1453
+ if (tag[index] === '"' || tag[index] === "'") {
1454
+ quote = quote === tag[index] ? null : quote ?? tag[index];
1455
+ continue;
1456
+ }
1457
+ if (tag.startsWith("${", index)) {
1458
+ if (!quote)
1459
+ parser.fail(line, "LNG1248", "XML interpolation is only valid in text or quoted attributes");
1460
+ const end = tag.indexOf("}", index + 2);
1461
+ if (end === -1) return null;
1462
+ appendText(tag.slice(segmentStart, index));
1463
+ const expression = tag.slice(index + 2, end).trim();
1464
+ if (!expression)
1465
+ parser.fail(line, "LNG1248", "XML interpolation requires an expression");
1466
+ parts.push({ type: "expression", value: expr(expression, line, parser) });
1467
+ index = end;
1468
+ segmentStart = end + 1;
1469
+ }
1470
+ }
1471
+ appendText(tag.slice(segmentStart));
1472
+ return { name: match[1], selfClosing };
1473
+ };
1474
+
1475
+ const stack = [];
1476
+ let rootSeen = false;
1477
+ let rootClosed = false;
1478
+ let at = 0;
1479
+ while (at < source.length) {
1480
+ if (source[at] !== "<") {
1481
+ const end = source.indexOf("<", at);
1482
+ const text = source.slice(at, end === -1 ? source.length : end);
1483
+ if (rootClosed && text.trim())
1484
+ parser.fail(line, "LNG1249", "XML literal may contain one root element");
1485
+ validateEntities(text.replace(/\$\{[^}]*\}/g, ""));
1486
+ if (!appendInterpolations(text, true)) return null;
1487
+ at = end === -1 ? source.length : end;
1488
+ continue;
1489
+ }
1490
+ if (source.startsWith("<!--", at)) {
1491
+ const end = source.indexOf("-->", at + 4);
1492
+ if (end === -1) return null;
1493
+ appendText(source.slice(at, end + 3));
1494
+ at = end + 3;
1495
+ continue;
1496
+ }
1497
+ const end = xmlTagEnd(source, at);
1498
+ if (end === -1) return null;
1499
+ const tag = source.slice(at, end + 1);
1500
+ if (tag.startsWith("</")) {
1501
+ const match = /^<\/([A-Za-z_][\w:.-]*)\s*>$/.exec(tag);
1502
+ if (!match) parser.fail(line, "LNG1249", "invalid XML closing tag");
1503
+ if (stack.pop() !== match[1])
1504
+ parser.fail(line, "LNG1249", `XML closing tag ${match[1]} does not match its opening tag`);
1505
+ appendText(tag);
1506
+ if (stack.length === 0) rootClosed = true;
1507
+ } else {
1508
+ if (rootClosed) parser.fail(line, "LNG1249", "XML literal may contain one root element");
1509
+ const opening = appendOpenTag(tag);
1510
+ if (opening === null) return null;
1511
+ if (!rootSeen) rootSeen = true;
1512
+ if (!opening.selfClosing) stack.push(opening.name);
1513
+ else if (stack.length === 0) rootClosed = true;
1514
+ }
1515
+ at = end + 1;
1516
+ }
1517
+ if (!rootSeen || stack.length) return null;
1518
+ return { type: "xml", parts, line: line.number };
1519
+ }
1520
+
1521
+ function xmlTagEnd(source, start) {
1522
+ let quote = null;
1523
+ for (let index = start + 1; index < source.length; index += 1) {
1524
+ const character = source[index];
1525
+ if (character === '"' || character === "'") quote = quote === character ? null : quote ?? character;
1526
+ else if (character === ">" && !quote) return index;
1527
+ }
1528
+ return -1;
1529
+ }
1530
+
1531
+ function stripComment(line) {
1532
+ let quote = null;
1533
+ let escaped = false;
1534
+ for (let index = 0; index < line.length; index += 1) {
1535
+ const character = line[index];
1536
+ if (escaped) {
1537
+ escaped = false;
1538
+ continue;
1539
+ }
1540
+ if (quote && character === "\\") {
1541
+ escaped = true;
1542
+ continue;
1543
+ }
1544
+ if (character === '"' || character === "'") {
1545
+ quote = quote === character ? null : quote ?? character;
1546
+ continue;
1547
+ }
1548
+ if (!quote && character === "#") return line.slice(0, index).trimEnd();
1549
+ }
1550
+ return line;
1551
+ }
1552
+
1553
+ function literal(value, line, parser) {
1554
+ const quote = value[0];
1555
+ if (!((quote === "'" || quote === '"') && value.at(-1) === quote))
1556
+ parser.fail(line, "LNG1109", "expected a string literal");
1557
+ const decoded = value
1558
+ .slice(1, -1)
1559
+ .replace(/\\n/g, "\n")
1560
+ .replace(/\\t/g, "\t")
1561
+ .replace(/\\(['"\\])/g, "$1");
1562
+ const parts = [];
1563
+ let at = 0;
1564
+ const regex = /\$\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[\d+\])?)\}/g;
1565
+ let match;
1566
+ while ((match = regex.exec(decoded))) {
1567
+ parts.push({ text: decoded.slice(at, match.index), value: match[1] });
1568
+ at = match.index + match[0].length;
1569
+ }
1570
+ parts.push({ text: decoded.slice(at), value: null });
1571
+ return parts;
1572
+ }
1573
+
1574
+ function validate(program, imports = []) {
1575
+ const names = new Map(program.functions.map((item) => [item.name, item]));
1576
+ const globals = new Set(program.globals.map((item) => item.name));
1577
+ const importNames = [
1578
+ ...imports.flatMap((item) =>
1579
+ item.names.map((name) => name.split(" as ").at(-1)),
1580
+ ),
1581
+ ...program.jsImports.map((item) => item.name),
1582
+ ];
1583
+ const imported = new Set(importNames);
1584
+ const importedGlobals = new Set(
1585
+ imports.flatMap((item) =>
1586
+ (item.selections ?? [])
1587
+ .filter((selection) => selection.kind === "global")
1588
+ .map((selection) => selection.localName),
1589
+ ),
1590
+ );
1591
+ const jsImportNames = new Set(program.jsImports.map((item) => item.name));
1592
+ const importedFunctions = new Map(
1593
+ imports.flatMap((item) =>
1594
+ (item.selections ?? [])
1595
+ .filter((selection) => selection.kind === "function")
1596
+ .map((selection) => [
1597
+ selection.localName,
1598
+ { parameters: selection.parameters },
1599
+ ]),
1600
+ ),
1601
+ );
1602
+ const importedClasses = new Map(
1603
+ imports.flatMap((item) =>
1604
+ (item.selections ?? [])
1605
+ .filter((selection) => selection.kind === "class")
1606
+ .map((selection) => [selection.localName, selection.interface]),
1607
+ ),
1608
+ );
1609
+ const standardFunctions = new Map([
1610
+ ["from_json", 1],
1611
+ ["to_json", 1],
1612
+ ["get_class", 1],
1613
+ ["is_kindof", 2],
1614
+ ["is_number", 1],
1615
+ ["is_string", 1],
1616
+ ["is_bool", 1],
1617
+ ["is_object", 1],
1618
+ ["is_array", 1],
1619
+ ["range", [1, 2, 3]],
1620
+ ["to_js_number", 1],
1621
+ ["start_trans", 0], ["commit_trans", 0], ["rollback_trans", 0], ["trans_state", 0],
1622
+ ]);
1623
+ const hostFunctions = new Set(["fetch"]);
1624
+ const isDecimalValue = (value) =>
1625
+ value &&
1626
+ ((value.type === "raw" && (value.value.startsWith("$") || value.value.includes(".$"))) ||
1627
+ (value.type === "unary" &&
1628
+ value.operator === "negate" &&
1629
+ isDecimalValue(value.value)) ||
1630
+ (value.type === "binary" &&
1631
+ ["+", "-", "*", "/", "%", "**"].includes(value.operator) &&
1632
+ (isDecimalValue(value.left) || isDecimalValue(value.right))) ||
1633
+ (value.type === "binary" &&
1634
+ value.operator === "??" &&
1635
+ ((value.left.type === "raw" && value.left.value === "null" && isDecimalValue(value.right)) ||
1636
+ (value.right.type === "raw" && value.right.value === "null" && isDecimalValue(value.left)) ||
1637
+ (isDecimalValue(value.left) && isDecimalValue(value.right)))));
1638
+ const returns = (body, result = []) => {
1639
+ for (const node of body) {
1640
+ if (node.type === "return") result.push(node.value);
1641
+ [node.thenBody, node.elseBody, node.tryBody, node.catchBody, node.finallyBody]
1642
+ .filter(Boolean)
1643
+ .forEach((child) => returns(child, result));
1644
+ }
1645
+ return result;
1646
+ };
1647
+ const decimalFunctions = new Set(
1648
+ program.functions
1649
+ .filter((fn) => returns(fn.body).some(isDecimalValue))
1650
+ .map((fn) => fn.name),
1651
+ );
1652
+ let decimalFunctionChanged = true;
1653
+ while (decimalFunctionChanged) {
1654
+ decimalFunctionChanged = false;
1655
+ for (const fn of program.functions)
1656
+ if (
1657
+ !decimalFunctions.has(fn.name) &&
1658
+ returns(fn.body).some(
1659
+ (value) => value?.type === "call" && decimalFunctions.has(value.name),
1660
+ )
1661
+ ) {
1662
+ decimalFunctions.add(fn.name);
1663
+ decimalFunctionChanged = true;
1664
+ }
1665
+ }
1666
+ for (const fn of program.functions) {
1667
+ const values = returns(fn.body);
1668
+ const decimalReturn = (value) =>
1669
+ isDecimalValue(value) ||
1670
+ (value?.type === "call" && decimalFunctions.has(value.name));
1671
+ if (values.some(decimalReturn) && values.some((value) => !decimalReturn(value)))
1672
+ throw new LangoError(
1673
+ "LNG1243",
1674
+ fn.line,
1675
+ `${fn.name}: Decimal-returning function has a non-Decimal return path`,
1676
+ );
1677
+ }
1678
+ const validateDecimalAssignments = (body) => {
1679
+ for (const node of body) {
1680
+ if (
1681
+ node.type === "assign" &&
1682
+ !node.name.startsWith("$") &&
1683
+ node.value.type === "call" &&
1684
+ decimalFunctions.has(node.value.name)
1685
+ )
1686
+ throw new LangoError(
1687
+ "LNG1244",
1688
+ 1,
1689
+ `${node.value.name} returns Decimal; assign it to a $ binding or call to_js_number(...)`,
1690
+ );
1691
+ [node.thenBody, node.elseBody, node.tryBody, node.catchBody, node.finallyBody, node.body]
1692
+ .filter(Boolean)
1693
+ .forEach(validateDecimalAssignments);
1694
+ }
1695
+ };
1696
+ const validateSqlParameters = (body, bindings = new Map()) => {
1697
+ for (const node of body) {
1698
+ if (node.type === "assign" && node.value.type === "sql") {
1699
+ bindings.set(node.name, new Set(node.value.parameters));
1700
+ continue;
1701
+ }
1702
+ if (node.type === "assign") {
1703
+ const property = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/.exec(node.name);
1704
+ if (property && bindings.has(property[1]) && !bindings.get(property[1]).has(property[2]))
1705
+ throw new LangoError(
1706
+ "LNG1256",
1707
+ node.value.line ?? 1,
1708
+ `${property[1]}: unknown SQL parameter ${property[2]}`,
1709
+ );
1710
+ if (!property) bindings.delete(node.name);
1711
+ }
1712
+ [node.thenBody, node.elseBody, node.tryBody, node.catchBody, node.finallyBody]
1713
+ .filter(Boolean)
1714
+ .forEach((child) => validateSqlParameters(child, new Map(bindings)));
1715
+ }
1716
+ };
1717
+ program.functions.forEach((fn) => validateDecimalAssignments(fn.body));
1718
+ program.functions.forEach((fn) => validateSqlParameters(fn.body));
1719
+ program.tests.forEach((test) => validateSqlParameters(test.body));
1720
+ program.classes.forEach((classNode) =>
1721
+ classNode.methods.forEach((method) => validateSqlParameters(method.body)),
1722
+ );
1723
+ const nameDiagnostics = [];
1724
+ const duplicateDiagnostics = (items, code, label) => {
1725
+ const seen = new Map();
1726
+ for (const item of items) {
1727
+ const name = typeof item === "string" ? item : item.name;
1728
+ if (seen.has(name)) {
1729
+ const diagnostic = new LangoError(code, item.line ?? 1, `duplicate ${label} ${name}`);
1730
+ const first = seen.get(name);
1731
+ diagnostic.related = [{
1732
+ message: `first ${label} ${name} declaration`,
1733
+ span: {
1734
+ start: { line: first.line ?? 1, column: 1 },
1735
+ end: { line: first.line ?? 1, column: 1 },
1736
+ },
1737
+ }];
1738
+ nameDiagnostics.push(diagnostic);
1739
+ } else {
1740
+ seen.set(name, item);
1741
+ }
1742
+ }
1743
+ };
1744
+ duplicateDiagnostics(program.functions, "LNG1201", "function name");
1745
+ duplicateDiagnostics(program.globals, "LNG1216", "global name");
1746
+ duplicateDiagnostics(importNames, "LNG1228", "imported local name");
1747
+ const declarationNames = new Set([
1748
+ ...program.functions.map((item) => item.name),
1749
+ ...program.classes.map((item) => item.name),
1750
+ ...program.globals.map((item) => item.name),
1751
+ ]);
1752
+ for (const name of imported)
1753
+ if (declarationNames.has(name))
1754
+ nameDiagnostics.push(new LangoError(
1755
+ "LNG1229",
1756
+ 1,
1757
+ `imported local name ${name} conflicts with a Lango declaration`,
1758
+ ));
1759
+ const classes = new Map(program.classes.map((item) => [item.name, item]));
1760
+ duplicateDiagnostics(program.classes, "LNG1205", "class name");
1761
+ if (nameDiagnostics.length) {
1762
+ const [primary] = nameDiagnostics;
1763
+ primary.diagnostics = nameDiagnostics;
1764
+ throw primary;
1765
+ }
1766
+ const classDeclarationDiagnostics = [];
1767
+ for (const classNode of program.classes) {
1768
+ if (classNode.ancestor && !classes.has(classNode.ancestor))
1769
+ classDeclarationDiagnostics.push(new LangoError(
1770
+ "LNG1206",
1771
+ classNode.line,
1772
+ `${classNode.name}: unknown ancestor ${classNode.ancestor}`,
1773
+ ));
1774
+ const publicInstanceNames = new Set();
1775
+ for (const name of [
1776
+ ...classNode.publicProperties,
1777
+ ...classNode.publicMethods
1778
+ .filter((item) => !item.static)
1779
+ .map((item) => item.name),
1780
+ ]) {
1781
+ if (publicInstanceNames.has(name))
1782
+ classDeclarationDiagnostics.push(new LangoError(
1783
+ "LNG1225",
1784
+ classNode.line,
1785
+ `${classNode.name}: duplicate public instance member ${name}`,
1786
+ ));
1787
+ publicInstanceNames.add(name);
1788
+ }
1789
+ const staticNames = new Set();
1790
+ for (const name of [
1791
+ ...classNode.staticValues.map((item) => item.name),
1792
+ ...classNode.enumValues.map((item) => item.name),
1793
+ ...classNode.publicMethods
1794
+ .filter((item) => item.static)
1795
+ .map((item) => item.name),
1796
+ ]) {
1797
+ if (staticNames.has(name))
1798
+ classDeclarationDiagnostics.push(new LangoError(
1799
+ "LNG1224",
1800
+ classNode.line,
1801
+ `${classNode.name}: duplicate static member ${name}`,
1802
+ ));
1803
+ staticNames.add(name);
1804
+ }
1805
+ const implementations = new Set();
1806
+ for (const method of classNode.methods) {
1807
+ const key = `${method.static ? "static" : "instance"}:${method.name}`;
1808
+ if (implementations.has(key))
1809
+ classDeclarationDiagnostics.push(new LangoError(
1810
+ "LNG1226",
1811
+ method.line,
1812
+ `${classNode.name}: duplicate ${method.static ? "static" : "instance"} method ${method.name}`,
1813
+ ));
1814
+ implementations.add(key);
1815
+ }
1816
+ for (const method of classNode.publicMethods) {
1817
+ const implementation = classNode.methods.find(
1818
+ (item) => item.name === method.name && item.static === method.static,
1819
+ );
1820
+ if (!implementation) {
1821
+ classDeclarationDiagnostics.push(new LangoError(
1822
+ "LNG1209",
1823
+ classNode.line,
1824
+ `${classNode.name}: public method ${method.static ? "::" : ""}${method.name} has no implementation`,
1825
+ ));
1826
+ continue;
1827
+ }
1828
+ if (JSON.stringify(method.parameters) !== JSON.stringify(implementation.parameters))
1829
+ classDeclarationDiagnostics.push(new LangoError(
1830
+ "LNG1223",
1831
+ implementation.line,
1832
+ `${classNode.name}: public method ${method.name} signature does not match its declaration`,
1833
+ ));
1834
+ }
1835
+ }
1836
+ if (classDeclarationDiagnostics.length) {
1837
+ const [primary] = classDeclarationDiagnostics;
1838
+ primary.diagnostics = classDeclarationDiagnostics;
1839
+ throw primary;
1840
+ }
1841
+ const ancestorState = new Map();
1842
+ const visitAncestor = (classNode) => {
1843
+ const state = ancestorState.get(classNode.name);
1844
+ if (state === "visiting")
1845
+ throw new LangoError(
1846
+ "LNG1236",
1847
+ classNode.line,
1848
+ `${classNode.name}: cyclic class inheritance`,
1849
+ );
1850
+ if (state === "done") return;
1851
+ ancestorState.set(classNode.name, "visiting");
1852
+ const ancestor = classes.get(classNode.ancestor);
1853
+ if (ancestor) visitAncestor(ancestor);
1854
+ ancestorState.set(classNode.name, "done");
1855
+ };
1856
+ program.classes.forEach(visitAncestor);
1857
+ const closureNames = (body, result = new Set()) => {
1858
+ for (const node of body) {
1859
+ if (node.type === "closureAssign") result.add(node.name);
1860
+ if (
1861
+ node.type === "assign" &&
1862
+ (node.value.type === "call" || node.value.type === "methodCall")
1863
+ )
1864
+ result.add(node.name);
1865
+ [
1866
+ node.body,
1867
+ node.thenBody,
1868
+ node.elseBody,
1869
+ node.tryBody,
1870
+ node.catchBody,
1871
+ node.finallyBody,
1872
+ ]
1873
+ .filter(Boolean)
1874
+ .forEach((child) => closureNames(child, result));
1875
+ }
1876
+ return result;
1877
+ };
1878
+ const dynamicBindingNames = (body, result = new Set()) => {
1879
+ for (const node of body) {
1880
+ if (node.type === "assign" && !node.name.startsWith("@"))
1881
+ result.add(node.name);
1882
+ if (node.type === "closureAssign") result.add(node.name);
1883
+ if (node.type === "for") result.add(node.item);
1884
+ if (node.type === "forObject") {
1885
+ result.add(node.key);
1886
+ result.add(node.value);
1887
+ }
1888
+ [
1889
+ node.body,
1890
+ node.thenBody,
1891
+ node.elseBody,
1892
+ node.tryBody,
1893
+ node.catchBody,
1894
+ node.finallyBody,
1895
+ ...(node.cases?.map((item) => item.body) ?? []),
1896
+ ]
1897
+ .filter(Boolean)
1898
+ .forEach((child) => dynamicBindingNames(child, result));
1899
+ }
1900
+ return result;
1901
+ };
1902
+ const validateRequiredArguments = (target, value, label) => {
1903
+ const missing = target.parameters.findIndex(
1904
+ (parameter, index) =>
1905
+ parameter.defaultValue === null && value.arguments.length <= index,
1906
+ );
1907
+ if (missing !== -1)
1908
+ throw new LangoError(
1909
+ "LNG1203",
1910
+ value.line,
1911
+ `${label} is missing required argument ${missing + 1}`,
1912
+ );
1913
+ };
1914
+ const validateJavaScriptArguments = (value, label) => {
1915
+ const decimalIndex = value.arguments.findIndex(isDecimalValue);
1916
+ if (decimalIndex !== -1)
1917
+ throw new LangoError(
1918
+ "LNG1252",
1919
+ value.line,
1920
+ `${label}: Decimal argument ${decimalIndex + 1} requires to_js_number(...) at a JavaScript boundary`,
1921
+ );
1922
+ };
1923
+ const receiverRoot = (receiver) =>
1924
+ typeof receiver === "string"
1925
+ ? receiver.split(/[.[]/, 1)[0]
1926
+ : receiver?.type === "raw"
1927
+ ? receiver.value.split(/[.[]/, 1)[0]
1928
+ : receiver?.receiver
1929
+ ? receiverRoot(receiver.receiver)
1930
+ : null;
1931
+ const hostReturningFunctions = new Set(
1932
+ program.functions
1933
+ .filter((fn) => returns(fn.body).some(
1934
+ (value) =>
1935
+ (value?.type === "raw" && hostFunctions.has(value.value)) ||
1936
+ (value?.type === "property" && receiverRoot(value.receiver) === "args"),
1937
+ ))
1938
+ .map((fn) => fn.name),
1939
+ );
1940
+ let hostReturningChanged = true;
1941
+ while (hostReturningChanged) {
1942
+ hostReturningChanged = false;
1943
+ for (const fn of program.functions)
1944
+ if (
1945
+ !hostReturningFunctions.has(fn.name) &&
1946
+ returns(fn.body).some(
1947
+ (value) => value?.type === "call" && hostReturningFunctions.has(value.name),
1948
+ )
1949
+ ) {
1950
+ hostReturningFunctions.add(fn.name);
1951
+ hostReturningChanged = true;
1952
+ }
1953
+ }
1954
+ const validateHostAliases = (body, aliases = new Set()) => {
1955
+ const inspect = (value) => {
1956
+ if (!value || typeof value !== "object") return;
1957
+ if (value.type === "call" && aliases.has(value.name))
1958
+ validateJavaScriptArguments(value, value.name);
1959
+ Object.values(value).forEach((item) =>
1960
+ Array.isArray(item) ? item.forEach(inspect) : inspect(item),
1961
+ );
1962
+ };
1963
+ for (const node of body) {
1964
+ if (node.type === "assign") inspect(node.value);
1965
+ else if (node.value) inspect(node.value);
1966
+ if (
1967
+ node.type === "assign" &&
1968
+ ((node.value.type === "raw" && hostFunctions.has(node.value.value)) ||
1969
+ (node.value.type === "raw" && aliases.has(node.value.value)) ||
1970
+ (node.value.type === "call" && hostReturningFunctions.has(node.value.name)) ||
1971
+ (node.value.type === "property" && receiverRoot(node.value.receiver) === "args"))
1972
+ )
1973
+ aliases.add(node.name);
1974
+ else if (node.type === "assign") aliases.delete(node.name);
1975
+ if (node.thenBody) {
1976
+ const thenAliases = validateHostAliases(node.thenBody, new Set(aliases));
1977
+ const elseAliases = node.elseBody
1978
+ ? validateHostAliases(node.elseBody, new Set(aliases))
1979
+ : new Set();
1980
+ for (const name of thenAliases)
1981
+ if (elseAliases.has(name)) aliases.add(name);
1982
+ continue;
1983
+ }
1984
+ if (node.tryBody) {
1985
+ const tryAliases = validateHostAliases(node.tryBody, new Set(aliases));
1986
+ const catchAliases = node.catchBody
1987
+ ? validateHostAliases(node.catchBody, new Set(aliases))
1988
+ : tryAliases;
1989
+ const normalAliases = new Set(
1990
+ [...tryAliases].filter((name) => catchAliases.has(name)),
1991
+ );
1992
+ const finalAliases = node.finallyBody
1993
+ ? validateHostAliases(node.finallyBody, normalAliases)
1994
+ : normalAliases;
1995
+ aliases.clear();
1996
+ finalAliases.forEach((name) => aliases.add(name));
1997
+ continue;
1998
+ }
1999
+ [node.thenBody, node.elseBody, node.tryBody, node.catchBody, node.finallyBody, node.body]
2000
+ .filter(Boolean)
2001
+ .forEach((child) => validateHostAliases(child, new Set(aliases)));
2002
+ }
2003
+ return aliases;
2004
+ };
2005
+ const findMethod = (classNode, name, staticMethod) => {
2006
+ let current = classNode;
2007
+ while (current) {
2008
+ const method = current.methods.find(
2009
+ (item) => item.name === name && item.static === staticMethod,
2010
+ );
2011
+ if (method) return method;
2012
+ current = current.ancestor ? classes.get(current.ancestor) : null;
2013
+ }
2014
+ return null;
2015
+ };
2016
+ const findMethodWithOwner = (classNode, name, staticMethod) => {
2017
+ let current = classNode;
2018
+ while (current) {
2019
+ const method = current.methods.find(
2020
+ (item) => item.name === name && item.static === staticMethod,
2021
+ );
2022
+ if (method) return { method, owner: current };
2023
+ current = current.ancestor ? classes.get(current.ancestor) : null;
2024
+ }
2025
+ return null;
2026
+ };
2027
+ const findConstructor = (classNode) =>
2028
+ findMethod(classNode, "constructor", false);
2029
+ const walk = (value, localCallables, classNode = null, staticMethod = false) => {
2030
+ if (!value || typeof value !== "object") return;
2031
+ if (value.type === "binary" && ["+", "-", "*", "/", "%", "**", ">", "<", ">=", "<="].includes(value.operator)) {
2032
+ const string = (item) => item.type === "string";
2033
+ const number = (item) => item.type === "raw" && /^\d/.test(item.value);
2034
+ const boolean = (item) => item.type === "raw" && /^(true|false)$/.test(item.value);
2035
+ const absent = (item) => item.type === "raw" && item.value === "null";
2036
+ const kind = (item) => string(item) ? "string" : number(item) ? "number" : boolean(item) ? "boolean" : absent(item) ? "null" : null;
2037
+ const leftKind = kind(value.left);
2038
+ const rightKind = kind(value.right);
2039
+ if (leftKind && rightKind &&
2040
+ (leftKind !== rightKind || leftKind === "boolean" || leftKind === "null" ||
2041
+ (leftKind === "string" && value.operator !== "+")))
2042
+ throw new LangoError("LNG1254", value.line ?? 1, "primitive values do not coerce in operators");
2043
+ }
2044
+ if (value.type === "call") {
2045
+ const target = names.get(value.name) ?? importedFunctions.get(value.name);
2046
+ if (
2047
+ !target &&
2048
+ !imported.has(value.name) &&
2049
+ !standardFunctions.has(value.name) &&
2050
+ !hostFunctions.has(value.name) &&
2051
+ !localCallables.has(value.name)
2052
+ )
2053
+ throw new LangoError(
2054
+ "LNG1202",
2055
+ value.line,
2056
+ `unknown Lango function ${value.name}`,
2057
+ );
2058
+ if (target) {
2059
+ validateRequiredArguments(target, value, value.name);
2060
+ value.arguments.forEach((argument, index) => {
2061
+ const parameter = target.parameters[index];
2062
+ if (
2063
+ parameter &&
2064
+ !parameter.name.startsWith("$") &&
2065
+ isDecimalValue(argument)
2066
+ )
2067
+ throw new LangoError(
2068
+ "LNG1245",
2069
+ value.line,
2070
+ `${value.name}: Decimal argument ${index + 1} requires a $ parameter`,
2071
+ );
2072
+ });
2073
+ }
2074
+ const arities = standardFunctions.get(value.name);
2075
+ if (
2076
+ standardFunctions.has(value.name) &&
2077
+ !(Array.isArray(arities) ? arities : [arities]).includes(value.arguments.length)
2078
+ )
2079
+ throw new LangoError(
2080
+ "LNG1219",
2081
+ value.line,
2082
+ `${value.name} expects ${Array.isArray(arities) ? arities.join(" or ") : arities} arguments, got ${value.arguments.length}`,
2083
+ );
2084
+ if (hostFunctions.has(value.name)) validateJavaScriptArguments(value, value.name);
2085
+ }
2086
+ if (value.type === "new") {
2087
+ if (
2088
+ value.name.includes("/") &&
2089
+ !jsImportNames.has(value.name.split("/")[0])
2090
+ )
2091
+ throw new LangoError(
2092
+ "LNG1233",
2093
+ value.line,
2094
+ `${value.name.split("/")[0]} is not an imported JavaScript namespace`,
2095
+ );
2096
+ if (
2097
+ value.name.includes(".") &&
2098
+ jsImportNames.has(value.name.split(".")[0])
2099
+ )
2100
+ throw new LangoError(
2101
+ "LNG1232",
2102
+ value.line,
2103
+ `use / between JavaScript namespace segments in new ${value.name}(...)`,
2104
+ );
2105
+ if (value.name.includes("/")) validateJavaScriptArguments(value, value.name);
2106
+ const target = classes.get(value.name);
2107
+ const constructor = target && findConstructor(target);
2108
+ if (constructor) validateRequiredArguments(constructor, value, value.name);
2109
+ }
2110
+ if (value.type === "methodCall" && typeof value.receiver === "string") {
2111
+ if (
2112
+ value.receiver.includes(".") &&
2113
+ jsImportNames.has(value.receiver.split(".")[0])
2114
+ )
2115
+ throw new LangoError(
2116
+ "LNG1232",
2117
+ value.line,
2118
+ `use / between JavaScript namespace segments in ${value.receiver}.${value.name}(...)`,
2119
+ );
2120
+ const targetClass =
2121
+ value.receiver === "this"
2122
+ ? classNode
2123
+ : classes.get(value.receiver) ?? null;
2124
+ const importedStaticMethod =
2125
+ !targetClass &&
2126
+ importedClasses
2127
+ .get(value.receiver)
2128
+ ?.staticMethods.find((item) => item.name === value.name);
2129
+ const importedClass = !targetClass && importedClasses.get(value.receiver);
2130
+ if (importedStaticMethod) {
2131
+ validateRequiredArguments(
2132
+ importedStaticMethod,
2133
+ value,
2134
+ `${value.receiver}.${value.name}`,
2135
+ );
2136
+ }
2137
+ if (importedClass?.staticListed && !importedStaticMethod)
2138
+ throw new LangoError(
2139
+ "LNG1241",
2140
+ value.line,
2141
+ `${value.receiver}: unknown static method ${value.name}`,
2142
+ );
2143
+ const target =
2144
+ targetClass &&
2145
+ findMethodWithOwner(
2146
+ targetClass,
2147
+ value.name,
2148
+ value.receiver === "this" ? staticMethod : true,
2149
+ );
2150
+ if (targetClass && !target)
2151
+ throw new LangoError(
2152
+ "LNG1241",
2153
+ value.line,
2154
+ `${targetClass.name}: unknown ${value.receiver === "this" ? "instance" : "static"} method ${value.name}`,
2155
+ );
2156
+ if (
2157
+ target &&
2158
+ !target.method.public &&
2159
+ target.owner !== classNode
2160
+ )
2161
+ throw new LangoError(
2162
+ "LNG1242",
2163
+ value.line,
2164
+ `${targetClass.name}: method ${value.name} is private`,
2165
+ );
2166
+ if (target)
2167
+ validateRequiredArguments(
2168
+ target.method,
2169
+ value,
2170
+ `${targetClass.name}${target.method.static ? "::" : ":"}${value.name}`,
2171
+ );
2172
+ }
2173
+ if (value.type === "methodCall" && receiverRoot(value.receiver) === "args")
2174
+ validateJavaScriptArguments(value, `args.${value.name}`);
2175
+ if (value.type === "namespaceMethodCall") {
2176
+ const root = value.receiver.split("/")[0];
2177
+ if (!jsImportNames.has(root))
2178
+ throw new LangoError(
2179
+ "LNG1233",
2180
+ value.line,
2181
+ `${root} is not an imported JavaScript namespace`,
2182
+ );
2183
+ validateJavaScriptArguments(value, `${value.receiver}/${value.name}`);
2184
+ }
2185
+ if (value.type === "superMethodCall" && classNode?.ancestor) {
2186
+ if (staticMethod)
2187
+ throw new LangoError(
2188
+ "LNG1239",
2189
+ value.line,
2190
+ "super.method(...) is only valid in an instance method",
2191
+ );
2192
+ let target = null;
2193
+ for (
2194
+ let ancestor = classes.get(classNode.ancestor);
2195
+ ancestor;
2196
+ ancestor = ancestor.ancestor ? classes.get(ancestor.ancestor) : null
2197
+ ) {
2198
+ const candidate = ancestor.methods.find(
2199
+ (item) => item.name === value.name && !item.static && item.public,
2200
+ );
2201
+ if (candidate) {
2202
+ target = candidate;
2203
+ break;
2204
+ }
2205
+ }
2206
+ if (!target)
2207
+ throw new LangoError(
2208
+ "LNG1240",
2209
+ value.line,
2210
+ `${classNode.name}: unknown or private ancestor method ${value.name}`,
2211
+ );
2212
+ validateRequiredArguments(target, value, `super.${value.name}`);
2213
+ }
2214
+ Object.values(value).forEach((item) =>
2215
+ Array.isArray(item)
2216
+ ? item.forEach((child) => walk(child, localCallables, classNode, staticMethod))
2217
+ : walk(item, localCallables, classNode, staticMethod),
2218
+ );
2219
+ };
2220
+ [...program.functions, ...program.tests].forEach((fn) =>
2221
+ walk(
2222
+ fn,
2223
+ new Set([
2224
+ ...dynamicBindingNames(fn.body),
2225
+ ...(fn.parameters ?? []).map((parameter) => parameter.name),
2226
+ ]),
2227
+ ),
2228
+ );
2229
+ [...program.functions, ...program.tests].forEach((fn) => validateHostAliases(fn.body));
2230
+ program.classes.forEach((classNode) =>
2231
+ classNode.methods.forEach((method) => validateHostAliases(method.body)),
2232
+ );
2233
+ program.classes.forEach((classNode) =>
2234
+ classNode.methods.forEach((method) =>
2235
+ walk(
2236
+ method,
2237
+ new Set([
2238
+ ...dynamicBindingNames(method.body),
2239
+ ...method.parameters.map((parameter) => parameter.name),
2240
+ ]),
2241
+ classNode,
2242
+ method.static,
2243
+ ),
2244
+ ),
2245
+ );
2246
+
2247
+ const expressionReferences = (node, references) => {
2248
+ if (!node || typeof node !== "object") return;
2249
+ if (node.type === "raw") {
2250
+ const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(node.value);
2251
+ if (match) references.add(match[1]);
2252
+ return;
2253
+ }
2254
+ if (node.type === "closure") return;
2255
+ if (node.type === "call") {
2256
+ if (!names.has(node.name) && !imported.has(node.name))
2257
+ references.add(node.name);
2258
+ node.arguments.forEach((argument) =>
2259
+ expressionReferences(argument, references),
2260
+ );
2261
+ return;
2262
+ }
2263
+ if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
2264
+ if (typeof node.receiver === "string" && node.receiver !== "this")
2265
+ references.add(node.receiver.split(/[./]/)[0]);
2266
+ else if (typeof node.receiver !== "string")
2267
+ expressionReferences(node.receiver, references);
2268
+ node.arguments.forEach((argument) =>
2269
+ expressionReferences(argument, references),
2270
+ );
2271
+ return;
2272
+ }
2273
+ if (node.type === "index") {
2274
+ if (typeof node.receiver === "string")
2275
+ references.add(node.receiver.split(/[./]/)[0]);
2276
+ else expressionReferences(node.receiver, references);
2277
+ expressionReferences(node.index, references);
2278
+ return;
2279
+ }
2280
+ if (node.type === "property") {
2281
+ expressionReferences(node.receiver, references);
2282
+ return;
2283
+ }
2284
+ if (node.type === "string" || node.type === "translate") {
2285
+ node.literal
2286
+ .filter((part) => part.value)
2287
+ .forEach((part) => {
2288
+ const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(part.value);
2289
+ if (match) references.add(match[1]);
2290
+ });
2291
+ return;
2292
+ }
2293
+ Object.values(node).forEach((value) => {
2294
+ if (Array.isArray(value))
2295
+ value.forEach((item) => expressionReferences(item, references));
2296
+ else expressionReferences(value, references);
2297
+ });
2298
+ };
2299
+
2300
+ const nestedBodies = (node) => [
2301
+ node.thenBody,
2302
+ node.elseBody,
2303
+ node.tryBody,
2304
+ node.catchBody,
2305
+ node.finallyBody,
2306
+ node.body,
2307
+ ...(node.cases?.map((item) => item.body) ?? []),
2308
+ ].filter(Boolean);
2309
+
2310
+ const reachesEnd = (nodes) => {
2311
+ for (const node of nodes) {
2312
+ if (
2313
+ node.type === "return" ||
2314
+ node.type === "throw" ||
2315
+ node.type === "break" ||
2316
+ node.type === "continue"
2317
+ ) return false;
2318
+ if (
2319
+ node.type === "if" &&
2320
+ node.elseBody &&
2321
+ !reachesEnd(node.thenBody) &&
2322
+ !reachesEnd(node.elseBody)
2323
+ )
2324
+ return false;
2325
+ if (
2326
+ node.type === "switch" &&
2327
+ node.elseBody &&
2328
+ node.cases.every((item) => !reachesEnd(item.body)) &&
2329
+ !reachesEnd(node.elseBody)
2330
+ )
2331
+ return false;
2332
+ if (node.type === "try") {
2333
+ if (node.finallyBody && !reachesEnd(node.finallyBody)) return false;
2334
+ if (node.catchBody)
2335
+ return reachesEnd(node.tryBody) || reachesEnd(node.catchBody);
2336
+ return reachesEnd(node.tryBody);
2337
+ }
2338
+ }
2339
+ return true;
2340
+ };
2341
+
2342
+ const contextDeclarations = (body, names = new Set()) => {
2343
+ for (const node of body) {
2344
+ if (node.type === "contextDeclare") names.add(node.name);
2345
+ if (node.type !== "closureAssign")
2346
+ nestedBodies(node).forEach((child) => contextDeclarations(child, names));
2347
+ }
2348
+ return names;
2349
+ };
2350
+
2351
+ const globalDeclarations = (body, names = new Set()) => {
2352
+ for (const node of body) {
2353
+ if (node.type === "globalDeclare") names.add(node.name);
2354
+ if (node.type !== "closureAssign")
2355
+ nestedBodies(node).forEach((child) => globalDeclarations(child, names));
2356
+ }
2357
+ return names;
2358
+ };
2359
+
2360
+ const directBindings = (body, bindings = new Set()) => {
2361
+ for (const node of body) {
2362
+ if (node.type === "closureAssign") {
2363
+ bindings.add(node.name);
2364
+ continue;
2365
+ }
2366
+ if (node.type === "assign" && !node.name.startsWith("@"))
2367
+ bindings.add(node.name);
2368
+ if (node.type === "for") bindings.add(node.item);
2369
+ if (node.type === "forObject") {
2370
+ bindings.add(node.key);
2371
+ bindings.add(node.value);
2372
+ }
2373
+ nestedBodies(node).forEach((child) => directBindings(child, bindings));
2374
+ }
2375
+ return bindings;
2376
+ };
2377
+
2378
+ const directReferences = (body, references = new Set()) => {
2379
+ for (const node of body) {
2380
+ if (node.type === "closureAssign" || node.type === "closure") continue;
2381
+ if (node.type === "increment" || node.type === "add")
2382
+ references.add(node.name ?? node.target);
2383
+ if (node.type === "indexAssign") {
2384
+ references.add(node.receiver.split(".")[0]);
2385
+ (node.indexes ?? [node.index]).forEach((index) =>
2386
+ expressionReferences(index, references),
2387
+ );
2388
+ }
2389
+ if (node.type === "for" || node.type === "forObject")
2390
+ expressionReferences(node.collection, references);
2391
+ [node.value, node.condition, node.arguments, node.tests].forEach((value) => {
2392
+ if (Array.isArray(value))
2393
+ value.forEach((item) => expressionReferences(item, references));
2394
+ else expressionReferences(value, references);
2395
+ });
2396
+ nestedBodies(node).forEach((child) => directReferences(child, references));
2397
+ }
2398
+ return references;
2399
+ };
2400
+
2401
+ const validateClosures = (
2402
+ body,
2403
+ outerBindings,
2404
+ classScope = null,
2405
+ inheritedExpiredCatchBindings = new Set(),
2406
+ ) => {
2407
+ const available = new Set(outerBindings);
2408
+ const expiredCatchBindings = new Set(inheritedExpiredCatchBindings);
2409
+ for (const node of body) {
2410
+ const references = new Set();
2411
+ [node.value, node.condition, node.collection, node.arguments, node.tests].forEach((value) => {
2412
+ if (Array.isArray(value)) value.forEach((item) => expressionReferences(item, references));
2413
+ else expressionReferences(value, references);
2414
+ });
2415
+ for (const name of references)
2416
+ if (expiredCatchBindings.has(name))
2417
+ throw new LangoError(
2418
+ "LNG1261",
2419
+ node.line ?? 1,
2420
+ `${name} is only available inside its catch block`,
2421
+ );
2422
+ if (node.type === "if") {
2423
+ const left = validateClosures(
2424
+ node.thenBody,
2425
+ new Set(available),
2426
+ classScope,
2427
+ expiredCatchBindings,
2428
+ );
2429
+ const right = node.elseBody
2430
+ ? validateClosures(
2431
+ node.elseBody,
2432
+ new Set(available),
2433
+ classScope,
2434
+ expiredCatchBindings,
2435
+ )
2436
+ : available;
2437
+ const exits = [];
2438
+ if (reachesEnd(node.thenBody)) exits.push(left);
2439
+ if (!node.elseBody || reachesEnd(node.elseBody)) exits.push(right);
2440
+ for (const name of exits[0] ?? [])
2441
+ if (exits.every((exit) => exit.has(name))) available.add(name);
2442
+ continue;
2443
+ }
2444
+ if (node.type === "switch") {
2445
+ const exits = node.cases
2446
+ .filter((item) => reachesEnd(item.body))
2447
+ .map((item) =>
2448
+ validateClosures(
2449
+ item.body,
2450
+ new Set(available),
2451
+ classScope,
2452
+ expiredCatchBindings,
2453
+ ),
2454
+ );
2455
+ if (!node.elseBody || reachesEnd(node.elseBody))
2456
+ exits.push(
2457
+ node.elseBody
2458
+ ? validateClosures(
2459
+ node.elseBody,
2460
+ new Set(available),
2461
+ classScope,
2462
+ expiredCatchBindings,
2463
+ )
2464
+ : new Set(available),
2465
+ );
2466
+ for (const name of exits[0] ?? [])
2467
+ if (exits.every((exit) => exit.has(name))) available.add(name);
2468
+ continue;
2469
+ }
2470
+ if (node.type === "try") {
2471
+ const catchWasAvailable = node.catchName && available.has(node.catchName);
2472
+ const exits = [];
2473
+ const tryExit = validateClosures(
2474
+ node.tryBody,
2475
+ new Set(available),
2476
+ classScope,
2477
+ expiredCatchBindings,
2478
+ );
2479
+ if (reachesEnd(node.tryBody)) exits.push(tryExit);
2480
+ if (node.catchBody) {
2481
+ const catchExit = validateClosures(
2482
+ node.catchBody,
2483
+ new Set([...available, node.catchName]),
2484
+ classScope,
2485
+ new Set(
2486
+ [...expiredCatchBindings].filter((name) => name !== node.catchName),
2487
+ ),
2488
+ );
2489
+ if (reachesEnd(node.catchBody)) exits.push(catchExit);
2490
+ }
2491
+ const certain = new Set(exits[0] ?? available);
2492
+ for (const name of certain)
2493
+ if (!exits.every((exit) => exit.has(name))) certain.delete(name);
2494
+ if (node.catchName && !catchWasAvailable) certain.delete(node.catchName);
2495
+ const afterFinally = node.finallyBody
2496
+ ? validateClosures(
2497
+ node.finallyBody,
2498
+ certain,
2499
+ classScope,
2500
+ new Set([...expiredCatchBindings, node.catchName].filter(Boolean)),
2501
+ )
2502
+ : certain;
2503
+ if (!node.finallyBody || reachesEnd(node.finallyBody))
2504
+ afterFinally.forEach((name) => available.add(name));
2505
+ if (node.catchName && !catchWasAvailable) expiredCatchBindings.add(node.catchName);
2506
+ continue;
2507
+ }
2508
+ if (node.type === "for") {
2509
+ validateClosures(
2510
+ node.body,
2511
+ new Set([...available, node.item]),
2512
+ classScope,
2513
+ expiredCatchBindings,
2514
+ );
2515
+ continue;
2516
+ }
2517
+ if (node.type === "forObject") {
2518
+ validateClosures(
2519
+ node.body,
2520
+ new Set([...available, node.key, node.value]),
2521
+ classScope,
2522
+ expiredCatchBindings,
2523
+ );
2524
+ continue;
2525
+ }
2526
+ const chain = node.type === "chain"
2527
+ ? node
2528
+ : node.type === "return" && node.value?.type === "chain"
2529
+ ? node.value
2530
+ : null;
2531
+ if (chain) {
2532
+ for (const step of chain.steps)
2533
+ validateClosures(
2534
+ [{ type: "return", value: { type: "closure", ...step } }],
2535
+ new Set(available),
2536
+ classScope,
2537
+ expiredCatchBindings,
2538
+ );
2539
+ continue;
2540
+ }
2541
+ const closure =
2542
+ node.type === "closureAssign"
2543
+ ? node
2544
+ : node.type === "return" && node.value?.type === "closure"
2545
+ ? node.value
2546
+ : null;
2547
+ if (closure) {
2548
+ const localBindings = new Set([
2549
+ ...closure.parameters.map((parameter) => parameter.name),
2550
+ ...directBindings(closure.body),
2551
+ ]);
2552
+ if (classScope)
2553
+ [...localBindings]
2554
+ .filter((name) => name.startsWith("_"))
2555
+ .forEach((name) => localBindings.delete(name));
2556
+ closure.captures
2557
+ .filter((name) => !name.startsWith("@") && !name.startsWith("_"))
2558
+ .forEach((name) => localBindings.delete(name));
2559
+ const contextNames = contextDeclarations(closure.body);
2560
+ const globalNames = globalDeclarations(closure.body);
2561
+ const needed = [...directReferences(closure.body)].filter(
2562
+ (name) =>
2563
+ !localBindings.has(name) &&
2564
+ !contextNames.has(name) &&
2565
+ !globalNames.has(name),
2566
+ );
2567
+ const captures = new Set(closure.captures);
2568
+ for (const name of needed)
2569
+ if (!captures.has(name))
2570
+ throw new LangoError(
2571
+ "LNG1210",
2572
+ closure.line,
2573
+ `MissingCapture: ${name}; add capture ${name}`,
2574
+ );
2575
+ for (const name of captures) {
2576
+ if (!needed.includes(name))
2577
+ throw new LangoError(
2578
+ "LNG1211",
2579
+ closure.line,
2580
+ `unused capture ${name}`,
2581
+ );
2582
+ const protectedMember = Boolean(classScope && name.startsWith("_"));
2583
+ const propertyCapture = name.startsWith("@") || protectedMember;
2584
+ if (!propertyCapture && !available.has(name))
2585
+ throw new LangoError(
2586
+ "LNG1212",
2587
+ closure.line,
2588
+ `capture ${name} is not assigned before this closure`,
2589
+ );
2590
+ if (propertyCapture) {
2591
+ const staticMember = name.startsWith("@@");
2592
+ const property = name.startsWith("@")
2593
+ ? name.slice(staticMember ? 2 : 1)
2594
+ : name;
2595
+ const members = staticMember
2596
+ ? classScope?.staticMembers
2597
+ : classScope?.instanceMembers;
2598
+ if (!members)
2599
+ throw new LangoError(
2600
+ "LNG1213",
2601
+ closure.line,
2602
+ `${name} is only valid in a class method`,
2603
+ );
2604
+ if (!members.has(property))
2605
+ throw new LangoError(
2606
+ "LNG1214",
2607
+ closure.line,
2608
+ `unknown captured property ${name}`,
2609
+ );
2610
+ }
2611
+ }
2612
+ validateClosures(
2613
+ closure.body,
2614
+ new Set([...available, ...localBindings]),
2615
+ classScope,
2616
+ expiredCatchBindings,
2617
+ );
2618
+ if (node.type === "closureAssign") available.add(node.name);
2619
+ continue;
2620
+ }
2621
+ if (node.type === "assign" && !node.name.startsWith("@"))
2622
+ available.add(node.name);
2623
+ if (!reachesEnd([node])) return available;
2624
+ }
2625
+ return available;
2626
+ };
2627
+ const validateDefiniteAssignments = (body, initial = new Set()) => {
2628
+ const candidates = directBindings(body);
2629
+ const visit = (nodes, assigned) => {
2630
+ const current = new Set(assigned);
2631
+ for (const node of nodes) {
2632
+ const references = new Set();
2633
+ if (node.type === "indexAssign") {
2634
+ references.add(node.receiver.split(".")[0]);
2635
+ (node.indexes ?? [node.index]).forEach((index) =>
2636
+ expressionReferences(index, references),
2637
+ );
2638
+ }
2639
+ if (node.type === "increment" || node.type === "add")
2640
+ references.add(node.name ?? node.target);
2641
+ if (
2642
+ node.type === "call" ||
2643
+ node.type === "methodCall" ||
2644
+ node.type === "namespaceMethodCall"
2645
+ )
2646
+ expressionReferences(node, references);
2647
+ [node.value, node.condition, node.collection, node.arguments, node.tests].forEach((value) => {
2648
+ if (Array.isArray(value)) value.forEach((item) => expressionReferences(item, references));
2649
+ else expressionReferences(value, references);
2650
+ });
2651
+ if (node.type === "switch")
2652
+ node.cases.forEach((item) =>
2653
+ item.tests.forEach((value) => expressionReferences(value, references)),
2654
+ );
2655
+ for (const name of references)
2656
+ if (candidates.has(name) && !current.has(name))
2657
+ throw new LangoError("LNG1246", node.line ?? 1, `${name} is not assigned on every path`);
2658
+ if (node.type === "if") {
2659
+ const left = visit(node.thenBody, current);
2660
+ const right = node.elseBody ? visit(node.elseBody, current) : current;
2661
+ const exits = [];
2662
+ if (reachesEnd(node.thenBody)) exits.push(left);
2663
+ if (!node.elseBody || reachesEnd(node.elseBody)) exits.push(right);
2664
+ for (const name of exits[0] ?? [])
2665
+ if (exits.every((exit) => exit.has(name))) current.add(name);
2666
+ continue;
2667
+ }
2668
+ if (node.type === "switch") {
2669
+ const exits = node.cases
2670
+ .filter((item) => reachesEnd(item.body))
2671
+ .map((item) => visit(item.body, current));
2672
+ if (!node.elseBody || reachesEnd(node.elseBody))
2673
+ exits.push(node.elseBody ? visit(node.elseBody, current) : new Set(current));
2674
+ for (const name of exits[0] ?? [])
2675
+ if (exits.every((exit) => exit.has(name))) current.add(name);
2676
+ continue;
2677
+ }
2678
+ if (node.type === "try") {
2679
+ const normalExits = [];
2680
+ const tryExit = visit(node.tryBody, current);
2681
+ if (reachesEnd(node.tryBody)) normalExits.push(tryExit);
2682
+ if (node.catchBody) {
2683
+ const catchExit = visit(
2684
+ node.catchBody,
2685
+ new Set([...current, node.catchName]),
2686
+ );
2687
+ if (reachesEnd(node.catchBody)) normalExits.push(catchExit);
2688
+ }
2689
+ const certain = new Set(normalExits[0] ?? current);
2690
+ for (const name of certain)
2691
+ if (!normalExits.every((exit) => exit.has(name))) certain.delete(name);
2692
+ if (node.catchName) certain.delete(node.catchName);
2693
+ if (node.finallyBody) {
2694
+ const afterFinally = visit(node.finallyBody, certain);
2695
+ if (reachesEnd(node.finallyBody))
2696
+ afterFinally.forEach((name) => current.add(name));
2697
+ } else certain.forEach((name) => current.add(name));
2698
+ continue;
2699
+ }
2700
+ if (node.type === "loop" || node.type === "for" || node.type === "forObject") {
2701
+ const loopBindings = new Set(current);
2702
+ if (node.type === "for") loopBindings.add(node.item);
2703
+ if (node.type === "forObject") {
2704
+ loopBindings.add(node.key);
2705
+ loopBindings.add(node.value);
2706
+ }
2707
+ visit(node.body, loopBindings);
2708
+ continue;
2709
+ }
2710
+ if (node.type === "assign" || node.type === "closureAssign") current.add(node.name);
2711
+ }
2712
+ return current;
2713
+ };
2714
+ visit(body, initial);
2715
+ };
2716
+ program.functions.forEach((fn) =>
2717
+ validateDefiniteAssignments(
2718
+ fn.body,
2719
+ new Set([...fn.parameters.map((item) => item.name), ...globals]),
2720
+ ),
2721
+ );
2722
+ program.tests.forEach((test) =>
2723
+ validateDefiniteAssignments(test.body, new Set([...globals, ...imported])),
2724
+ );
2725
+
2726
+ program.functions.forEach((fn) =>
2727
+ validateClosures(
2728
+ fn.body,
2729
+ new Set([
2730
+ ...fn.parameters.map((parameter) => parameter.name),
2731
+ ...imported,
2732
+ ]),
2733
+ ),
2734
+ );
2735
+ program.tests.forEach((test) =>
2736
+ validateClosures(test.body, new Set(imported)),
2737
+ );
2738
+ const validateContextAssignments = (body, contextNames) => {
2739
+ for (const node of body) {
2740
+ if (node.type === "assign" && contextNames.has(node.name))
2741
+ throw new LangoError(
2742
+ "LNG1215",
2743
+ 1,
2744
+ `${node.name} is a context binding; use context ${node.name} = ...`,
2745
+ );
2746
+ if (node.type !== "closureAssign")
2747
+ nestedBodies(node).forEach((child) =>
2748
+ validateContextAssignments(child, contextNames),
2749
+ );
2750
+ }
2751
+ };
2752
+ const validateGlobalAccess = (body, declarations = new Set()) => {
2753
+ for (const node of body) {
2754
+ if (node.type === "assign" && importedGlobals.has(node.name))
2755
+ throw new LangoError(
2756
+ "LNG1250",
2757
+ node.line ?? 1,
2758
+ `${node.name} is an imported global and cannot be assigned here`,
2759
+ );
2760
+ if (node.type === "globalDeclare") {
2761
+ if (!globals.has(node.name))
2762
+ throw new LangoError(
2763
+ "LNG1217",
2764
+ node.line,
2765
+ `unknown global ${node.name}`,
2766
+ );
2767
+ declarations.add(node.name);
2768
+ }
2769
+ if (node.type === "assign" && globals.has(node.name) && !declarations.has(node.name))
2770
+ throw new LangoError(
2771
+ "LNG1218",
2772
+ 1,
2773
+ `${node.name} is a global; declare global ${node.name} first`,
2774
+ );
2775
+ if (node.type === "closureAssign") {
2776
+ validateGlobalAccess(node.body);
2777
+ continue;
2778
+ }
2779
+ if (node.type === "return" && node.value?.type === "closure") {
2780
+ validateGlobalAccess(node.value.body);
2781
+ continue;
2782
+ }
2783
+ if (node.type !== "closureAssign")
2784
+ nestedBodies(node).forEach((child) =>
2785
+ validateGlobalAccess(child, declarations),
2786
+ );
2787
+ }
2788
+ for (const name of directReferences(body))
2789
+ if (globals.has(name) && !declarations.has(name))
2790
+ throw new LangoError(
2791
+ "LNG1218",
2792
+ 1,
2793
+ `${name} is a global; declare global ${name} first`,
2794
+ );
2795
+ };
2796
+ program.functions.forEach((fn) =>
2797
+ validateContextAssignments(fn.body, contextDeclarations(fn.body)),
2798
+ );
2799
+ program.tests.forEach((test) =>
2800
+ validateContextAssignments(test.body, contextDeclarations(test.body)),
2801
+ );
2802
+ program.functions.forEach((fn) => validateGlobalAccess(fn.body));
2803
+ program.tests.forEach((test) => validateGlobalAccess(test.body));
2804
+ const loopLevelDiagnostics = [];
2805
+ const validateLoopLevels = (body, depth = 0) => {
2806
+ for (const node of body) {
2807
+ if (
2808
+ (node.type === "break" || node.type === "continue") &&
2809
+ node.level > depth
2810
+ )
2811
+ loopLevelDiagnostics.push(new LangoError(
2812
+ "LNG1220",
2813
+ node.line,
2814
+ `${node.type} ${node.level} exceeds enclosing loop depth ${depth}`,
2815
+ ));
2816
+ const childDepth = ["loop", "for", "forObject"].includes(node.type)
2817
+ ? depth + 1
2818
+ : depth;
2819
+ nestedBodies(node).forEach((child) => validateLoopLevels(child, childDepth));
2820
+ }
2821
+ };
2822
+ program.functions.forEach((fn) => validateLoopLevels(fn.body));
2823
+ program.tests.forEach((test) => validateLoopLevels(test.body));
2824
+ for (const classNode of program.classes) {
2825
+ if (classNode.ancestor && !classes.has(classNode.ancestor))
2826
+ throw new LangoError(
2827
+ "LNG1206",
2828
+ classNode.line,
2829
+ `${classNode.name}: unknown ancestor ${classNode.ancestor}`,
2830
+ );
2831
+ const publicInstanceNames = new Set();
2832
+ for (const name of [
2833
+ ...classNode.publicProperties,
2834
+ ...classNode.publicMethods
2835
+ .filter((item) => !item.static)
2836
+ .map((item) => item.name),
2837
+ ]) {
2838
+ if (publicInstanceNames.has(name))
2839
+ throw new LangoError(
2840
+ "LNG1225",
2841
+ classNode.line,
2842
+ `${classNode.name}: duplicate public instance member ${name}`,
2843
+ );
2844
+ publicInstanceNames.add(name);
2845
+ }
2846
+ const staticNames = new Set();
2847
+ for (const name of [
2848
+ ...classNode.staticValues.map((item) => item.name),
2849
+ ...classNode.enumValues.map((item) => item.name),
2850
+ ...classNode.publicMethods
2851
+ .filter((item) => item.static)
2852
+ .map((item) => item.name),
2853
+ ]) {
2854
+ if (staticNames.has(name))
2855
+ throw new LangoError(
2856
+ "LNG1224",
2857
+ classNode.line,
2858
+ `${classNode.name}: duplicate static member ${name}`,
2859
+ );
2860
+ staticNames.add(name);
2861
+ }
2862
+ const inheritedMethods = [];
2863
+ for (
2864
+ let ancestor = classNode.ancestor ? classes.get(classNode.ancestor) : null;
2865
+ ancestor;
2866
+ ancestor = ancestor.ancestor ? classes.get(ancestor.ancestor) : null
2867
+ )
2868
+ inheritedMethods.push(...ancestor.methods);
2869
+ for (const method of classNode.methods) {
2870
+ if (method.name === "constructor") continue;
2871
+ const inherited = inheritedMethods.find(
2872
+ (candidate) =>
2873
+ candidate.name === method.name && candidate.static === method.static,
2874
+ );
2875
+ if (!inherited) continue;
2876
+ if (!inherited.public)
2877
+ throw new LangoError(
2878
+ "LNG1237",
2879
+ method.line,
2880
+ `${classNode.name}: method ${method.name} conflicts with an ancestor private method`,
2881
+ );
2882
+ if (
2883
+ !classNode.publicMethods.some(
2884
+ (candidate) =>
2885
+ candidate.name === method.name && candidate.static === method.static,
2886
+ )
2887
+ )
2888
+ throw new LangoError(
2889
+ "LNG1238",
2890
+ method.line,
2891
+ `${classNode.name}: override of public method ${method.name} cannot be private`,
2892
+ );
2893
+ }
2894
+ const implementations = new Set();
2895
+ for (const method of classNode.methods) {
2896
+ const key = `${method.static ? "static" : "instance"}:${method.name}`;
2897
+ if (implementations.has(key))
2898
+ throw new LangoError(
2899
+ "LNG1226",
2900
+ method.line,
2901
+ `${classNode.name}: duplicate ${method.static ? "static" : "instance"} method ${method.name}`,
2902
+ );
2903
+ implementations.add(key);
2904
+ }
2905
+ for (const method of classNode.publicMethods) {
2906
+ const implementation = classNode.methods.find(
2907
+ (item) => item.name === method.name && item.static === method.static,
2908
+ );
2909
+ if (!implementation)
2910
+ throw new LangoError(
2911
+ "LNG1209",
2912
+ classNode.line,
2913
+ `${classNode.name}: public method ${method.static ? "::" : ""}${method.name} has no implementation`,
2914
+ );
2915
+ const declaredParameters = JSON.stringify(method.parameters);
2916
+ const implementedParameters = JSON.stringify(implementation.parameters);
2917
+ if (declaredParameters !== implementedParameters)
2918
+ throw new LangoError(
2919
+ "LNG1223",
2920
+ implementation.line,
2921
+ `${classNode.name}: public method ${method.name} signature does not match its declaration`,
2922
+ );
2923
+ }
2924
+ const readonly = new Set(classNode.enumValues.map((item) => item.name));
2925
+ const superCalls = [];
2926
+ const hasSuperMethodCall = (value) =>
2927
+ value &&
2928
+ typeof value === "object" &&
2929
+ (value.type === "superMethodCall" ||
2930
+ Object.values(value).some((item) =>
2931
+ Array.isArray(item)
2932
+ ? item.some(hasSuperMethodCall)
2933
+ : hasSuperMethodCall(item),
2934
+ ));
2935
+ const validateMethod = (body) =>
2936
+ body.forEach((node) => {
2937
+ if (
2938
+ node.type === "assign" &&
2939
+ node.name.startsWith("@@") &&
2940
+ readonly.has(node.name.slice(2))
2941
+ )
2942
+ throw new LangoError(
2943
+ "LNG1204",
2944
+ classNode.line,
2945
+ `${classNode.name}: enum value ${node.name.slice(2)} is readonly`,
2946
+ );
2947
+ if (node.type === "superConstructor") superCalls.push(node);
2948
+ [
2949
+ node.thenBody,
2950
+ node.elseBody,
2951
+ node.tryBody,
2952
+ node.catchBody,
2953
+ node.finallyBody,
2954
+ node.body,
2955
+ ]
2956
+ .filter(Boolean)
2957
+ .forEach(validateMethod);
2958
+ });
2959
+ const instanceMembers = new Set(classNode.publicProperties);
2960
+ const collectInstanceMembers = (body) => {
2961
+ for (const node of body) {
2962
+ if (node.type === "assign" && /^@[A-Za-z_]\w*$/.test(node.name))
2963
+ instanceMembers.add(node.name.slice(1));
2964
+ if (node.type === "assign" && /^_[A-Za-z_]\w*$/.test(node.name))
2965
+ instanceMembers.add(node.name);
2966
+ nestedBodies(node).forEach(collectInstanceMembers);
2967
+ }
2968
+ };
2969
+ classNode.methods.forEach((method) => collectInstanceMembers(method.body));
2970
+ const collectInheritedProtectedMembers = (body, members) => {
2971
+ for (const node of body) {
2972
+ if (node.type === "assign" && /^_[A-Za-z_]\w*$/.test(node.name))
2973
+ members.add(node.name);
2974
+ nestedBodies(node).forEach((child) =>
2975
+ collectInheritedProtectedMembers(child, members),
2976
+ );
2977
+ }
2978
+ };
2979
+ const inheritedInstanceMembers = new Set();
2980
+ const inheritedStaticMembers = new Set();
2981
+ let ancestor = classNode.ancestor ? classes.get(classNode.ancestor) : null;
2982
+ while (ancestor) {
2983
+ ancestor.publicProperties.forEach((name) => inheritedInstanceMembers.add(name));
2984
+ ancestor.methods.forEach((method) =>
2985
+ collectInheritedProtectedMembers(method.body, inheritedInstanceMembers),
2986
+ );
2987
+ ancestor.staticValues.forEach((value) => inheritedStaticMembers.add(value.name));
2988
+ ancestor.enumValues.forEach((value) => {
2989
+ inheritedStaticMembers.add(value.name);
2990
+ readonly.add(value.name);
2991
+ });
2992
+ ancestor = ancestor.ancestor ? classes.get(ancestor.ancestor) : null;
2993
+ }
2994
+ const classScope = {
2995
+ instanceMembers: new Set([...instanceMembers, ...inheritedInstanceMembers]),
2996
+ staticMembers: new Set([
2997
+ ...classNode.staticValues.map((item) => item.name),
2998
+ ...classNode.enumValues.map((item) => item.name),
2999
+ ...inheritedStaticMembers,
3000
+ ]),
3001
+ };
3002
+ const validateProperties = (value) => {
3003
+ if (!value || typeof value !== "object") return;
3004
+ const name =
3005
+ value.type === "assign"
3006
+ ? value.name
3007
+ : value.type === "raw"
3008
+ ? value.value
3009
+ : null;
3010
+ if (name && /^@@[A-Za-z_]\w*$/.test(name)) {
3011
+ if (!classScope.staticMembers.has(name.slice(2)))
3012
+ throw new LangoError(
3013
+ "LNG1221",
3014
+ classNode.line,
3015
+ `${classNode.name}: unknown static property ${name}`,
3016
+ );
3017
+ }
3018
+ if (name && /^@[A-Za-z_]\w*$/.test(name)) {
3019
+ if (!classScope.instanceMembers.has(name.slice(1)))
3020
+ throw new LangoError(
3021
+ "LNG1222",
3022
+ classNode.line,
3023
+ `${classNode.name}: unknown instance property ${name}`,
3024
+ );
3025
+ }
3026
+ if (name && /^_[A-Za-z_]\w*$/.test(name)) {
3027
+ if (!classScope.instanceMembers.has(name))
3028
+ throw new LangoError(
3029
+ "LNG1222",
3030
+ classNode.line,
3031
+ `${classNode.name}: unknown protected property ${name}`,
3032
+ );
3033
+ }
3034
+ Object.values(value).forEach((item) => {
3035
+ if (Array.isArray(item)) item.forEach(validateProperties);
3036
+ else validateProperties(item);
3037
+ });
3038
+ };
3039
+ const validateStaticInstanceAccess = (value) => {
3040
+ if (!value || typeof value !== "object") return;
3041
+ const name =
3042
+ value.type === "assign"
3043
+ ? value.name
3044
+ : value.type === "raw"
3045
+ ? value.value
3046
+ : null;
3047
+ if (
3048
+ name &&
3049
+ (/^@[A-Za-z_]\w*$/.test(name) || /^_[A-Za-z_]\w*$/.test(name))
3050
+ )
3051
+ throw new LangoError(
3052
+ "LNG1231",
3053
+ classNode.line,
3054
+ `${classNode.name}: static method cannot access instance property ${name}`,
3055
+ );
3056
+ Object.values(value).forEach((item) => {
3057
+ if (Array.isArray(item)) item.forEach(validateStaticInstanceAccess);
3058
+ else validateStaticInstanceAccess(item);
3059
+ });
3060
+ };
3061
+ classNode.methods.forEach((method) => {
3062
+ validateDefiniteAssignments(
3063
+ method.body,
3064
+ new Set([...method.parameters.map((parameter) => parameter.name), ...globals]),
3065
+ );
3066
+ validateClosures(
3067
+ method.body,
3068
+ new Set([
3069
+ ...method.parameters.map((parameter) => parameter.name),
3070
+ ...imported,
3071
+ ]),
3072
+ classScope,
3073
+ );
3074
+ validateContextAssignments(
3075
+ method.body,
3076
+ contextDeclarations(method.body),
3077
+ );
3078
+ validateGlobalAccess(method.body);
3079
+ validateLoopLevels(method.body);
3080
+ if (method.static) validateStaticInstanceAccess(method.body);
3081
+ validateProperties(method.body);
3082
+ superCalls.length = 0;
3083
+ validateMethod(method.body);
3084
+ if (
3085
+ method.name === "constructor" &&
3086
+ classNode.ancestor &&
3087
+ (method.body[0]?.type !== "superConstructor" || superCalls.length !== 1)
3088
+ )
3089
+ throw new LangoError(
3090
+ "LNG1207",
3091
+ method.line,
3092
+ `${classNode.name}: constructor must call super(...) once as its first statement`,
3093
+ );
3094
+ if (method.name === "constructor" && classNode.ancestor) {
3095
+ const ancestor = classes.get(classNode.ancestor);
3096
+ const constructor = findConstructor(ancestor);
3097
+ if (constructor)
3098
+ validateRequiredArguments(constructor, method.body[0], `super for ${classNode.name}`);
3099
+ }
3100
+ if (
3101
+ (method.name !== "constructor" || !classNode.ancestor) &&
3102
+ superCalls.length
3103
+ )
3104
+ throw new LangoError(
3105
+ "LNG1208",
3106
+ method.line,
3107
+ `${classNode.name}: super(...) is only valid as the first child-constructor statement`,
3108
+ );
3109
+ if (!classNode.ancestor && hasSuperMethodCall(method.body))
3110
+ throw new LangoError(
3111
+ "LNG1208",
3112
+ method.line,
3113
+ `${classNode.name}: super.method(...) requires an ancestor`,
3114
+ );
3115
+ });
3116
+ }
3117
+ if (loopLevelDiagnostics.length) {
3118
+ const [primary] = loopLevelDiagnostics;
3119
+ primary.diagnostics = loopLevelDiagnostics;
3120
+ throw primary;
3121
+ }
3122
+ }
3123
+
3124
+ function emit(program, moduleImports = [], includeTests = false) {
3125
+ const flags = {
3126
+ print: false,
3127
+ translate: false,
3128
+ context: false,
3129
+ decimal: program.globals.some((item) => item.name.startsWith("$")),
3130
+ loopCounter: 0,
3131
+ loopStack: [],
3132
+ functionNames: new Set(program.functions.map((item) => item.name)),
3133
+ importedFunctionNames: new Set(
3134
+ moduleImports.flatMap((item) =>
3135
+ item.names.map((name) => name.split(" as ").at(-1)),
3136
+ ),
3137
+ ),
3138
+ };
3139
+ const classes = program.classes
3140
+ .map((item) => emitClass(item, flags))
3141
+ .join("\n");
3142
+ const functions = program.functions
3143
+ .map((fn) => emitFunction(fn, flags))
3144
+ .join("\n");
3145
+ const testImports = includeTests
3146
+ ? moduleImports
3147
+ .map(
3148
+ (item, index) =>
3149
+ `import { __langoTests as __langoImportedTests${index} } from ${JSON.stringify(item.specifier)};\n`,
3150
+ )
3151
+ .join("")
3152
+ : "";
3153
+ const tests = includeTests
3154
+ ? `const __langoOwnTests = [\n${program.tests
3155
+ .map((test) => emitTest(test, flags))
3156
+ .join(",\n")}\n];\nexport const __langoTests = [${moduleImports
3157
+ .map((_, index) => `...__langoImportedTests${index}`)
3158
+ .concat("...__langoOwnTests")
3159
+ .join(", ")}];\n`
3160
+ : "";
3161
+ const helpers = [
3162
+ flags.decimal && "decimal as __decimal",
3163
+ flags.print && "println as __println",
3164
+ flags.translate && "translate as __translate",
3165
+ flags.xml && "xmlEscape as __xmlEscape",
3166
+ flags.context && "contextGet as __contextGet",
3167
+ flags.context && "contextSet as __contextSet",
3168
+ flags.context && "contextRestore as __contextRestore",
3169
+ flags.json && "fromJson as __fromJson",
3170
+ flags.json && "toJson as __toJson",
3171
+ flags.reflection && "getClass as __getClass",
3172
+ flags.reflection && "isKindof as __isKindof",
3173
+ flags.predicates && "isNumber as __isNumber",
3174
+ flags.predicates && "isString as __isString",
3175
+ flags.predicates && "isBool as __isBool",
3176
+ flags.predicates && "isObject as __isObject",
3177
+ flags.predicates && "isArray as __isArray",
3178
+ flags.membership && "includes as __includes",
3179
+ flags.entries && "entries as __entries",
3180
+ flags.range && "range as __range",
3181
+ flags.add && "add as __add",
3182
+ flags.remove && "remove as __remove",
3183
+ flags.removeAll && "remove_all as __removeAll",
3184
+ flags.clear && "clear as __clear",
3185
+ flags.removeAt && "remove_at as __removeAt",
3186
+ flags.insertAt && "insert_at as __insertAt",
3187
+ flags.optionalMethod && "optional as __optional",
3188
+ flags.toJsNumber && "toJsNumber as __toJsNumber",
3189
+ flags.boolean && "requireBoolean as __requireBoolean",
3190
+ flags.assert && "assert as __assert",
3191
+ flags.normalize && "normalize as __normalize",
3192
+ flags.finite && "finite as __finite",
3193
+ flags.number && "requireNumber as __requireNumber",
3194
+ flags.call && "call as __call",
3195
+ flags.boundary && "boundary as __boundary",
3196
+ flags.database && "startTrans as __startTrans",
3197
+ flags.database && "commitTrans as __commitTrans",
3198
+ flags.database && "rollbackTrans as __rollbackTrans",
3199
+ flags.database && "transState as __transState",
3200
+ flags.databaseEnum && "DB as __DB",
3201
+ flags.sql && "query as __query",
3202
+ flags.arity && "requireArity as __requireArity",
3203
+ flags.closureArity && "withArity as __withArity",
3204
+ ].filter(Boolean);
3205
+ const imports = helpers.length
3206
+ ? `import {\n${helpers.map((item) => ` ${item},`).join("\n")}\n} from \"./.lango/runtime.mjs\";\n\n`
3207
+ : "";
3208
+ const modules = moduleImports
3209
+ .map(
3210
+ (item) =>
3211
+ `import { ${item.names.join(", ")} } from ${JSON.stringify(item.specifier)};\n`,
3212
+ )
3213
+ .join("");
3214
+ const jsModules = program.jsImports
3215
+ .map(
3216
+ (item) =>
3217
+ `import * as ${item.name} from ${JSON.stringify(item.specifier)};\n`,
3218
+ )
3219
+ .join("");
3220
+ const globals = program.globals
3221
+ .map((item) => {
3222
+ const value = emitExpr(item.value, flags);
3223
+ const prefix = item.exported ? "export let" : "let";
3224
+ if (!item.name.startsWith("$")) return `${prefix} ${item.name} = ${value};\n`;
3225
+ flags.decimal = true;
3226
+ return `${prefix} ${item.name} = __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value});\n`;
3227
+ })
3228
+ .join("");
3229
+ const testReset = includeTests
3230
+ ? `export function __langoResetTests() {\n${program.globals
3231
+ .map((item) => ` ${item.name} = ${emitExpr(item.value, flags)};\n`)
3232
+ .join("")}${program.classes
3233
+ .flatMap((item) =>
3234
+ [...item.staticValues, ...item.enumValues].map(
3235
+ (value) =>
3236
+ ` ${item.name}.${value.name} = ${emitExpr(value.value, flags)};\n`,
3237
+ ),
3238
+ )
3239
+ .join("")}}\n`
3240
+ : "";
3241
+ return `${modules}${testImports}${jsModules}${imports}${globals}${classes}${functions}${testReset}${tests}//# sourceMappingURL=main.mjs.map\n`;
3242
+ }
3243
+ function emitTest(test, flags) {
3244
+ const priorGlobalNames = flags.globalNames;
3245
+ const priorContextNames = flags.contextNames;
3246
+ flags.globalNames = collectGlobalNames(test.body);
3247
+ flags.contextNames = collectContextNames(test.body);
3248
+ const declared = new Set();
3249
+ const body = test.body
3250
+ .map((node) => emitStatement(node, 3, declared, flags))
3251
+ .join("");
3252
+ flags.globalNames = priorGlobalNames;
3253
+ flags.contextNames = priorContextNames;
3254
+ return ` { name: ${JSON.stringify(test.name)}, run() {\n${body} } }`;
3255
+ }
3256
+ function collectGlobalNames(body, names = new Set()) {
3257
+ for (const node of body) {
3258
+ if (node.type === "globalDeclare") names.add(node.name);
3259
+ if (node.type === "closureAssign") continue;
3260
+ [
3261
+ node.thenBody,
3262
+ node.elseBody,
3263
+ node.tryBody,
3264
+ node.catchBody,
3265
+ node.finallyBody,
3266
+ node.body,
3267
+ ...(node.cases?.map((item) => item.body) ?? []),
3268
+ ]
3269
+ .filter(Boolean)
3270
+ .forEach((child) => collectGlobalNames(child, names));
3271
+ }
3272
+ return names;
3273
+ }
3274
+ function collectContextNames(body, names = new Set()) {
3275
+ for (const node of body) {
3276
+ if (node.type === "contextDeclare" || node.type === "contextSet")
3277
+ names.add(node.name);
3278
+ if (node.type === "closureAssign") continue;
3279
+ [
3280
+ node.thenBody,
3281
+ node.elseBody,
3282
+ node.tryBody,
3283
+ node.catchBody,
3284
+ node.finallyBody,
3285
+ node.body,
3286
+ ...(node.cases?.map((item) => item.body) ?? []),
3287
+ ]
3288
+ .filter(Boolean)
3289
+ .forEach((child) => collectContextNames(child, names));
3290
+ }
3291
+ return names;
3292
+ }
3293
+ function emitClass(item, flags) {
3294
+ const prior = {
3295
+ publicProperties: flags.publicProperties,
3296
+ className: flags.className,
3297
+ methods: flags.methods,
3298
+ staticMethod: flags.staticMethod,
3299
+ };
3300
+ flags.publicProperties = new Set(item.publicProperties);
3301
+ flags.className = item.name;
3302
+ flags.methods = new Map(
3303
+ item.methods.map((method) => [
3304
+ `${method.static ? "static" : "instance"}:${method.name}`,
3305
+ method.public,
3306
+ ]),
3307
+ );
3308
+ const fields = [
3309
+ ...new Set(
3310
+ item.methods
3311
+ .flatMap(
3312
+ (method) =>
3313
+ JSON.stringify(method.body).match(/@@?\$?[A-Za-z_]\w*/g) ?? [],
3314
+ )
3315
+ .filter((name) => !name.startsWith("@@"))
3316
+ .map((name) => name.slice(1)),
3317
+ ),
3318
+ ].filter((field) => !flags.publicProperties.has(field));
3319
+ const implicitConstructor =
3320
+ item.ancestor &&
3321
+ !item.methods.some((method) => method.name === "constructor")
3322
+ ? ` constructor(...args) {\n super(...args);\n }\n`
3323
+ : "";
3324
+ const body = item.methods
3325
+ .map((method) => {
3326
+ flags.staticMethod = method.static;
3327
+ const declared = new Set(method.parameters.map((p) => p.name));
3328
+ const priorArrayBindings = flags.arrayBindings;
3329
+ flags.arrayBindings = new Set();
3330
+ const priorGlobalNames = flags.globalNames;
3331
+ flags.globalNames = collectGlobalNames(method.body);
3332
+ const priorContextNames = flags.contextNames;
3333
+ const contextNames = collectContextNames(method.body);
3334
+ flags.contextNames = contextNames;
3335
+ const parameters = method.parameters
3336
+ .map((p) =>
3337
+ p.defaultValue === null
3338
+ ? p.name
3339
+ : `${p.name} = ${emitExpr(p.defaultValue, flags)}`,
3340
+ )
3341
+ .join(", ");
3342
+ const suffix = method.name === "constructor" ? "" : " return null;\n";
3343
+ const name =
3344
+ method.name === "constructor" || method.public
3345
+ ? method.name
3346
+ : `#${method.name}`;
3347
+ const statementDepth = contextNames.size ? 3 : 2;
3348
+ const statements = method.body
3349
+ .map((node) =>
3350
+ emitStatement(node, statementDepth, declared, flags),
3351
+ )
3352
+ const arityGuard = emitArityGuard(
3353
+ method.parameters,
3354
+ "arguments.length",
3355
+ method.name,
3356
+ statementDepth,
3357
+ flags,
3358
+ );
3359
+ const boundary = emitBoundaryChecks(method.parameters, statementDepth, flags);
3360
+ const methodBody =
3361
+ method.name === "constructor" && item.ancestor
3362
+ ? `${statements.shift() ?? ""}${arityGuard}${boundary}${statements.join("")}`
3363
+ : `${arityGuard}${boundary}${statements.join("")}`;
3364
+ flags.contextNames = priorContextNames;
3365
+ flags.globalNames = priorGlobalNames;
3366
+ flags.arrayBindings = priorArrayBindings;
3367
+ if (!contextNames.size)
3368
+ return ` ${method.static ? "static " : ""}${name}(${parameters}) {\n${methodBody}${suffix} }\n`;
3369
+ flags.context = true;
3370
+ const returnNull = method.name === "constructor" ? "" : " return null;\n";
3371
+ return ` ${method.static ? "static " : ""}${name}(${parameters}) {\n const __contextScope = [];\n try {\n${methodBody}${returnNull} } finally {\n __contextRestore(__contextScope);\n }\n }\n`;
3372
+ })
3373
+ .join("");
3374
+ const statics = [...item.staticValues, ...item.enumValues]
3375
+ .map(
3376
+ (value) => {
3377
+ const emitted = emitExpr(value.value, flags);
3378
+ if (!value.name.startsWith("$"))
3379
+ return ` static ${value.name} = ${emitted};\n`;
3380
+ flags.decimal = true;
3381
+ return ` static ${value.name} = __decimal(${value.value.type === "raw" ? JSON.stringify(value.value.value) : emitted});\n`;
3382
+ },
3383
+ )
3384
+ .join("");
3385
+ flags.publicProperties = prior.publicProperties;
3386
+ flags.className = prior.className;
3387
+ flags.methods = prior.methods;
3388
+ flags.staticMethod = prior.staticMethod;
3389
+ return `${item.exported ? "export " : ""}class ${item.name}${item.ancestor ? ` extends ${item.ancestor}` : ""} {\n${item.publicProperties.map((field) => ` ${field};\n`).join("")}${statics}${fields.map((field) => ` #${field};\n`).join("")}${implicitConstructor}${body}}\n`;
3390
+ }
3391
+ function emitFunction(fn, flags) {
3392
+ const topLevelAssigned = new Set(
3393
+ fn.body
3394
+ .filter((node) => node.type === "assign" || node.type === "closureAssign")
3395
+ .map((node) => node.name),
3396
+ );
3397
+ const hoisted = new Set(
3398
+ [...collectEmitLocals(fn.body)].filter((name) => !topLevelAssigned.has(name)),
3399
+ );
3400
+ const declared = new Set([...fn.parameters.map((p) => p.name), ...hoisted]);
3401
+ const priorArrayBindings = flags.arrayBindings;
3402
+ flags.arrayBindings = new Set();
3403
+ const priorContextNames = flags.contextNames;
3404
+ const priorGlobalNames = flags.globalNames;
3405
+ const contextNames = collectContextNames(fn.body);
3406
+ flags.contextNames = contextNames;
3407
+ flags.globalNames = collectGlobalNames(fn.body);
3408
+ const parameters = fn.parameters
3409
+ .map((p) =>
3410
+ p.defaultValue === null
3411
+ ? p.name
3412
+ : `${p.name} = ${emitExpr(p.defaultValue, flags)}`,
3413
+ )
3414
+ .join(", ");
3415
+ const body = `${emitArityGuard(
3416
+ fn.parameters,
3417
+ "arguments.length",
3418
+ fn.name,
3419
+ contextNames.size ? 2 : 1,
3420
+ flags,
3421
+ )}${emitBoundaryChecks(fn.parameters, contextNames.size ? 2 : 1, flags)}${[...hoisted].map((name) => `${" ".repeat(contextNames.size ? 2 : 1)}let ${name};\n`).join("")}${fn.body
3422
+ .map((item) => emitStatement(item, contextNames.size ? 2 : 1, declared, flags))
3423
+ .join("")}`;
3424
+ flags.contextNames = priorContextNames;
3425
+ flags.globalNames = priorGlobalNames;
3426
+ flags.arrayBindings = priorArrayBindings;
3427
+ if (!contextNames.size)
3428
+ return `${fn.exported ? "export " : ""}function ${fn.name}(${parameters}) {\n${body} return null;\n}\n`;
3429
+ flags.context = true;
3430
+ return `${fn.exported ? "export " : ""}function ${fn.name}(${parameters}) {\n const __contextScope = [];\n try {\n${body} return null;\n } finally {\n __contextRestore(__contextScope);\n }\n}\n`;
3431
+ }
3432
+ function collectEmitLocals(body, names = new Set(), nested = false) {
3433
+ for (const node of body) {
3434
+ if (nested && node.type === "assign" && /^[A-Za-z_]\w*$/.test(node.name))
3435
+ names.add(node.name);
3436
+ if (nested && node.type === "closureAssign") names.add(node.name);
3437
+ [node.thenBody, node.elseBody, node.tryBody, node.catchBody, node.finallyBody]
3438
+ .filter(Boolean)
3439
+ .forEach((child) => collectEmitLocals(child, names, true));
3440
+ }
3441
+ return names;
3442
+ }
3443
+ function emitDecimalExpression(node, flags) {
3444
+ const operand = (value) => {
3445
+ if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
3446
+ return emitDecimalExpression(value, flags);
3447
+ if (value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)) {
3448
+ flags.decimal = true;
3449
+ return `__decimal(${JSON.stringify(value.value)})`;
3450
+ }
3451
+ if (value.type === "raw" && value.value.startsWith("$")) return emitExpr(value, flags);
3452
+ flags.decimal = true;
3453
+ return `__decimal(${emitExpr(value, flags)})`;
3454
+ };
3455
+ if (node.type !== "binary" || !["+", "-", "*", "/", "%", "**"].includes(node.operator))
3456
+ return operand(node);
3457
+ flags.decimal = true;
3458
+ const methods = { "+": "plus", "-": "minus", "*": "times", "/": "div", "%": "mod", "**": "pow" };
3459
+ return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
3460
+ }
3461
+ function needsLoopLabel(body, nestedLoopDepth = 0) {
3462
+ for (const node of body) {
3463
+ if (
3464
+ (node.type === "break" || node.type === "continue") &&
3465
+ node.level === nestedLoopDepth + 1 &&
3466
+ node.level > 1
3467
+ ) return true;
3468
+ const childLoopDepth = ["loop", "for", "forObject"].includes(node.type)
3469
+ ? nestedLoopDepth + 1
3470
+ : nestedLoopDepth;
3471
+ const childBodies = [
3472
+ node.thenBody,
3473
+ node.elseBody,
3474
+ node.tryBody,
3475
+ node.catchBody,
3476
+ node.finallyBody,
3477
+ node.body,
3478
+ ...(node.cases?.map((item) => item.body) ?? []),
3479
+ ].filter(Boolean);
3480
+ if (childBodies.some((child) => needsLoopLabel(child, childLoopDepth))) return true;
3481
+ }
3482
+ return false;
3483
+ }
3484
+
3485
+ function emitStatement(node, depth, declared, flags) {
3486
+ const pad = " ".repeat(depth);
3487
+ if (node.type === "globalDeclare") return "";
3488
+ if (node.type === "contextDeclare") return "";
3489
+ if (node.type === "contextSet") {
3490
+ flags.context = true;
3491
+ return `${pad}__contextScope.push(__contextSet(${JSON.stringify(node.name)}, ${emitExpr(node.value, flags)}));\n`;
3492
+ }
3493
+ if (node.type === "assert") {
3494
+ flags.assert = true;
3495
+ return `${pad}__assert(${emitBoolean(node.value, flags)}, ${JSON.stringify(`line ${node.line}: ${node.source}`)});\n`;
3496
+ }
3497
+ if (node.type === "assign") {
3498
+ const decimalBinding = node.name.startsWith("$") || /^@{1,2}\$/.test(node.name);
3499
+ const property = node.name.slice(node.name.startsWith("@@") ? 2 : 1);
3500
+ const target = /^[A-Za-z_]\w*\.\$?[A-Za-z_]\w*$/.test(node.name)
3501
+ ? node.name
3502
+ : node.name.startsWith("@@")
3503
+ ? `${flags.className}.${property}`
3504
+ : node.name.startsWith("@")
3505
+ ? flags.publicProperties?.has(property)
3506
+ ? `this.${property}`
3507
+ : `this.#${property}`
3508
+ : flags.className && node.name.startsWith("_")
3509
+ ? `this.${node.name}`
3510
+ : node.name;
3511
+ const prefix =
3512
+ /^[A-Za-z_]\w*\.\$?[A-Za-z_]\w*$/.test(node.name)
3513
+ ? ""
3514
+ : node.name.startsWith("@") ||
3515
+ (flags.className && node.name.startsWith("_")) ||
3516
+ declared.has(node.name) || flags.globalNames?.has(node.name)
3517
+ ? ""
3518
+ : "let ";
3519
+ declared.add(node.name);
3520
+ if (node.value.type === "array") flags.arrayBindings?.add(node.name);
3521
+ const value = decimalBinding && node.value.type === "binary"
3522
+ ? emitDecimalExpression(node.value, flags)
3523
+ : emitExpr(node.value, flags);
3524
+ if (decimalBinding) flags.decimal = true;
3525
+ const decimalInput =
3526
+ node.value.type === "raw" && /^\d+(?:\.\d+)?$/.test(node.value.value)
3527
+ ? JSON.stringify(node.value.value)
3528
+ : value;
3529
+ const decimalLiteral =
3530
+ node.value.type === "raw" && /^\$(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(node.value.value);
3531
+ return `${pad}${prefix}${target} = ${decimalBinding && node.value.type !== "binary" && !decimalLiteral ? `__decimal(${decimalInput})` : value};\n`;
3532
+ }
3533
+ if (node.type === "indexAssign") {
3534
+ const target = `${node.receiver}${(node.indexes ?? [node.index]).map((index) => `[${emitExpr(index, flags)}]`).join("")}`;
3535
+ const value = emitExpr(node.value, flags);
3536
+ if (node.operator) flags.finite = true;
3537
+ return `${pad}${target} = ${node.operator ? `__finite(${target} ${node.operator} ${value})` : value};\n`;
3538
+ }
3539
+ if (node.type === "expression")
3540
+ return `${pad}${emitExpr(node.value, flags)};\n`;
3541
+ if (node.type === "chain")
3542
+ return `${pad}${emitChain(node, depth, flags)};\n`;
3543
+ if (node.type === "closureAssign") {
3544
+ declared.add(node.name);
3545
+ return `${pad}let ${node.name} = ${emitClosure(node, depth, flags)};\n`;
3546
+ }
3547
+ if (node.type === "superConstructor")
3548
+ return `${pad}super(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")});\n`;
3549
+ if (node.type === "increment") return `${pad}${node.name} += 1;\n`;
3550
+ if (node.type === "add")
3551
+ return `${pad}${node.target}.push(${emitExpr(node.value, flags)});\n`;
3552
+ if (node.type === "return")
3553
+ return `${pad}return${node.value === null ? " null" : node.value.type === "closure" ? ` ${emitClosure(node.value, depth, flags)}` : node.value.type === "chain" ? ` ${emitChain(node.value, depth, flags)}` : ` ${emitExpr(node.value, flags)}`};\n`;
3554
+ if (node.type === "throw")
3555
+ return `${pad}throw ${emitExpr(node.value, flags)};\n`;
3556
+ if (node.type === "println") {
3557
+ flags.print = true;
3558
+ return `${pad}__println(${emitExpr(node.value, flags)});\n`;
3559
+ }
3560
+ if (node.type === "break" || node.type === "continue") {
3561
+ const label = flags.loopStack.at(-node.level);
3562
+ return `${pad}${node.type}${node.level === 1 ? "" : ` ${label}`};\n`;
3563
+ }
3564
+ if (node.type === "try") {
3565
+ let out = `${pad}try {\n${node.tryBody.map((item) => emitStatement(item, depth + 1, declared, flags)).join("")}${pad}}`;
3566
+ if (node.catchBody)
3567
+ out += ` catch (${node.catchName}) {\n${node.catchBody.map((item) => emitStatement(item, depth + 1, declared, flags)).join("")}${pad}}`;
3568
+ if (node.finallyBody)
3569
+ out += ` finally {\n${node.finallyBody.map((item) => emitStatement(item, depth + 1, declared, flags)).join("")}${pad}}`;
3570
+ return `${out}\n`;
3571
+ }
3572
+ if (node.type === "loop") {
3573
+ const label = `__loop${++flags.loopCounter}`;
3574
+ flags.loopStack.push(label);
3575
+ const body = node.body
3576
+ .map((item) => emitStatement(item, depth + 1, declared, flags))
3577
+ .join("");
3578
+ flags.loopStack.pop();
3579
+ return `${pad}${label}: while (${node.condition === null ? "true" : `${node.until ? "!" : ""}${emitBoolean(node.condition, flags)}`}) {\n${body}${pad}}\n`;
3580
+ }
3581
+ if (node.type === "for") {
3582
+ const label = needsLoopLabel(node.body) ? `__loop${++flags.loopCounter}` : null;
3583
+ flags.loopStack.push(label);
3584
+ const body = node.body
3585
+ .map((item) => emitStatement(item, depth + 1, declared, flags))
3586
+ .join("");
3587
+ flags.loopStack.pop();
3588
+ return `${pad}${label ? `${label}: ` : ""}for (const ${node.item} of ${emitExpr(node.collection, flags)}) {\n${body}${pad}}\n`;
3589
+ }
3590
+ if (node.type === "forObject") {
3591
+ flags.entries = true;
3592
+ const label = needsLoopLabel(node.body) ? `__loop${++flags.loopCounter}` : null;
3593
+ flags.loopStack.push(label);
3594
+ const body = node.body
3595
+ .map((item) => emitStatement(item, depth + 1, declared, flags))
3596
+ .join("");
3597
+ flags.loopStack.pop();
3598
+ return `${pad}${label ? `${label}: ` : ""}for (const [${node.key}, ${node.value}] of __entries(${emitExpr(node.collection, flags)})) {\n${body}${pad}}\n`;
3599
+ }
3600
+ if (node.type === "switch") {
3601
+ const conditions = node.cases.map((item) =>
3602
+ item.tests
3603
+ .map((test) =>
3604
+ node.value
3605
+ ? `${emitExpr(node.value, flags)} === ${emitExpr(test, flags)}`
3606
+ : emitBoolean(test, flags),
3607
+ )
3608
+ .join(" || "),
3609
+ );
3610
+ let out = node.cases
3611
+ .map(
3612
+ (item, index) =>
3613
+ `${pad}${index ? "else " : ""}if (${conditions[index]}) {\n${item.body.map((child) => emitStatement(child, depth + 1, declared, flags)).join("")}${pad}}`,
3614
+ )
3615
+ .join(" ");
3616
+ if (node.elseBody)
3617
+ out += ` else {\n${node.elseBody.map((child) => emitStatement(child, depth + 1, declared, flags)).join("")}${pad}}`;
3618
+ return `${out}\n`;
3619
+ }
3620
+ let out = `${pad}if (${emitBoolean(node.condition, flags)}) {\n${node.thenBody.map((item) => emitStatement(item, depth + 1, declared, flags)).join("")}${pad}}`;
3621
+ if (node.elseBody)
3622
+ out += ` else {\n${node.elseBody.map((item) => emitStatement(item, depth + 1, declared, flags)).join("")}${pad}}`;
3623
+ return `${out}\n`;
3624
+ }
3625
+ function emitChain(node, depth, flags) {
3626
+ return `${emitExpr(node.base, flags)}${node.steps
3627
+ .map((step) => `.${step.name}(${emitClosure(step, depth, flags)})`)
3628
+ .join("")}`;
3629
+ }
3630
+ function emitClosure(node, depth, flags) {
3631
+ const parameters = node.parameters
3632
+ .map((item) =>
3633
+ item.defaultValue === null
3634
+ ? item.name
3635
+ : `${item.name} = ${emitExpr(item.defaultValue, flags)}`,
3636
+ )
3637
+ .join(", ");
3638
+ const declared = new Set([
3639
+ ...node.parameters.map((parameter) => parameter.name),
3640
+ ...node.captures.filter((name) => !name.startsWith("@") && !name.startsWith("_")),
3641
+ ]);
3642
+ const pad = " ".repeat(depth);
3643
+ const priorContextNames = flags.contextNames;
3644
+ const priorGlobalNames = flags.globalNames;
3645
+ const priorArrayBindings = flags.arrayBindings;
3646
+ const contextNames = collectContextNames(node.body);
3647
+ flags.contextNames = contextNames;
3648
+ flags.globalNames = collectGlobalNames(node.body);
3649
+ flags.arrayBindings = new Set();
3650
+ const body = node.body
3651
+ .map((item) =>
3652
+ emitStatement(item, depth + (contextNames.size ? 2 : 1), declared, flags),
3653
+ )
3654
+ .join("");
3655
+ const boundary = emitBoundaryChecks(
3656
+ node.parameters,
3657
+ depth + (contextNames.size ? 2 : 1),
3658
+ flags,
3659
+ );
3660
+ flags.contextNames = priorContextNames;
3661
+ flags.globalNames = priorGlobalNames;
3662
+ flags.arrayBindings = priorArrayBindings;
3663
+ const requiredPositions = node.parameters
3664
+ .map((parameter, index) => (parameter.defaultValue === null ? index : null))
3665
+ .filter((index) => index !== null);
3666
+ const wrap = (value) => {
3667
+ if (!requiredPositions.length) return value;
3668
+ flags.closureArity = true;
3669
+ return `__withArity(${value}, [${requiredPositions.join(", ")}], "closure")`;
3670
+ };
3671
+ if (!contextNames.size)
3672
+ return wrap(`(${parameters}) => {\n${boundary}${body}${pad} return null;\n${pad}}`);
3673
+ flags.context = true;
3674
+ return wrap(`(${parameters}) => {\n${pad} const __contextScope = [];\n${pad} try {\n${boundary}${body}${pad} return null;\n${pad} } finally {\n${pad} __contextRestore(__contextScope);\n${pad} }\n${pad}}`);
3675
+ }
3676
+ function emitArityGuard(parameters, actual, name, depth, flags) {
3677
+ const positions = parameters
3678
+ .map((parameter, index) => (parameter.defaultValue === null ? index : null))
3679
+ .filter((index) => index !== null);
3680
+ if (!positions.length) return "";
3681
+ flags.arity = true;
3682
+ return `${" ".repeat(depth)}__requireArity(${actual}, [${positions.join(", ")}], ${JSON.stringify(name)});\n`;
3683
+ }
3684
+ function emitBoundaryChecks(parameters, depth, flags) {
3685
+ if (!parameters.length) return "";
3686
+ flags.boundary = true;
3687
+ const pad = " ".repeat(depth);
3688
+ return parameters
3689
+ .map((parameter) => {
3690
+ if (parameter.name.startsWith("$")) {
3691
+ flags.decimal = true;
3692
+ return `${pad}${parameter.name} = __decimal(${parameter.name});\n`;
3693
+ }
3694
+ return `${pad}${parameter.name} = __boundary(${parameter.name});\n`;
3695
+ })
3696
+ .join("");
3697
+ }
3698
+ function emitExpr(node, flags) {
3699
+ const decimalRaw = (value) =>
3700
+ value?.type === "raw" &&
3701
+ (value.value.startsWith("$") || value.value.includes(".$"));
3702
+ const decimalValue = (value) =>
3703
+ decimalRaw(value) ||
3704
+ (value?.type === "binary" && (decimalValue(value.left) || decimalValue(value.right))) ||
3705
+ (value?.type === "unary" && decimalValue(value.value));
3706
+ const normalize = (value) => {
3707
+ flags.normalize = true;
3708
+ return `__normalize(${value})`;
3709
+ };
3710
+ if (node.type === "raw")
3711
+ if (flags.contextNames?.has(node.value)) {
3712
+ flags.context = true;
3713
+ return `__contextGet(${JSON.stringify(node.value)})`;
3714
+ }
3715
+ if (node.type === "raw")
3716
+ if (/^DB\.(Active|RollbackOnly|Failed)$/.test(node.value)) {
3717
+ flags.databaseEnum = true;
3718
+ return `__${node.value}`;
3719
+ }
3720
+ if (node.type === "raw")
3721
+ if (/^\$(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(node.value)) {
3722
+ flags.decimal = true;
3723
+ return `__decimal(${JSON.stringify(node.value.slice(1))})`;
3724
+ }
3725
+ if (node.type === "raw")
3726
+ return node.value.startsWith("@@")
3727
+ ? normalize(`${flags.className}.${node.value.slice(2)}`)
3728
+ : node.value.startsWith("@")
3729
+ ? flags.publicProperties?.has(node.value.slice(1))
3730
+ ? normalize(`this.${node.value.slice(1)}`)
3731
+ : normalize(`this.#${node.value.slice(1)}`)
3732
+ : flags.className && node.value.startsWith("_")
3733
+ ? normalize(`this.${node.value}`)
3734
+ : (node.value.includes("?.") || /^(?:[A-Za-z_]\w*\.[A-Za-z_]\w*|[A-Za-z_]\w*\[(?:\d+|[A-Za-z_]\w*|['"][^'"]*['"])\])$/.test(node.value))
3735
+ ? normalize(node.value)
3736
+ : node.value;
3737
+ if (node.type === "sql") {
3738
+ flags.sql = true;
3739
+ const text = node.parts
3740
+ .map((part) => part.type === "text" ? JSON.stringify(part.value) : `String(${emitExpr(part.value, flags)})`)
3741
+ .join(" + ");
3742
+ return `__query(${text}, ${JSON.stringify(node.parameters)})`;
3743
+ }
3744
+ if (node.type === "unary") {
3745
+ if (node.operator === "not") return `!${emitBoolean(node.value, flags)}`;
3746
+ const decimalExpression = (value) =>
3747
+ decimalRaw(value) ||
3748
+ (value.type === "binary" && (decimalExpression(value.left) || decimalExpression(value.right)));
3749
+ if (decimalExpression(node.value)) {
3750
+ flags.decimal = true;
3751
+ return `__decimal("0").minus(${emitExpr(node.value, flags)})`;
3752
+ }
3753
+ flags.finite = true;
3754
+ const numericLiteral =
3755
+ node.value.type === "raw" &&
3756
+ /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(node.value.value);
3757
+ if (!numericLiteral) flags.number = true;
3758
+ return `__finite(-${numericLiteral ? emitExpr(node.value, flags) : `__requireNumber(${emitExpr(node.value, flags)})`})`;
3759
+ }
3760
+ if (node.type === "new")
3761
+ return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3762
+ if (node.type === "superMethodCall")
3763
+ return `super.${node.name}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3764
+ if (node.type === "index")
3765
+ return normalize(
3766
+ `${typeof node.receiver === "string" ? node.receiver : emitExpr(node.receiver, flags)}${node.optional ? "?." : ""}[${emitExpr(node.index, flags)}]`,
3767
+ );
3768
+ if (node.type === "property")
3769
+ return normalize(
3770
+ `${emitExpr(node.receiver, flags)}${node.optional ? "?." : "."}${node.name}`,
3771
+ );
3772
+ if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
3773
+ const namespace = node.type === "namespaceMethodCall";
3774
+ const sourceReceiver = namespace
3775
+ ? node.receiver.replaceAll("/", ".")
3776
+ : typeof node.receiver === "string"
3777
+ ? node.receiver
3778
+ : emitExpr(node.receiver, flags);
3779
+ const type =
3780
+ sourceReceiver === flags.className ||
3781
+ (sourceReceiver === "this" && flags.staticMethod)
3782
+ ? "static"
3783
+ : "instance";
3784
+ const visibility = flags.methods?.get(`${type}:${node.name}`);
3785
+ const receiver =
3786
+ sourceReceiver === "this" && flags.staticMethod && visibility === false
3787
+ ? flags.className
3788
+ : sourceReceiver;
3789
+ const method =
3790
+ !namespace &&
3791
+ node.name === "add" &&
3792
+ typeof node.receiver === "string" &&
3793
+ flags.arrayBindings?.has(node.receiver)
3794
+ ? "push"
3795
+ : node.name;
3796
+ const arguments_ = node.arguments.map((item) => emitExpr(item, flags)).join(", ");
3797
+ const helperCall = (helper) => {
3798
+ const call = `${helper}(${receiver}${arguments_ ? `, ${arguments_}` : ""})`;
3799
+ if (!node.optional) return call;
3800
+ flags.optionalMethod = true;
3801
+ return `__optional(${receiver}, (value) => ${helper}(value${arguments_ ? `, ${arguments_}` : ""}))`;
3802
+ };
3803
+ if (!namespace && node.name === "add" && visibility === undefined && method !== "push") {
3804
+ flags.add = true;
3805
+ return helperCall("__add");
3806
+ }
3807
+ if (!namespace && node.name === "remove" && visibility === undefined) {
3808
+ flags.remove = true;
3809
+ return helperCall("__remove");
3810
+ }
3811
+ if (!namespace && node.name === "remove_all" && visibility === undefined) {
3812
+ flags.removeAll = true;
3813
+ return helperCall("__removeAll");
3814
+ }
3815
+ if (!namespace && node.name === "clear" && visibility === undefined) {
3816
+ flags.clear = true;
3817
+ return helperCall("__clear");
3818
+ }
3819
+ if (!namespace && node.name === "remove_at" && visibility === undefined) {
3820
+ flags.removeAt = true;
3821
+ return helperCall("__removeAt");
3822
+ }
3823
+ if (!namespace && node.name === "insert_at" && visibility === undefined) {
3824
+ flags.insertAt = true;
3825
+ return helperCall("__insertAt");
3826
+ }
3827
+ const call = `${receiver}${visibility === false ? ".#" : node.optional ? "?." : "."}${method}(${arguments_})`;
3828
+ if (node.optional) return normalize(call);
3829
+ if (
3830
+ visibility === undefined &&
3831
+ !(!namespace && node.name === "add" && typeof node.receiver === "string" && flags.arrayBindings?.has(node.receiver))
3832
+ ) {
3833
+ flags.normalize = true;
3834
+ return `__normalize(${call})`;
3835
+ }
3836
+ return call;
3837
+ }
3838
+ if (node.type === "array")
3839
+ return `[${node.values.map((item) => emitExpr(item, flags)).join(", ")}]`;
3840
+ if (node.type === "object")
3841
+ return `{ ${node.fields.map((item) => {
3842
+ const value = emitExpr(item.value, flags);
3843
+ if (!item.key.startsWith("$")) return `${item.key}: ${value}`;
3844
+ flags.decimal = true;
3845
+ return `${JSON.stringify(item.key)}: __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value})`;
3846
+ }).join(", ")} }`;
3847
+ if (node.type === "binary") {
3848
+ const operators = { "==": "===", "!=": "!==", and: "&&", or: "||" };
3849
+ if (node.operator === "and" || node.operator === "or")
3850
+ return `${emitBoolean(node.left, flags)} ${operators[node.operator]} ${emitBoolean(node.right, flags)}`;
3851
+ if (["==", "!="].includes(node.operator) &&
3852
+ (decimalValue(node.left) || decimalValue(node.right))) {
3853
+ flags.decimal = true;
3854
+ const operand = (value) =>
3855
+ decimalValue(value)
3856
+ ? emitExpr(value, flags)
3857
+ : value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
3858
+ ? `__decimal(${JSON.stringify(value.value)})`
3859
+ : `__decimal(${emitExpr(value, flags)})`;
3860
+ const comparison = decimalRaw(node.right) || decimalValue(node.left)
3861
+ ? `${operand(node.left)}.eq(${operand(node.right)})`
3862
+ : `${operand(node.right)}.eq(${operand(node.left)})`;
3863
+ return node.operator === "!=" ? `!(${comparison})` : comparison;
3864
+ }
3865
+ if (["<", ">", "<=", ">="].includes(node.operator) &&
3866
+ (decimalValue(node.left) || decimalValue(node.right))) {
3867
+ flags.decimal = true;
3868
+ const operand = (value) =>
3869
+ decimalValue(value)
3870
+ ? emitExpr(value, flags)
3871
+ : value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
3872
+ ? `__decimal(${JSON.stringify(value.value)})`
3873
+ : `__decimal(${emitExpr(value, flags)})`;
3874
+ const methods = { "<": "lt", ">": "gt", "<=": "lte", ">=": "gte" };
3875
+ const reverse = { "<": "gt", ">": "lt", "<=": "gte", ">=": "lte" };
3876
+ return decimalValue(node.left)
3877
+ ? `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`
3878
+ : `${operand(node.right)}.${reverse[node.operator]}(${operand(node.left)})`;
3879
+ }
3880
+ if (["in", "not in"].includes(node.operator)) {
3881
+ flags.membership = true;
3882
+ const membership = `__includes(${emitExpr(node.left, flags)}, ${emitExpr(node.right, flags)})`;
3883
+ return node.operator === "not in" ? `!${membership}` : membership;
3884
+ }
3885
+ if (["+", "-", "*", "/", "%", "**"].includes(node.operator) &&
3886
+ (decimalValue(node.left) || decimalValue(node.right))) {
3887
+ flags.decimal = true;
3888
+ const methods = { "+": "plus", "-": "minus", "*": "times", "/": "div", "%": "mod", "**": "pow" };
3889
+ const operand = (value) =>
3890
+ decimalValue(value)
3891
+ ? emitExpr(value, flags)
3892
+ : value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
3893
+ ? `__decimal(${JSON.stringify(value.value)})`
3894
+ : emitExpr(value, flags);
3895
+ return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
3896
+ }
3897
+ const value = `${emitExpr(node.left, flags)} ${operators[node.operator] ?? node.operator} ${emitExpr(node.right, flags)}`;
3898
+ if (["+", "-", "*", "/", "%", "**"].includes(node.operator)) {
3899
+ flags.finite = true;
3900
+ return `__finite(${value})`;
3901
+ }
3902
+ return value;
3903
+ }
3904
+ if (node.type === "call")
3905
+ if (node.name === "range") {
3906
+ flags.range = true;
3907
+ const arguments_ = node.arguments.map((item) => emitExpr(item, flags));
3908
+ return `__range(${arguments_.length === 1 ? `0, ${arguments_[0]}` : arguments_.join(", ")})`;
3909
+ }
3910
+ if (node.type === "call")
3911
+ if (node.name === "to_js_number") {
3912
+ flags.toJsNumber = true;
3913
+ return `__toJsNumber(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3914
+ }
3915
+ if (node.type === "call")
3916
+ if (node.name === "from_json" || node.name === "to_json") {
3917
+ flags.json = true;
3918
+ const helper = node.name === "from_json" ? "__fromJson" : "__toJson";
3919
+ return `${helper}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3920
+ }
3921
+ if (node.type === "call")
3922
+ if (node.name === "get_class" || node.name === "is_kindof") {
3923
+ flags.reflection = true;
3924
+ const helper = node.name === "get_class" ? "__getClass" : "__isKindof";
3925
+ return `${helper}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3926
+ }
3927
+ if (node.type === "call")
3928
+ if (["is_number", "is_string", "is_bool", "is_object", "is_array"].includes(node.name)) {
3929
+ flags.predicates = true;
3930
+ const helper = {
3931
+ is_number: "__isNumber",
3932
+ is_string: "__isString",
3933
+ is_bool: "__isBool",
3934
+ is_object: "__isObject",
3935
+ is_array: "__isArray",
3936
+ }[node.name];
3937
+ return `${helper}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3938
+ }
3939
+ if (node.type === "call")
3940
+ if (["start_trans", "commit_trans", "rollback_trans", "trans_state"].includes(node.name)) {
3941
+ flags.database = true;
3942
+ const helper = { start_trans: "__startTrans", commit_trans: "__commitTrans", rollback_trans: "__rollbackTrans", trans_state: "__transState" }[node.name];
3943
+ return `${helper}()`;
3944
+ }
3945
+ if (node.type === "call")
3946
+ if (
3947
+ !flags.functionNames.has(node.name) &&
3948
+ !flags.importedFunctionNames.has(node.name)
3949
+ ) {
3950
+ flags.call = true;
3951
+ return `__call(${node.name}, ${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3952
+ } else
3953
+ return `${node.name}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3954
+ if (node.type === "string") return emitString(node.literal, flags);
3955
+ if (node.type === "xml") {
3956
+ flags.xml = true;
3957
+ return `[${node.parts
3958
+ .map((part) =>
3959
+ part.type === "text"
3960
+ ? JSON.stringify(part.value)
3961
+ : `__xmlEscape(${emitExpr(part.value, flags)})`,
3962
+ )
3963
+ .join(", ")}].join("")`;
3964
+ }
3965
+ flags.translate = true;
3966
+ const template = node.literal
3967
+ .map(
3968
+ (item, index) => `${item.text}${item.value === null ? "" : `{${index}}`}`,
3969
+ )
3970
+ .join("");
3971
+ const values = node.literal
3972
+ .filter((item) => item.value)
3973
+ .map((item) => item.value);
3974
+ return `__translate(${JSON.stringify(template)}, [${values.join(", ")}])`;
3975
+ }
3976
+ function emitBoolean(node, flags) {
3977
+ flags.boolean = true;
3978
+ return `__requireBoolean(${emitExpr(node, flags)})`;
3979
+ }
3980
+ function emitString(parts) {
3981
+ if (parts.length === 1) return JSON.stringify(parts[0].text);
3982
+ return `\`${parts.map((item) => `${item.text.replaceAll("`", "\\`")}${item.value === null ? "" : `\${${item.value}}`}`).join("")}\``;
3983
+ }
3984
+ function spaces(line) {
3985
+ return line.length - line.trimStart().length;
3986
+ }
3987
+ function collectImports(source, inputPath, outputPath) {
3988
+ const lines = source.split(/\r?\n/);
3989
+ const items = [];
3990
+ for (let index = 0; index < lines.length; index += 1) {
3991
+ const match = /^import "(\.[^"]+)"$/.exec(lines[index]);
3992
+ if (!match) continue;
3993
+ const names = [];
3994
+ const selections = [];
3995
+ while (true) {
3996
+ const item = lines[index + 1] ?? "";
3997
+ const functionImport = /^ ([A-Za-z_]\w*)\(([^)]*)\)$/.exec(item);
3998
+ const functionAlias =
3999
+ /^ alias ([A-Za-z_]\w*) = ([A-Za-z_]\w*)\(([^)]*)\)$/.exec(item);
4000
+ const classImport = /^ class ([A-Za-z_]\w*)$/.exec(item);
4001
+ const classAlias =
4002
+ /^ alias ([A-Za-z_]\w*) = class ([A-Za-z_]\w*)$/.exec(item);
4003
+ const globalImport = /^ global (\$?[A-Za-z_]\w*)$/.exec(item);
4004
+ const globalAlias =
4005
+ /^ alias (\$?[A-Za-z_]\w*) = global (\$?[A-Za-z_]\w*)$/.exec(item);
4006
+ if (!functionImport && !functionAlias && !classImport && !classAlias && !globalImport && !globalAlias) break;
4007
+ names.push(
4008
+ classAlias || functionAlias || globalAlias
4009
+ ? `${(classAlias ?? functionAlias ?? globalAlias)[2]} as ${(classAlias ?? functionAlias ?? globalAlias)[1]}`
4010
+ : (functionImport ?? classImport ?? globalImport)[1],
4011
+ );
4012
+ const alias = classAlias ?? functionAlias ?? globalAlias;
4013
+ const sourceName = alias
4014
+ ? alias[2]
4015
+ : (functionImport ?? classImport ?? globalImport)[1];
4016
+ const selection = {
4017
+ sourceName,
4018
+ localName: alias ? alias[1] : sourceName,
4019
+ kind: classImport || classAlias ? "class" : globalImport || globalAlias ? "global" : "function",
4020
+ parameters:
4021
+ classImport || classAlias || globalImport || globalAlias
4022
+ ? []
4023
+ : importParameters(alias ? alias[3] : functionImport[2]),
4024
+ };
4025
+ index += 1;
4026
+ if (selection.kind === "class")
4027
+ selection.interface = collectClassImportInterface(lines, () => index, (value) => {
4028
+ index = value;
4029
+ });
4030
+ selections.push(selection);
4031
+ }
4032
+ const sourcePath = path.resolve(
4033
+ path.dirname(inputPath),
4034
+ `${match[1]}.lango`,
4035
+ );
4036
+ const outputPathForImport = path.resolve(
4037
+ path.dirname(outputPath),
4038
+ `${match[1]}.mjs`,
4039
+ );
4040
+ items.push({
4041
+ names,
4042
+ selections,
4043
+ sourcePath,
4044
+ outputPath: outputPathForImport,
4045
+ specifier: `./${path.basename(outputPathForImport)}`,
4046
+ });
4047
+ }
4048
+ return items;
4049
+ }
4050
+
4051
+ async function validateModuleInitializerCycles(entryPath, entryOutputPath) {
4052
+ const modules = new Map();
4053
+ const visit = async (modulePath, outputPath) => {
4054
+ if (modules.has(modulePath)) return;
4055
+ const source = await readFile(modulePath, "utf8");
4056
+ const imports = collectImports(source, modulePath, outputPath);
4057
+ const program = new Parser(source, modulePath).parse();
4058
+ modules.set(modulePath, { program, imports });
4059
+ for (const item of imports) await visit(item.sourcePath, item.outputPath);
4060
+ };
4061
+ await visit(entryPath, entryOutputPath);
4062
+
4063
+ const reaches = (from, target, visited = new Set()) => {
4064
+ if (from === target) return true;
4065
+ if (visited.has(from)) return false;
4066
+ visited.add(from);
4067
+ return (modules.get(from)?.imports ?? []).some((item) =>
4068
+ reaches(item.sourcePath, target, visited),
4069
+ );
4070
+ };
4071
+ const reads = (node, name, staticValue) => {
4072
+ if (!node || typeof node !== "object") return false;
4073
+ if (node.type === "raw")
4074
+ return staticValue
4075
+ ? node.value.startsWith(`${name}.`)
4076
+ : node.value === name || node.value.startsWith(`${name}.`) || node.value.startsWith(`${name}[`);
4077
+ return Object.values(node).some((value) =>
4078
+ Array.isArray(value)
4079
+ ? value.some((child) => reads(child, name, staticValue))
4080
+ : reads(value, name, staticValue),
4081
+ );
4082
+ };
4083
+ const initializers = (program) => [
4084
+ ...program.globals.map((item) => ({ value: item.value, line: item.line })),
4085
+ ...program.classes.flatMap((classNode) =>
4086
+ [...classNode.staticValues, ...classNode.enumValues].map((item) => ({
4087
+ value: item.value,
4088
+ line: item.line ?? classNode.line,
4089
+ })),
4090
+ ),
4091
+ ];
4092
+
4093
+ for (const [modulePath, module] of modules)
4094
+ for (const item of module.imports) {
4095
+ if (!reaches(item.sourcePath, modulePath)) continue;
4096
+ for (const selection of item.selections)
4097
+ for (const initializer of initializers(module.program))
4098
+ if (
4099
+ (selection.kind === "global" && reads(initializer.value, selection.localName, false)) ||
4100
+ (selection.kind === "class" && reads(initializer.value, selection.localName, true))
4101
+ )
4102
+ throw new LangoError(
4103
+ "LNG1251",
4104
+ initializer.line,
4105
+ `${modulePath}: cyclic module initializer reads imported ${selection.kind} ${selection.localName}`,
4106
+ );
4107
+ }
4108
+ }
4109
+ function collectClassImportInterface(lines, getIndex, setIndex) {
4110
+ const api = {
4111
+ properties: [],
4112
+ methods: [],
4113
+ staticValues: [],
4114
+ staticMethods: [],
4115
+ staticListed: false,
4116
+ };
4117
+ let index = getIndex();
4118
+ while (true) {
4119
+ const section = lines[index + 1] ?? "";
4120
+ const target =
4121
+ section === " properties"
4122
+ ? api.properties
4123
+ : section === " methods"
4124
+ ? api.methods
4125
+ : section === " static"
4126
+ ? null
4127
+ : undefined;
4128
+ if (target === undefined) break;
4129
+ index += 1;
4130
+ if (section === " static") api.staticListed = true;
4131
+ while (true) {
4132
+ const line = lines[index + 1] ?? "";
4133
+ const method = /^ ([A-Za-z_]\w*)\(([^)]*)\)$/.exec(line);
4134
+ const name = /^ ([A-Za-z_]\w*)$/.exec(line);
4135
+ if (!method && !name) break;
4136
+ if (section === " properties" && name) target.push(name[1]);
4137
+ else if (section === " methods" && method)
4138
+ target.push({ name: method[1], parameters: importParameters(method[2]) });
4139
+ else if (section === " static" && name) api.staticValues.push(name[1]);
4140
+ else if (section === " static" && method)
4141
+ api.staticMethods.push({
4142
+ name: method[1],
4143
+ parameters: importParameters(method[2]),
4144
+ });
4145
+ else break;
4146
+ index += 1;
4147
+ }
4148
+ }
4149
+ setIndex(index);
4150
+ return api;
4151
+ }
4152
+ function validateImportedClassInterface(
4153
+ selection,
4154
+ target,
4155
+ program,
4156
+ inputPath,
4157
+ sourcePath,
4158
+ ) {
4159
+ const api = selection.interface;
4160
+ if (!api) return;
4161
+ const classes = new Map(program.classes.map((item) => [item.name, item]));
4162
+ const hierarchy = [];
4163
+ for (let current = target; current; current = classes.get(current.ancestor))
4164
+ hierarchy.push(current);
4165
+ const missing = (kind, name) => {
4166
+ throw new LangoError(
4167
+ "LNG1234",
4168
+ 1,
4169
+ `${inputPath}: imported class ${selection.sourceName} does not export ${kind} ${name} from ${sourcePath}`,
4170
+ );
4171
+ };
4172
+ for (const name of api.properties)
4173
+ if (!hierarchy.some((item) => item.publicProperties.includes(name)))
4174
+ missing("property", name);
4175
+ for (const name of api.staticValues)
4176
+ if (
4177
+ !hierarchy.some((item) =>
4178
+ [...item.staticValues, ...item.enumValues].some(
4179
+ (value) => value.name === name,
4180
+ ),
4181
+ )
4182
+ )
4183
+ missing("static value", name);
4184
+ for (const [kind, methods] of [
4185
+ ["method", api.methods],
4186
+ ["static method", api.staticMethods],
4187
+ ])
4188
+ for (const method of methods) {
4189
+ const targetMethod = hierarchy
4190
+ .flatMap((item) => item.publicMethods)
4191
+ .find(
4192
+ (candidate) =>
4193
+ candidate.name === method.name &&
4194
+ candidate.static === (kind === "static method"),
4195
+ );
4196
+ if (!targetMethod) missing(kind, method.name);
4197
+ if (!sameParameters(method.parameters, targetMethod.parameters))
4198
+ throw new LangoError(
4199
+ "LNG1235",
4200
+ 1,
4201
+ `${inputPath}: import signature for ${selection.sourceName}.${method.name} does not match ${sourcePath}`,
4202
+ );
4203
+ }
4204
+ }
4205
+ function importParameters(raw) {
4206
+ if (!raw) return [];
4207
+ return splitTopLevel(raw).map((parameter) => {
4208
+ const match = /^([A-Za-z_]\w*)(?: = .+)?$/.exec(parameter);
4209
+ if (!match) throw new Error("invalid import parameter");
4210
+ return {
4211
+ name: match[1],
4212
+ defaultValue: parameter.includes(" = ") ? parameter : null,
4213
+ };
4214
+ });
4215
+ }
4216
+ function sameParameters(left, right) {
4217
+ return (
4218
+ left.length === right.length &&
4219
+ left.every(
4220
+ (parameter, index) =>
4221
+ parameter.name === right[index].name &&
4222
+ (parameter.defaultValue === null) ===
4223
+ (right[index].defaultValue === null),
4224
+ )
4225
+ );
4226
+ }