@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.
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +900 -2
- package/package.json +32 -4
- package/src/cli.mjs +172 -0
- package/src/compiler.mjs +4226 -0
- package/src/database.mjs +67 -0
- package/src/runtime.mjs +487 -0
- package/src/template/.vscode/extensions.json +5 -0
- package/src/template/.vscode/settings.json +5 -0
- package/src/template/README.md +18 -0
- package/src/template/gitignore +2 -0
- package/src/template/package.json +18 -0
- package/src/template/src/main.lango +4 -0
- package/src/template/test/main.lango +2 -0
- package/src/vendor/bignumber.LICENCE.md +26 -0
- package/src/vendor/bignumber.mjs +3119 -0
package/src/compiler.mjs
ADDED
|
@@ -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
|
+
}
|