@lango-project/lango-js 0.1.7 → 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.7",
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 };
@@ -1803,6 +1818,8 @@ function validate(program, imports = []) {
1803
1818
  ),
1804
1819
  );
1805
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));
1806
1823
  const importedFunctions = new Map(
1807
1824
  imports.flatMap((item) =>
1808
1825
  (item.selections ?? [])
@@ -1836,7 +1853,7 @@ function validate(program, imports = []) {
1836
1853
  ["xml2obj", 1],
1837
1854
  ["start_trans", 0], ["commit_trans", 0], ["rollback_trans", 0], ["trans_state", 0],
1838
1855
  ]);
1839
- const hostFunctions = new Set(["fetch"]);
1856
+ const hostFunctions = new Set(["fetch", ...hostImportNames]);
1840
1857
  const isDecimalValue = (value) =>
1841
1858
  value &&
1842
1859
  (value.type === "decimal" ||
@@ -2260,6 +2277,18 @@ function validate(program, imports = []) {
2260
2277
  findMethod(classNode, "constructor", false);
2261
2278
  const walk = (value, localCallables, classNode = null, staticMethod = false) => {
2262
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`);
2263
2292
  if (value.type === "binary" && ["+", "-", "*", "/", "%", "**", ">", "<", ">=", "<="].includes(value.operator)) {
2264
2293
  const string = (item) => item.type === "string";
2265
2294
  const number = (item) => item.type === "raw" && /^\d/.test(item.value);
@@ -2336,7 +2365,7 @@ function validate(program, imports = []) {
2336
2365
  value.line,
2337
2366
  `use / between JavaScript namespace segments in new ${value.name}(...)`,
2338
2367
  );
2339
- if (value.name.includes("/")) validateJavaScriptArguments(value, value.name);
2368
+ if (value.name.includes("/") || hostImportNames.has(value.name)) validateJavaScriptArguments(value, value.name);
2340
2369
  const target = classes.get(value.name);
2341
2370
  const constructor = target && findConstructor(target);
2342
2371
  if (constructor) validateRequiredArguments(constructor, value, value.name);
@@ -2344,7 +2373,7 @@ function validate(program, imports = []) {
2344
2373
  if (value.type === "methodCall" && typeof value.receiver === "string") {
2345
2374
  if (
2346
2375
  value.receiver.includes(".") &&
2347
- jsImportNames.has(value.receiver.split(".")[0])
2376
+ jsModuleNames.has(value.receiver.split(".")[0])
2348
2377
  )
2349
2378
  throw new LangoError(
2350
2379
  "LNG1232",
@@ -2404,6 +2433,8 @@ function validate(program, imports = []) {
2404
2433
  `${targetClass.name}${target.method.static ? "::" : ":"}${value.name}`,
2405
2434
  );
2406
2435
  }
2436
+ if (value.type === "methodCall" && hostImportNames.has(receiverRoot(value.receiver)))
2437
+ validateJavaScriptArguments(value, `${receiverRoot(value.receiver)}.${value.name}`);
2407
2438
  if (value.type === "methodCall" && receiverRoot(value.receiver) === "args")
2408
2439
  validateJavaScriptArguments(value, `args.${value.name}`);
2409
2440
  if (value.type === "namespaceMethodCall") {
@@ -3401,6 +3432,7 @@ function emit(program, moduleImports = [], includeTests = false) {
3401
3432
  item.names.map((name) => name.split(" as ").at(-1)),
3402
3433
  ),
3403
3434
  ),
3435
+ hostGlobals: new Map(program.jsImports.filter((item) => item.globalPath).map((item) => [item.name, item.globalPath])),
3404
3436
  };
3405
3437
  const classes = program.classes
3406
3438
  .map((item) => emitClass(item, flags))
@@ -3424,7 +3456,18 @@ function emit(program, moduleImports = [], includeTests = false) {
3424
3456
  .concat("...__langoOwnTests")
3425
3457
  .join(", ")}];\n`
3426
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("");
3427
3468
  const helpers = [
3469
+ flags.hostGlobal && "hostGlobal as __hostGlobal",
3470
+ flags.hostGlobalCall && "hostGlobalCall as __hostGlobalCall",
3428
3471
  flags.decimal && "decimal as __decimal",
3429
3472
  flags.print && "println as __println",
3430
3473
  flags.translate && "translate as __translate",
@@ -3482,20 +3525,12 @@ function emit(program, moduleImports = [], includeTests = false) {
3482
3525
  )
3483
3526
  .join("");
3484
3527
  const jsModules = program.jsImports
3528
+ .filter((item) => !item.globalPath)
3485
3529
  .map(
3486
3530
  (item) =>
3487
3531
  `import * as ${item.name} from ${JSON.stringify(item.specifier)};\n`,
3488
3532
  )
3489
3533
  .join("");
3490
- const globals = program.globals
3491
- .map((item) => {
3492
- const value = emitExpr(item.value, flags);
3493
- const prefix = item.exported ? "export let" : "let";
3494
- if (!item.name.startsWith("$")) return `${prefix} ${item.name} = ${value};\n`;
3495
- flags.decimal = true;
3496
- return `${prefix} ${item.name} = __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value});\n`;
3497
- })
3498
- .join("");
3499
3534
  const testReset = includeTests
3500
3535
  ? `export function __langoResetTests() {\n${program.globals
3501
3536
  .map((item) => ` ${item.name} = ${emitExpr(item.value, flags)};\n`)
@@ -3984,6 +4019,11 @@ function emitBoundaryChecks(parameters, depth, flags) {
3984
4019
  .join("");
3985
4020
  }
3986
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
+ }
3987
4027
  const member = /^(@{1,2})(\$?[A-Za-z_]\w*)(.*)$/.exec(name);
3988
4028
  if (member) {
3989
4029
  const [, scope, property, suffix] = member;
@@ -4014,6 +4054,13 @@ function emitExpr(node, flags) {
4014
4054
  flags.normalize = true;
4015
4055
  return `__normalize(${value})`;
4016
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
+ }
4017
4064
  if (node.type === "raw")
4018
4065
  if (/^@{1,2}\$?[A-Za-z_]\w*[.\[?]/.test(node.value))
4019
4066
  return normalize(emitReceiverName(node.value, flags));
@@ -4076,7 +4123,7 @@ function emitExpr(node, flags) {
4076
4123
  return `__finite(-${numericLiteral ? emitExpr(node.value, flags) : `__requireNumber(${emitExpr(node.value, flags)})`})`;
4077
4124
  }
4078
4125
  if (node.type === "new")
4079
- 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) =>
4080
4127
  item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
4081
4128
  ).join(", ")})`;
4082
4129
  if (node.type === "superMethodCall")
@@ -4092,7 +4139,7 @@ function emitExpr(node, flags) {
4092
4139
  if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
4093
4140
  const namespace = node.type === "namespaceMethodCall";
4094
4141
  const sourceReceiver = namespace
4095
- ? node.receiver.replaceAll("/", ".")
4142
+ ? emitReceiverName(node.receiver.replaceAll("/", "."), flags)
4096
4143
  : typeof node.receiver === "string"
4097
4144
  ? emitReceiverName(node.receiver, flags)
4098
4145
  : emitExpr(node.receiver, flags);
package/src/runtime.mjs CHANGED
@@ -548,6 +548,36 @@ export function call(value, ...arguments_) {
548
548
  return normalize(value(...arguments_));
549
549
  }
550
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
+
551
581
  export function callMethod(receiver, name, ...arguments_) {
552
582
  const value = receiver?.[name];
553
583
  if (typeof value !== "function") throw new Error("NotCallable");