@lango-project/lango-js 0.1.6 → 0.1.8
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/package.json +1 -1
- package/src/compiler.mjs +146 -83
- package/src/runtime.mjs +43 -0
package/package.json
CHANGED
package/src/compiler.mjs
CHANGED
|
@@ -283,6 +283,17 @@ class Parser {
|
|
|
283
283
|
this.blank();
|
|
284
284
|
continue;
|
|
285
285
|
}
|
|
286
|
+
const hostImport = /^import js global ([A-Za-z_]\w*(?:\/[A-Za-z_]\w*)*)(?: as ([A-Za-z_]\w*))?$/.exec(line.text);
|
|
287
|
+
if (hostImport) {
|
|
288
|
+
if (declarationSeen)
|
|
289
|
+
this.fail(line, "LNG1121", "imports must precede class and function declarations");
|
|
290
|
+
const name = hostImport[2] ?? hostImport[1].split("/").at(-1);
|
|
291
|
+
if (jsImports.some((item) => item.name === name))
|
|
292
|
+
this.fail(line, "LNG1122", `duplicate JavaScript import name ${name}`);
|
|
293
|
+
jsImports.push({ name, globalPath: hostImport[1].split("/").map(kebabName) });
|
|
294
|
+
this.blank();
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
286
297
|
const jsImport = /^import js "([^"]+)" as ([A-Za-z_]\w*)$/.exec(line.text);
|
|
287
298
|
if (jsImport) {
|
|
288
299
|
if (declarationSeen)
|
|
@@ -1074,8 +1085,12 @@ class Parser {
|
|
|
1074
1085
|
}
|
|
1075
1086
|
captures() {
|
|
1076
1087
|
if (!(this.peek() ?? "").trimStart().startsWith("capture ")) return [];
|
|
1077
|
-
const line = this.
|
|
1078
|
-
const
|
|
1088
|
+
const line = { text: this.peek(), number: this.i + 1 };
|
|
1089
|
+
const indent = spaces(line.text);
|
|
1090
|
+
const names = [];
|
|
1091
|
+
while (spaces(this.peek() ?? "") === indent && (this.peek() ?? "").trimStart().startsWith("capture ")) {
|
|
1092
|
+
names.push(...splitTopLevel(this.line().text.trim().slice(8)));
|
|
1093
|
+
}
|
|
1079
1094
|
if (
|
|
1080
1095
|
names.some(
|
|
1081
1096
|
(name) =>
|
|
@@ -1116,7 +1131,7 @@ class Parser {
|
|
|
1116
1131
|
value:
|
|
1117
1132
|
match[2] === "{"
|
|
1118
1133
|
? this.object(indent + 4)
|
|
1119
|
-
: expr(match[2], line, this),
|
|
1134
|
+
: expr(this.continuedStatement(match[2], line, indent + 4), line, this),
|
|
1120
1135
|
});
|
|
1121
1136
|
}
|
|
1122
1137
|
return { type: "object", fields };
|
|
@@ -1181,18 +1196,42 @@ function collectSqlParameters(sql) {
|
|
|
1181
1196
|
return [...new Set(names)];
|
|
1182
1197
|
}
|
|
1183
1198
|
|
|
1199
|
+
function interpolationEnd(text, start) {
|
|
1200
|
+
let depth = 1;
|
|
1201
|
+
for (let index = start; index < text.length; index += 1) {
|
|
1202
|
+
const character = text[index];
|
|
1203
|
+
if (character === "'" || character === '"') {
|
|
1204
|
+
index = quotedStringEnd(text, index);
|
|
1205
|
+
if (index < 0) return -1;
|
|
1206
|
+
} else if (character === "{") depth += 1;
|
|
1207
|
+
else if (character === "}" && --depth === 0) return index;
|
|
1208
|
+
}
|
|
1209
|
+
return -1;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function quotedStringEnd(text, start) {
|
|
1213
|
+
const quote = text[start];
|
|
1214
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
1215
|
+
if (text[index] === "\\") { index += 1; continue; }
|
|
1216
|
+
if (text.startsWith("${", index)) {
|
|
1217
|
+
index = interpolationEnd(text, index + 2);
|
|
1218
|
+
if (index < 0) return -1;
|
|
1219
|
+
} else if (text[index] === quote) return index;
|
|
1220
|
+
}
|
|
1221
|
+
return -1;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function skipString(text, start) {
|
|
1225
|
+
const end = quotedStringEnd(text, start);
|
|
1226
|
+
return end < 0 ? text.length : end;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1184
1229
|
function topLevelBinary(text, operators, rightAssociative = false) {
|
|
1185
|
-
let quote = null;
|
|
1186
1230
|
let depth = 0;
|
|
1187
1231
|
let match = null;
|
|
1188
1232
|
for (let index = 0; index < text.length; index += 1) {
|
|
1189
1233
|
const character = text[index];
|
|
1190
|
-
if (
|
|
1191
|
-
if (character === "\\") { index += 1; continue; }
|
|
1192
|
-
if (character === quote) quote = null;
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1234
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1196
1235
|
if ("([{".includes(character)) { depth += 1; continue; }
|
|
1197
1236
|
if (")]}".includes(character)) { depth -= 1; continue; }
|
|
1198
1237
|
if (depth !== 0) continue;
|
|
@@ -1208,16 +1247,10 @@ function topLevelBinary(text, operators, rightAssociative = false) {
|
|
|
1208
1247
|
}
|
|
1209
1248
|
|
|
1210
1249
|
function curlyDelimiterDepth(text) {
|
|
1211
|
-
let quote = null;
|
|
1212
1250
|
let depth = 0;
|
|
1213
1251
|
for (let index = 0; index < text.length; index += 1) {
|
|
1214
1252
|
const character = text[index];
|
|
1215
|
-
if (
|
|
1216
|
-
if (character === "\\") { index += 1; continue; }
|
|
1217
|
-
if (character === quote) quote = null;
|
|
1218
|
-
continue;
|
|
1219
|
-
}
|
|
1220
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1253
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1221
1254
|
if (character === "{") depth += 1;
|
|
1222
1255
|
if (character === "}") depth -= 1;
|
|
1223
1256
|
}
|
|
@@ -1228,16 +1261,14 @@ const KEBAB_MARKER = "__lango_dash__";
|
|
|
1228
1261
|
|
|
1229
1262
|
function normalizeKebabIdentifiers(source) {
|
|
1230
1263
|
let result = "";
|
|
1231
|
-
let quote = null;
|
|
1232
1264
|
for (let index = 0; index < source.length; index += 1) {
|
|
1233
1265
|
const character = source[index];
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1266
|
+
if (character === "'" || character === '"') {
|
|
1267
|
+
const end = skipString(source, index);
|
|
1268
|
+
result += source.slice(index, end + 1);
|
|
1269
|
+
index = end;
|
|
1238
1270
|
continue;
|
|
1239
1271
|
}
|
|
1240
|
-
if (character === "'" || character === '"') { quote = character; result += character; continue; }
|
|
1241
1272
|
const match = /^[A-Za-z_]\w*(?:-[A-Za-z_]\w*)+/.exec(source.slice(index));
|
|
1242
1273
|
if (match) {
|
|
1243
1274
|
result += match[0].replaceAll("-", KEBAB_MARKER);
|
|
@@ -1270,16 +1301,10 @@ function emitKebabPath(value) {
|
|
|
1270
1301
|
function splitTopLevel(text, separator = ", ") {
|
|
1271
1302
|
const parts = [];
|
|
1272
1303
|
let start = 0;
|
|
1273
|
-
let quote = null;
|
|
1274
1304
|
let depth = 0;
|
|
1275
1305
|
for (let index = 0; index < text.length; index += 1) {
|
|
1276
1306
|
const character = text[index];
|
|
1277
|
-
if (
|
|
1278
|
-
if (character === "\\") { index += 1; continue; }
|
|
1279
|
-
if (character === quote) quote = null;
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1307
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1283
1308
|
if ("([{".includes(character)) { depth += 1; continue; }
|
|
1284
1309
|
if (")]}".includes(character)) { depth -= 1; continue; }
|
|
1285
1310
|
if (depth === 0 && text.startsWith(separator, index)) {
|
|
@@ -1294,16 +1319,10 @@ function splitTopLevel(text, separator = ", ") {
|
|
|
1294
1319
|
|
|
1295
1320
|
function parenthesizedExpression(text) {
|
|
1296
1321
|
if (!text.startsWith("(") || !text.endsWith(")")) return null;
|
|
1297
|
-
let quote = null;
|
|
1298
1322
|
let depth = 0;
|
|
1299
1323
|
for (let index = 0; index < text.length; index += 1) {
|
|
1300
1324
|
const character = text[index];
|
|
1301
|
-
if (
|
|
1302
|
-
if (character === "\\") { index += 1; continue; }
|
|
1303
|
-
if (character === quote) quote = null;
|
|
1304
|
-
continue;
|
|
1305
|
-
}
|
|
1306
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1325
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1307
1326
|
if (character === "(") depth += 1;
|
|
1308
1327
|
if (character === ")") {
|
|
1309
1328
|
depth -= 1;
|
|
@@ -1744,23 +1763,13 @@ function xmlTagEnd(source, start) {
|
|
|
1744
1763
|
}
|
|
1745
1764
|
|
|
1746
1765
|
function stripComment(line) {
|
|
1747
|
-
let quote = null;
|
|
1748
|
-
let escaped = false;
|
|
1749
1766
|
for (let index = 0; index < line.length; index += 1) {
|
|
1750
1767
|
const character = line[index];
|
|
1751
|
-
if (escaped) {
|
|
1752
|
-
escaped = false;
|
|
1753
|
-
continue;
|
|
1754
|
-
}
|
|
1755
|
-
if (quote && character === "\\") {
|
|
1756
|
-
escaped = true;
|
|
1757
|
-
continue;
|
|
1758
|
-
}
|
|
1759
1768
|
if (character === '"' || character === "'") {
|
|
1760
|
-
|
|
1769
|
+
index = skipString(line, index);
|
|
1761
1770
|
continue;
|
|
1762
1771
|
}
|
|
1763
|
-
if (
|
|
1772
|
+
if (character === "#") return line.slice(0, index).trimEnd();
|
|
1764
1773
|
}
|
|
1765
1774
|
return line;
|
|
1766
1775
|
}
|
|
@@ -1769,20 +1778,25 @@ function literal(value, line, parser) {
|
|
|
1769
1778
|
const quote = value[0];
|
|
1770
1779
|
if (!((quote === "'" || quote === '"') && value.at(-1) === quote))
|
|
1771
1780
|
parser.fail(line, "LNG1109", "expected a string literal");
|
|
1772
|
-
const
|
|
1773
|
-
.slice(1, -1)
|
|
1781
|
+
const decode = (text) => text
|
|
1774
1782
|
.replace(/\\n/g, "\n")
|
|
1775
1783
|
.replace(/\\t/g, "\t")
|
|
1776
|
-
.replace(/\\(['"
|
|
1784
|
+
.replace(/\\(['"\\$])/g, "$1");
|
|
1777
1785
|
const parts = [];
|
|
1778
|
-
let at =
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1786
|
+
let at = 1;
|
|
1787
|
+
for (let index = 1; index < value.length - 1; index += 1) {
|
|
1788
|
+
if (value[index] === "\\") { index += 1; continue; }
|
|
1789
|
+
if (!value.startsWith("${", index)) continue;
|
|
1790
|
+
const end = interpolationEnd(value, index + 2);
|
|
1791
|
+
if (end < 0 || end >= value.length - 1)
|
|
1792
|
+
parser.fail(line, "LNG1267", "unterminated string interpolation");
|
|
1793
|
+
const source = value.slice(index + 2, end).trim();
|
|
1794
|
+
if (!source) parser.fail(line, "LNG1267", "string interpolation requires an expression");
|
|
1795
|
+
parts.push({ text: decode(value.slice(at, index)), value: expr(normalizeKebabIdentifiers(source), line, parser) });
|
|
1796
|
+
index = end;
|
|
1797
|
+
at = end + 1;
|
|
1784
1798
|
}
|
|
1785
|
-
parts.push({ text:
|
|
1799
|
+
parts.push({ text: decode(value.slice(at, -1)), value: null });
|
|
1786
1800
|
return parts;
|
|
1787
1801
|
}
|
|
1788
1802
|
|
|
@@ -1804,6 +1818,8 @@ function validate(program, imports = []) {
|
|
|
1804
1818
|
),
|
|
1805
1819
|
);
|
|
1806
1820
|
const jsImportNames = new Set(program.jsImports.map((item) => item.name));
|
|
1821
|
+
const jsModuleNames = new Set(program.jsImports.filter((item) => !item.globalPath).map((item) => item.name));
|
|
1822
|
+
const hostImportNames = new Set(program.jsImports.filter((item) => item.globalPath).map((item) => item.name));
|
|
1807
1823
|
const importedFunctions = new Map(
|
|
1808
1824
|
imports.flatMap((item) =>
|
|
1809
1825
|
(item.selections ?? [])
|
|
@@ -1837,7 +1853,7 @@ function validate(program, imports = []) {
|
|
|
1837
1853
|
["xml2obj", 1],
|
|
1838
1854
|
["start_trans", 0], ["commit_trans", 0], ["rollback_trans", 0], ["trans_state", 0],
|
|
1839
1855
|
]);
|
|
1840
|
-
const hostFunctions = new Set(["fetch"]);
|
|
1856
|
+
const hostFunctions = new Set(["fetch", ...hostImportNames]);
|
|
1841
1857
|
const isDecimalValue = (value) =>
|
|
1842
1858
|
value &&
|
|
1843
1859
|
(value.type === "decimal" ||
|
|
@@ -2261,6 +2277,18 @@ function validate(program, imports = []) {
|
|
|
2261
2277
|
findMethod(classNode, "constructor", false);
|
|
2262
2278
|
const walk = (value, localCallables, classNode = null, staticMethod = false) => {
|
|
2263
2279
|
if (!value || typeof value !== "object") return;
|
|
2280
|
+
if (value.type === "closure" || value.type === "closureAssign")
|
|
2281
|
+
localCallables = new Set([...localCallables, ...value.parameters.map((parameter) => parameter.name), ...dynamicBindingNames(value.body)]);
|
|
2282
|
+
const bindings = [
|
|
2283
|
+
...(["assign", "closureAssign", "increment", "contextSet", "contextDeclare", "globalDeclare"].includes(value.type) ? [value.name] : []),
|
|
2284
|
+
...(value.parameters ?? []).map((parameter) => parameter.name),
|
|
2285
|
+
...(value.type === "for" ? [value.item] : []),
|
|
2286
|
+
...(value.type === "forObject" ? [value.key, value.value] : []),
|
|
2287
|
+
...(value.catchName ? [value.catchName] : []),
|
|
2288
|
+
];
|
|
2289
|
+
const readonly = bindings.find((name) => hostImportNames.has(name));
|
|
2290
|
+
if (readonly)
|
|
2291
|
+
throw new LangoError("LNG1268", value.line ?? 1, `${readonly} is a readonly host import and cannot be rebound or shadowed`);
|
|
2264
2292
|
if (value.type === "binary" && ["+", "-", "*", "/", "%", "**", ">", "<", ">=", "<="].includes(value.operator)) {
|
|
2265
2293
|
const string = (item) => item.type === "string";
|
|
2266
2294
|
const number = (item) => item.type === "raw" && /^\d/.test(item.value);
|
|
@@ -2337,7 +2365,7 @@ function validate(program, imports = []) {
|
|
|
2337
2365
|
value.line,
|
|
2338
2366
|
`use / between JavaScript namespace segments in new ${value.name}(...)`,
|
|
2339
2367
|
);
|
|
2340
|
-
if (value.name.includes("/")) validateJavaScriptArguments(value, value.name);
|
|
2368
|
+
if (value.name.includes("/") || hostImportNames.has(value.name)) validateJavaScriptArguments(value, value.name);
|
|
2341
2369
|
const target = classes.get(value.name);
|
|
2342
2370
|
const constructor = target && findConstructor(target);
|
|
2343
2371
|
if (constructor) validateRequiredArguments(constructor, value, value.name);
|
|
@@ -2345,7 +2373,7 @@ function validate(program, imports = []) {
|
|
|
2345
2373
|
if (value.type === "methodCall" && typeof value.receiver === "string") {
|
|
2346
2374
|
if (
|
|
2347
2375
|
value.receiver.includes(".") &&
|
|
2348
|
-
|
|
2376
|
+
jsModuleNames.has(value.receiver.split(".")[0])
|
|
2349
2377
|
)
|
|
2350
2378
|
throw new LangoError(
|
|
2351
2379
|
"LNG1232",
|
|
@@ -2405,6 +2433,8 @@ function validate(program, imports = []) {
|
|
|
2405
2433
|
`${targetClass.name}${target.method.static ? "::" : ":"}${value.name}`,
|
|
2406
2434
|
);
|
|
2407
2435
|
}
|
|
2436
|
+
if (value.type === "methodCall" && hostImportNames.has(receiverRoot(value.receiver)))
|
|
2437
|
+
validateJavaScriptArguments(value, `${receiverRoot(value.receiver)}.${value.name}`);
|
|
2408
2438
|
if (value.type === "methodCall" && receiverRoot(value.receiver) === "args")
|
|
2409
2439
|
validateJavaScriptArguments(value, `args.${value.name}`);
|
|
2410
2440
|
if (value.type === "namespaceMethodCall") {
|
|
@@ -2482,6 +2512,7 @@ function validate(program, imports = []) {
|
|
|
2482
2512
|
const expressionReferences = (node, references) => {
|
|
2483
2513
|
if (!node || typeof node !== "object") return;
|
|
2484
2514
|
if (node.type === "raw") {
|
|
2515
|
+
if (["true", "false", "null", "this"].includes(node.value)) return;
|
|
2485
2516
|
const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(node.value);
|
|
2486
2517
|
if (match) references.add(match[1]);
|
|
2487
2518
|
return;
|
|
@@ -2519,10 +2550,7 @@ function validate(program, imports = []) {
|
|
|
2519
2550
|
if (node.type === "string" || node.type === "translate") {
|
|
2520
2551
|
node.literal
|
|
2521
2552
|
.filter((part) => part.value)
|
|
2522
|
-
.forEach((part) =>
|
|
2523
|
-
const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(part.value);
|
|
2524
|
-
if (match) references.add(match[1]);
|
|
2525
|
-
});
|
|
2553
|
+
.forEach((part) => expressionReferences(part.value, references));
|
|
2526
2554
|
return;
|
|
2527
2555
|
}
|
|
2528
2556
|
Object.values(node).forEach((value) => {
|
|
@@ -3404,6 +3432,7 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3404
3432
|
item.names.map((name) => name.split(" as ").at(-1)),
|
|
3405
3433
|
),
|
|
3406
3434
|
),
|
|
3435
|
+
hostGlobals: new Map(program.jsImports.filter((item) => item.globalPath).map((item) => [item.name, item.globalPath])),
|
|
3407
3436
|
};
|
|
3408
3437
|
const classes = program.classes
|
|
3409
3438
|
.map((item) => emitClass(item, flags))
|
|
@@ -3427,7 +3456,18 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3427
3456
|
.concat("...__langoOwnTests")
|
|
3428
3457
|
.join(", ")}];\n`
|
|
3429
3458
|
: "";
|
|
3459
|
+
const globals = program.globals
|
|
3460
|
+
.map((item) => {
|
|
3461
|
+
const value = emitExpr(item.value, flags);
|
|
3462
|
+
const prefix = item.exported ? "export let" : "let";
|
|
3463
|
+
if (!item.name.startsWith("$")) return `${prefix} ${item.name} = ${value};\n`;
|
|
3464
|
+
flags.decimal = true;
|
|
3465
|
+
return `${prefix} ${item.name} = __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value});\n`;
|
|
3466
|
+
})
|
|
3467
|
+
.join("");
|
|
3430
3468
|
const helpers = [
|
|
3469
|
+
flags.hostGlobal && "hostGlobal as __hostGlobal",
|
|
3470
|
+
flags.hostGlobalCall && "hostGlobalCall as __hostGlobalCall",
|
|
3431
3471
|
flags.decimal && "decimal as __decimal",
|
|
3432
3472
|
flags.print && "println as __println",
|
|
3433
3473
|
flags.translate && "translate as __translate",
|
|
@@ -3461,6 +3501,8 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3461
3501
|
flags.normalize && "normalize as __normalize",
|
|
3462
3502
|
flags.finite && "finite as __finite",
|
|
3463
3503
|
flags.number && "requireNumber as __requireNumber",
|
|
3504
|
+
flags.numeric && "requireNumeric as __requireNumeric",
|
|
3505
|
+
flags.numericAdd && "numericAdd as __numericAdd",
|
|
3464
3506
|
flags.call && "call as __call",
|
|
3465
3507
|
flags.callMethod && "callMethod as __callMethod",
|
|
3466
3508
|
flags.boundary && "boundary as __boundary",
|
|
@@ -3483,20 +3525,12 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3483
3525
|
)
|
|
3484
3526
|
.join("");
|
|
3485
3527
|
const jsModules = program.jsImports
|
|
3528
|
+
.filter((item) => !item.globalPath)
|
|
3486
3529
|
.map(
|
|
3487
3530
|
(item) =>
|
|
3488
3531
|
`import * as ${item.name} from ${JSON.stringify(item.specifier)};\n`,
|
|
3489
3532
|
)
|
|
3490
3533
|
.join("");
|
|
3491
|
-
const globals = program.globals
|
|
3492
|
-
.map((item) => {
|
|
3493
|
-
const value = emitExpr(item.value, flags);
|
|
3494
|
-
const prefix = item.exported ? "export let" : "let";
|
|
3495
|
-
if (!item.name.startsWith("$")) return `${prefix} ${item.name} = ${value};\n`;
|
|
3496
|
-
flags.decimal = true;
|
|
3497
|
-
return `${prefix} ${item.name} = __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value});\n`;
|
|
3498
|
-
})
|
|
3499
|
-
.join("");
|
|
3500
3534
|
const testReset = includeTests
|
|
3501
3535
|
? `export function __langoResetTests() {\n${program.globals
|
|
3502
3536
|
.map((item) => ` ${item.name} = ${emitExpr(item.value, flags)};\n`)
|
|
@@ -3712,7 +3746,7 @@ function collectEmitLocals(body, names = new Set(), nested = false) {
|
|
|
3712
3746
|
return names;
|
|
3713
3747
|
}
|
|
3714
3748
|
function emitDecimalExpression(node, flags) {
|
|
3715
|
-
const operand = (value) => {
|
|
3749
|
+
const operand = (value, numericOnly = false) => {
|
|
3716
3750
|
if (value.type === "decimal") return emitExpr(value, flags);
|
|
3717
3751
|
if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
|
|
3718
3752
|
return emitDecimalExpression(value, flags);
|
|
@@ -3722,13 +3756,15 @@ function emitDecimalExpression(node, flags) {
|
|
|
3722
3756
|
}
|
|
3723
3757
|
if (value.type === "raw" && value.value.startsWith("$")) return emitExpr(value, flags);
|
|
3724
3758
|
flags.decimal = true;
|
|
3725
|
-
|
|
3759
|
+
if (numericOnly) flags.numeric = true;
|
|
3760
|
+
const emitted = emitExpr(value, flags);
|
|
3761
|
+
return `__decimal(${numericOnly ? `__requireNumeric(${emitted})` : emitted})`;
|
|
3726
3762
|
};
|
|
3727
3763
|
if (node.type !== "binary" || !["+", "-", "*", "/", "%", "**"].includes(node.operator))
|
|
3728
3764
|
return operand(node);
|
|
3729
3765
|
flags.decimal = true;
|
|
3730
3766
|
const methods = { "+": "plus", "-": "minus", "*": "times", "/": "div", "%": "mod", "**": "pow" };
|
|
3731
|
-
return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
|
|
3767
|
+
return `${operand(node.left, node.operator === "+")}.${methods[node.operator]}(${operand(node.right, node.operator === "+")})`;
|
|
3732
3768
|
}
|
|
3733
3769
|
function needsLoopLabel(body, nestedLoopDepth = 0) {
|
|
3734
3770
|
for (const node of body) {
|
|
@@ -3800,6 +3836,10 @@ function emitStatement(node, depth, declared, flags) {
|
|
|
3800
3836
|
if (node.type === "indexAssign") {
|
|
3801
3837
|
const target = `${emitReceiverName(node.receiver, flags)}${(node.indexes ?? [node.index]).map((index) => `[${emitExpr(index, flags)}]`).join("")}`;
|
|
3802
3838
|
const value = emitExpr(node.value, flags);
|
|
3839
|
+
if (node.operator === "+") {
|
|
3840
|
+
flags.numericAdd = true;
|
|
3841
|
+
return `${pad}${target} = __numericAdd(${target}, ${value});\n`;
|
|
3842
|
+
}
|
|
3803
3843
|
if (node.operator) flags.finite = true;
|
|
3804
3844
|
return `${pad}${target} = ${node.operator ? `__finite(${target} ${node.operator} ${value})` : value};\n`;
|
|
3805
3845
|
}
|
|
@@ -3813,7 +3853,10 @@ function emitStatement(node, depth, declared, flags) {
|
|
|
3813
3853
|
}
|
|
3814
3854
|
if (node.type === "superConstructor")
|
|
3815
3855
|
return `${pad}super(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")});\n`;
|
|
3816
|
-
if (node.type === "increment")
|
|
3856
|
+
if (node.type === "increment") {
|
|
3857
|
+
flags.numericAdd = true;
|
|
3858
|
+
return `${pad}${node.name} = __numericAdd(${node.name}, 1);\n`;
|
|
3859
|
+
}
|
|
3817
3860
|
if (node.type === "add")
|
|
3818
3861
|
return `${pad}${node.target}.push(${emitExpr(node.value, flags)});\n`;
|
|
3819
3862
|
if (node.type === "return")
|
|
@@ -3976,6 +4019,11 @@ function emitBoundaryChecks(parameters, depth, flags) {
|
|
|
3976
4019
|
.join("");
|
|
3977
4020
|
}
|
|
3978
4021
|
function emitReceiverName(name, flags) {
|
|
4022
|
+
const host = /^([A-Za-z_]\w*)(.*)$/.exec(name);
|
|
4023
|
+
if (host && flags.hostGlobals?.has(host[1])) {
|
|
4024
|
+
flags.hostGlobal = true;
|
|
4025
|
+
return `__hostGlobal(${JSON.stringify(flags.hostGlobals.get(host[1]))})${emitKebabPath(host[2])}`;
|
|
4026
|
+
}
|
|
3979
4027
|
const member = /^(@{1,2})(\$?[A-Za-z_]\w*)(.*)$/.exec(name);
|
|
3980
4028
|
if (member) {
|
|
3981
4029
|
const [, scope, property, suffix] = member;
|
|
@@ -4006,6 +4054,13 @@ function emitExpr(node, flags) {
|
|
|
4006
4054
|
flags.normalize = true;
|
|
4007
4055
|
return `__normalize(${value})`;
|
|
4008
4056
|
};
|
|
4057
|
+
if (node.type === "raw" && flags.hostGlobals.has(node.value.split(/[.\[?]/)[0]))
|
|
4058
|
+
return normalize(emitReceiverName(node.value, flags));
|
|
4059
|
+
if (node.type === "call" && flags.hostGlobals.has(node.name)) {
|
|
4060
|
+
flags.hostGlobalCall = true;
|
|
4061
|
+
const arguments_ = node.arguments.map((item) => item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags)).join(", ");
|
|
4062
|
+
return `__hostGlobalCall(${JSON.stringify(flags.hostGlobals.get(node.name))}${arguments_ ? `, ${arguments_}` : ""})`;
|
|
4063
|
+
}
|
|
4009
4064
|
if (node.type === "raw")
|
|
4010
4065
|
if (/^@{1,2}\$?[A-Za-z_]\w*[.\[?]/.test(node.value))
|
|
4011
4066
|
return normalize(emitReceiverName(node.value, flags));
|
|
@@ -4068,7 +4123,7 @@ function emitExpr(node, flags) {
|
|
|
4068
4123
|
return `__finite(-${numericLiteral ? emitExpr(node.value, flags) : `__requireNumber(${emitExpr(node.value, flags)})`})`;
|
|
4069
4124
|
}
|
|
4070
4125
|
if (node.type === "new")
|
|
4071
|
-
return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
|
|
4126
|
+
return `new ${flags.hostGlobals.has(node.name.split("/")[0]) ? `(${emitReceiverName(node.name.replaceAll("/", "."), flags)})` : node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
|
|
4072
4127
|
item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
|
|
4073
4128
|
).join(", ")})`;
|
|
4074
4129
|
if (node.type === "superMethodCall")
|
|
@@ -4084,7 +4139,7 @@ function emitExpr(node, flags) {
|
|
|
4084
4139
|
if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
|
|
4085
4140
|
const namespace = node.type === "namespaceMethodCall";
|
|
4086
4141
|
const sourceReceiver = namespace
|
|
4087
|
-
? node.receiver.replaceAll("/", ".")
|
|
4142
|
+
? emitReceiverName(node.receiver.replaceAll("/", "."), flags)
|
|
4088
4143
|
: typeof node.receiver === "string"
|
|
4089
4144
|
? emitReceiverName(node.receiver, flags)
|
|
4090
4145
|
: emitExpr(node.receiver, flags);
|
|
@@ -4209,8 +4264,16 @@ function emitExpr(node, flags) {
|
|
|
4209
4264
|
: value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
|
|
4210
4265
|
? `__decimal(${JSON.stringify(value.value)})`
|
|
4211
4266
|
: emitExpr(value, flags);
|
|
4267
|
+
if (node.operator === "+") {
|
|
4268
|
+
flags.numericAdd = true;
|
|
4269
|
+
return `__numericAdd(${operand(node.left)}, ${operand(node.right)})`;
|
|
4270
|
+
}
|
|
4212
4271
|
return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
|
|
4213
4272
|
}
|
|
4273
|
+
if (node.operator === "+") {
|
|
4274
|
+
flags.numericAdd = true;
|
|
4275
|
+
return `__numericAdd(${emitExpr(node.left, flags)}, ${emitExpr(node.right, flags)})`;
|
|
4276
|
+
}
|
|
4214
4277
|
const value = `${emitExpr(node.left, flags)} ${operators[node.operator] ?? node.operator} ${emitExpr(node.right, flags)}`;
|
|
4215
4278
|
if (["+", "-", "*", "/", "%", "**"].includes(node.operator)) {
|
|
4216
4279
|
flags.finite = true;
|
|
@@ -4299,7 +4362,7 @@ function emitExpr(node, flags) {
|
|
|
4299
4362
|
.join("");
|
|
4300
4363
|
const values = node.literal
|
|
4301
4364
|
.filter((item) => item.value)
|
|
4302
|
-
.map((item) => item.value);
|
|
4365
|
+
.map((item) => emitExpr(item.value, flags));
|
|
4303
4366
|
return `__translate(${JSON.stringify(template)}, [${values.join(", ")}])`;
|
|
4304
4367
|
}
|
|
4305
4368
|
function emitBoolean(node, flags) {
|
|
@@ -4308,7 +4371,7 @@ function emitBoolean(node, flags) {
|
|
|
4308
4371
|
}
|
|
4309
4372
|
function emitString(parts, flags) {
|
|
4310
4373
|
if (parts.length === 1) return JSON.stringify(parts[0].text);
|
|
4311
|
-
return `\`${parts.map((item) => `${item.text.replaceAll("`", "\\`")}${item.value === null ? "" : `\${${
|
|
4374
|
+
return `\`${parts.map((item) => `${JSON.stringify(item.text).slice(1, -1).replaceAll("`", "\\`").replaceAll("${", "\\${")}${item.value === null ? "" : `\${${emitExpr(item.value, flags)}}`}`).join("")}\``;
|
|
4312
4375
|
}
|
|
4313
4376
|
function spaces(line) {
|
|
4314
4377
|
return line.length - line.trimStart().length;
|
package/src/runtime.mjs
CHANGED
|
@@ -530,11 +530,54 @@ export function requireNumber(value) {
|
|
|
530
530
|
return finite(value);
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
export function requireNumeric(value) {
|
|
534
|
+
if (isDecimal(value) && value.isFinite()) return value;
|
|
535
|
+
return requireNumber(value);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function numericAdd(left, right) {
|
|
539
|
+
requireNumeric(left);
|
|
540
|
+
requireNumeric(right);
|
|
541
|
+
if (isDecimal(left) || isDecimal(right))
|
|
542
|
+
return decimal(left).plus(right);
|
|
543
|
+
return finite(left + right);
|
|
544
|
+
}
|
|
545
|
+
|
|
533
546
|
export function call(value, ...arguments_) {
|
|
534
547
|
if (typeof value !== "function") throw new Error("NotCallable");
|
|
535
548
|
return normalize(value(...arguments_));
|
|
536
549
|
}
|
|
537
550
|
|
|
551
|
+
export class HostGlobalUnavailable extends Error {
|
|
552
|
+
constructor(path) {
|
|
553
|
+
super("HostGlobalUnavailable: " + path.join("/"));
|
|
554
|
+
this.name = "HostGlobalUnavailable";
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function resolveHostGlobal(path) {
|
|
559
|
+
let owner = globalThis;
|
|
560
|
+
let value = owner;
|
|
561
|
+
for (const segment of path) {
|
|
562
|
+
owner = value;
|
|
563
|
+
if (owner === null || owner === undefined || !(segment in Object(owner)))
|
|
564
|
+
throw new HostGlobalUnavailable(path);
|
|
565
|
+
value = owner[segment];
|
|
566
|
+
}
|
|
567
|
+
if (value === undefined) throw new HostGlobalUnavailable(path);
|
|
568
|
+
return { owner, value };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function hostGlobal(path) {
|
|
572
|
+
return resolveHostGlobal(path).value;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function hostGlobalCall(path, ...arguments_) {
|
|
576
|
+
const { owner, value } = resolveHostGlobal(path);
|
|
577
|
+
if (typeof value !== "function") throw new Error("NotCallable");
|
|
578
|
+
return normalize(Reflect.apply(value, owner, arguments_));
|
|
579
|
+
}
|
|
580
|
+
|
|
538
581
|
export function callMethod(receiver, name, ...arguments_) {
|
|
539
582
|
const value = receiver?.[name];
|
|
540
583
|
if (typeof value !== "function") throw new Error("NotCallable");
|