@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lango-project/lango-js",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
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.line();
1078
- const names = splitTopLevel(line.text.trim().slice(8));
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 (quote !== null) {
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 (quote !== null) {
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 (quote !== null) {
1235
- result += character;
1236
- if (character === "\\") result += source[++index] ?? "";
1237
- else if (character === quote) quote = null;
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 (quote !== null) {
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 (quote !== null) {
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
- quote = quote === character ? null : quote ?? character;
1769
+ index = skipString(line, index);
1761
1770
  continue;
1762
1771
  }
1763
- if (!quote && character === "#") return line.slice(0, index).trimEnd();
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 decoded = value
1773
- .slice(1, -1)
1781
+ const decode = (text) => text
1774
1782
  .replace(/\\n/g, "\n")
1775
1783
  .replace(/\\t/g, "\t")
1776
- .replace(/\\(['"\\])/g, "$1");
1784
+ .replace(/\\(['"\\$])/g, "$1");
1777
1785
  const parts = [];
1778
- let at = 0;
1779
- const regex = /\$\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[\d+\])?)\}/g;
1780
- let match;
1781
- while ((match = regex.exec(decoded))) {
1782
- parts.push({ text: decoded.slice(at, match.index), value: match[1] });
1783
- at = match.index + match[0].length;
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: decoded.slice(at), value: null });
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
- jsImportNames.has(value.receiver.split(".")[0])
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
- return `__decimal(${emitExpr(value, flags)})`;
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") return `${pad}${node.name} += 1;\n`;
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 ? "" : `\${${emitReceiverName(item.value, flags)}}`}`).join("")}\``;
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");