@lango-project/lango-js 0.1.7 → 0.1.10

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/README.md CHANGED
@@ -36,6 +36,24 @@ private `.lango/runtime.mjs` beside the output. Run the current checks with:
36
36
  npm test
37
37
  ```
38
38
 
39
+ Relative Lango imports are compiled under the entry output directory. The build
40
+ preserves their paths relative to the common source directory, and emitted
41
+ imports use the relative path to each actual output. Parent-directory imports
42
+ therefore stay inside the output tree. A shared dependency is emitted once;
43
+ cycles refer back to the configured entry filename. If that filename collides
44
+ with a dependency, dependencies are placed under `.lango/modules/`. Each output
45
+ folder receives its private runtime. Changing the source import graph may change
46
+ its common root and dependency paths; rebuild the complete entry graph rather
47
+ than relying on generated dependency filenames.
48
+
49
+ For a Node command-line entry, use `lango build input.lango --out cli.mjs
50
+ --executable`. It requires `export main(args)`, configures console output and
51
+ invokes main with the command-line arguments, waiting for a returned Promise.
52
+ The generated entry starts with `#!/usr/bin/env node`, so it can be an npm
53
+ `bin` target. npm sets its executable permission during installation; when
54
+ running the file directly outside npm, use `node cli.mjs` or set that permission
55
+ yourself. Ordinary library builds do not invoke main.
56
+
39
57
  Create a new application project with:
40
58
 
41
59
  ```sh
@@ -232,8 +250,8 @@ Faz 30 permits a function to return a closure directly. See
232
250
 
233
251
  ## Faz 31
234
252
 
235
- Faz 31 makes an explicit `capture @property` retain the original class
236
- instance in a returned closure. See `examples/031-property-capture`.
253
+ Faz 31 retains the original class instance for property access in a returned
254
+ closure. Member capture is now automatic (D-063). See `examples/031-property-capture`.
237
255
 
238
256
  ## Faz 32
239
257
 
@@ -252,7 +270,7 @@ capture declarations. See `examples/034-static-capture`.
252
270
 
253
271
  ## Faz 35
254
272
 
255
- Faz 35 extends explicit closure capture to protected properties. See
273
+ Faz 35 retains protected properties in closures; capture is now automatic. See
256
274
  `examples/035-protected-capture`.
257
275
 
258
276
  ## Faz 36
@@ -837,6 +855,11 @@ Faz 174 adds `Array#remove_all(value)` for removing every matching value. See
837
855
  Faz 175 preserves null-safe optional access for array mutation helpers. See
838
856
  `examples/175-optional-array-mutations`.
839
857
 
858
+ On non-array receivers, these helpers delegate to the receiver's own method,
859
+ preserving every supplied argument and the method receiver. This includes
860
+ `remove`, `remove_all`, `clear`, `remove_at` and `insert_at`; an undefined method
861
+ result becomes null. Array mutation semantics remain as described above.
862
+
840
863
  ## Faz 176
841
864
 
842
865
  Faz 176 requires `to_js_number()` before a Decimal enters a direct host method
@@ -901,3 +924,34 @@ are native finite numbers.
901
924
  ## Faz 209
902
925
 
903
926
  Faz 209 rejects dynamic non-numeric range bounds.
927
+
928
+ ## Implicit method receivers
929
+
930
+ Inside a class method, call another method as `prepare()`; instance and static
931
+ methods resolve within their respective scopes. Use the readonly `@this` only
932
+ when the receiver itself is needed. Callbacks automatically retain the receiver
933
+ for method calls and member access; outer locals and captured context bindings
934
+ still require `capture`. The former `this` and `this.method()` spellings produce migration
935
+ diagnostics. See [the class guide](design/guide.md#public),
936
+ [D-063](design/decisions.md#d-063-method-calls-have-an-implicit-receiver), and
937
+ `examples/261-implicit-receiver` and `examples/262-automatic-receiver` for
938
+ executable examples.
939
+
940
+ ## Inheritance across modules
941
+
942
+ A Lango class can extend an imported Lango class, including an aliased import.
943
+ Builds resolve inherited public members, constructor defaults and `super` method
944
+ signatures across module boundaries. Bare inherited method calls and `@property`
945
+ access use the inherited receiver; public fields are not redeclared in the
946
+ subclass. An omitted subclass constructor forwards its arguments. Private method
947
+ access/overrides remain rejected. This supports browser control subclasses in
948
+ separate Lango modules without JavaScript wrappers.
949
+
950
+ Method visibility and implicit-receiver lookup apply to the current Lango
951
+ receiver. Calls to a JavaScript namespace or another object keep their own method
952
+ and boundary normalization, even when the current class declares a private or
953
+ public method with the same name.
954
+
955
+ The CLI test runner waits for a Promise returned by a Lango test before
956
+ reporting its result and resetting state for the next test. Async rejections
957
+ are reported as test failures and remaining tests continue.
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.10",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/cli.mjs CHANGED
@@ -154,7 +154,7 @@ if (command === "init" && inputPath && rest.length === 0) {
154
154
  try {
155
155
  program.__langoResetTests?.();
156
156
  runtime.configure({ output: (value) => console.log(value), catalog, database_adapter: database });
157
- test.run();
157
+ await test.run();
158
158
  console.log(`PASS ${test.name}`);
159
159
  } catch (error) {
160
160
  failures += 1;
package/src/compiler.mjs CHANGED
@@ -30,20 +30,24 @@ export function formatDiagnostics(error) {
30
30
  }).join("\n");
31
31
  }
32
32
 
33
- export async function buildFile(inputPath, outputPath, seen = new Set(), includeTests = false, profile = {}) {
33
+ export async function buildFile(inputPath, outputPath, seen = new Set(), includeTests = false, profile = {}, layout = null) {
34
+ inputPath = path.resolve(inputPath);
35
+ outputPath = path.resolve(outputPath);
34
36
  const decimalProfile = validateDecimalProfile(profile);
35
37
  if (decimalProfile.target === "browser" && profile.executable)
36
38
  throw new LangoError("LNG1266", 1, "browser builds cannot use --executable", inputPath);
37
39
  const isEntryPoint = seen.size === 0;
38
- if (isEntryPoint)
39
- await validateModuleInitializerCycles(inputPath, outputPath);
40
+ if (layout === null) {
41
+ const modules = await validateModuleInitializerCycles(inputPath, outputPath);
42
+ layout = moduleOutputLayout(inputPath, outputPath, modules);
43
+ }
40
44
  if (seen.has(inputPath)) return;
41
45
  seen.add(inputPath);
42
46
 
43
47
  const source = await readFile(inputPath, "utf8");
44
- const imports = collectImports(source, inputPath, outputPath);
48
+ const imports = collectImports(source, inputPath, outputPath, layout);
45
49
  for (const item of imports)
46
- await buildFile(item.sourcePath, item.outputPath, seen, includeTests, decimalProfile);
50
+ await buildFile(item.sourcePath, item.outputPath, seen, includeTests, decimalProfile, layout);
47
51
  for (const item of imports) {
48
52
  const importedProgram = new Parser(
49
53
  await readFile(item.sourcePath, "utf8"),
@@ -77,14 +81,16 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
77
81
  1,
78
82
  `${inputPath}: import signature for ${selection.sourceName} does not match ${item.sourcePath}`,
79
83
  );
80
- if (selection.kind === "class")
84
+ if (selection.kind === "class") {
85
+ selection.classMetadata = await importedClassMetadata(item.sourcePath, selection.sourceName);
81
86
  validateImportedClassInterface(
82
87
  selection,
83
- target,
84
- importedProgram,
88
+ selection.classMetadata,
89
+ { classes: [selection.classMetadata] },
85
90
  inputPath,
86
91
  item.sourcePath,
87
92
  );
93
+ }
88
94
  }
89
95
  }
90
96
 
@@ -103,6 +109,7 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
103
109
  `//# sourceMappingURL=${path.basename(outputPath)}.map`,
104
110
  `const { configure } = await import("./.lango/runtime.mjs");\nconfigure({ output: (value) => console.log(value) });\nawait main(process.argv.slice(2));\n//# sourceMappingURL=${path.basename(outputPath)}.map`,
105
111
  );
112
+ output = `#!/usr/bin/env node\n${output}`;
106
113
  }
107
114
  const directory = path.dirname(outputPath);
108
115
 
@@ -283,6 +290,17 @@ class Parser {
283
290
  this.blank();
284
291
  continue;
285
292
  }
293
+ const hostImport = /^import js global ([A-Za-z_]\w*(?:\/[A-Za-z_]\w*)*)(?: as ([A-Za-z_]\w*))?$/.exec(line.text);
294
+ if (hostImport) {
295
+ if (declarationSeen)
296
+ this.fail(line, "LNG1121", "imports must precede class and function declarations");
297
+ const name = hostImport[2] ?? hostImport[1].split("/").at(-1);
298
+ if (jsImports.some((item) => item.name === name))
299
+ this.fail(line, "LNG1122", `duplicate JavaScript import name ${name}`);
300
+ jsImports.push({ name, globalPath: hostImport[1].split("/").map(kebabName) });
301
+ this.blank();
302
+ continue;
303
+ }
286
304
  const jsImport = /^import js "([^"]+)" as ([A-Za-z_]\w*)$/.exec(line.text);
287
305
  if (jsImport) {
288
306
  if (declarationSeen)
@@ -1074,8 +1092,12 @@ class Parser {
1074
1092
  }
1075
1093
  captures() {
1076
1094
  if (!(this.peek() ?? "").trimStart().startsWith("capture ")) return [];
1077
- const line = this.line();
1078
- const names = splitTopLevel(line.text.trim().slice(8));
1095
+ const line = { text: this.peek(), number: this.i + 1 };
1096
+ const indent = spaces(line.text);
1097
+ const names = [];
1098
+ while (spaces(this.peek() ?? "") === indent && (this.peek() ?? "").trimStart().startsWith("capture ")) {
1099
+ names.push(...splitTopLevel(this.line().text.trim().slice(8)));
1100
+ }
1079
1101
  if (
1080
1102
  names.some(
1081
1103
  (name) =>
@@ -1116,7 +1138,7 @@ class Parser {
1116
1138
  value:
1117
1139
  match[2] === "{"
1118
1140
  ? this.object(indent + 4)
1119
- : expr(match[2], line, this),
1141
+ : expr(this.continuedStatement(match[2], line, indent + 4), line, this),
1120
1142
  });
1121
1143
  }
1122
1144
  return { type: "object", fields };
@@ -1550,6 +1572,8 @@ function expr(source, line, parser) {
1550
1572
  : [],
1551
1573
  line: line.number,
1552
1574
  };
1575
+ if (methodCall?.[1] === "this")
1576
+ parser.fail(line, "LNG1270", `use ${methodCall[3]}(...) without this.`);
1553
1577
  if (methodCall)
1554
1578
  return {
1555
1579
  type: "methodCall",
@@ -1587,6 +1611,8 @@ function expr(source, line, parser) {
1587
1611
  return { type: "raw", value: text };
1588
1612
  }
1589
1613
  if (/^(true|false|null)$/.test(text)) return { type: "raw", value: text };
1614
+ if (/^this(?:$|[.\[?])/.test(text))
1615
+ parser.fail(line, "LNG1270", "use @this for the current receiver and method(...) for its methods");
1590
1616
  if (/^@{0,2}\$?[A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w*)*(?:\.|\?\.)_[A-Za-z_]\w*$/.test(text))
1591
1617
  parser.fail(
1592
1618
  line,
@@ -1803,6 +1829,8 @@ function validate(program, imports = []) {
1803
1829
  ),
1804
1830
  );
1805
1831
  const jsImportNames = new Set(program.jsImports.map((item) => item.name));
1832
+ const jsModuleNames = new Set(program.jsImports.filter((item) => !item.globalPath).map((item) => item.name));
1833
+ const hostImportNames = new Set(program.jsImports.filter((item) => item.globalPath).map((item) => item.name));
1806
1834
  const importedFunctions = new Map(
1807
1835
  imports.flatMap((item) =>
1808
1836
  (item.selections ?? [])
@@ -1836,7 +1864,7 @@ function validate(program, imports = []) {
1836
1864
  ["xml2obj", 1],
1837
1865
  ["start_trans", 0], ["commit_trans", 0], ["rollback_trans", 0], ["trans_state", 0],
1838
1866
  ]);
1839
- const hostFunctions = new Set(["fetch"]);
1867
+ const hostFunctions = new Set(["fetch", ...hostImportNames]);
1840
1868
  const isDecimalValue = (value) =>
1841
1869
  value &&
1842
1870
  (value.type === "decimal" ||
@@ -1988,7 +2016,12 @@ function validate(program, imports = []) {
1988
2016
  1,
1989
2017
  `imported local name ${name} conflicts with a Lango declaration`,
1990
2018
  ));
1991
- const classes = new Map(program.classes.map((item) => [item.name, item]));
2019
+ const classes = new Map([
2020
+ ...imports.flatMap((item) => (item.selections ?? [])
2021
+ .filter((selection) => selection.classMetadata && program.classes.some((node) => node.ancestor === selection.localName))
2022
+ .map((selection) => [selection.localName, { ...selection.classMetadata, name: selection.localName }])),
2023
+ ...program.classes.map((item) => [item.name, item]),
2024
+ ]);
1992
2025
  duplicateDiagnostics(program.classes, "LNG1205", "class name");
1993
2026
  if (nameDiagnostics.length) {
1994
2027
  const [primary] = nameDiagnostics;
@@ -2112,6 +2145,8 @@ function validate(program, imports = []) {
2112
2145
  if (node.type === "assign" && !node.name.startsWith("@"))
2113
2146
  result.add(node.name);
2114
2147
  if (node.type === "closureAssign") result.add(node.name);
2148
+ if (node.type === "closureAssign" || node.type === "closure") continue;
2149
+ if (["contextDeclare", "contextSet", "globalDeclare"].includes(node.type)) result.add(node.name);
2115
2150
  if (node.type === "for") result.add(node.item);
2116
2151
  if (node.type === "forObject") {
2117
2152
  result.add(node.key);
@@ -2260,6 +2295,24 @@ function validate(program, imports = []) {
2260
2295
  findMethod(classNode, "constructor", false);
2261
2296
  const walk = (value, localCallables, classNode = null, staticMethod = false) => {
2262
2297
  if (!value || typeof value !== "object") return;
2298
+ if (value.parameters && value.body)
2299
+ localCallables = new Set([...localCallables, ...value.parameters.map((parameter) => parameter.name), ...dynamicBindingNames(value.body)]);
2300
+ const bindings = [
2301
+ ...(["assign", "closureAssign", "increment", "contextSet", "contextDeclare", "globalDeclare"].includes(value.type) ? [value.name] : []),
2302
+ ...(value.parameters ?? []).map((parameter) => parameter.name),
2303
+ ...(value.type === "for" ? [value.item] : []),
2304
+ ...(value.type === "forObject" ? [value.key, value.value] : []),
2305
+ ...(value.catchName ? [value.catchName] : []),
2306
+ ];
2307
+ const readonly = bindings.find((name) => hostImportNames.has(name));
2308
+ if (readonly)
2309
+ throw new LangoError("LNG1268", value.line ?? 1, `${readonly} is a readonly host import and cannot be rebound or shadowed`);
2310
+ if (bindings.includes("this") || bindings.includes("@this"))
2311
+ throw new LangoError("LNG1270", value.line ?? 1, "the current receiver cannot be rebound or shadowed");
2312
+ if (value.type === "methodCall" && value.receiver === "this" && !value.implicitReceiver)
2313
+ throw new LangoError("LNG1270", value.line ?? 1, `use ${value.name}(...) without this.`);
2314
+ if (typeof value.receiver === "string" && /^@this(?:$|[.\[?])/.test(value.receiver) && !classNode)
2315
+ throw new LangoError("LNG1270", value.line ?? 1, "@this is only valid in a class method");
2263
2316
  if (value.type === "binary" && ["+", "-", "*", "/", "%", "**", ">", "<", ">=", "<="].includes(value.operator)) {
2264
2317
  const string = (item) => item.type === "string";
2265
2318
  const number = (item) => item.type === "raw" && /^\d/.test(item.value);
@@ -2273,6 +2326,21 @@ function validate(program, imports = []) {
2273
2326
  (leftKind === "string" && value.operator !== "+")))
2274
2327
  throw new LangoError("LNG1254", value.line ?? 1, "primitive values do not coerce in operators");
2275
2328
  }
2329
+ if (value.type === "call") {
2330
+ const member = classNode && findMethodWithOwner(classNode, value.name, staticMethod);
2331
+ if (member) {
2332
+ const competing = names.has(value.name) || imported.has(value.name) || globals.has(value.name) ||
2333
+ standardFunctions.has(value.name) || hostFunctions.has(value.name) ||
2334
+ localCallables.has(value.name);
2335
+ if (competing)
2336
+ throw new LangoError("LNG1269", value.line, `ambiguous call ${value.name}: a method and another binding have the same name`);
2337
+ value.type = "methodCall";
2338
+ value.receiver = "this";
2339
+ value.implicitReceiver = true;
2340
+ }
2341
+ }
2342
+ if (value.type === "raw" && /^@this(?:$|[.\[?])/.test(value.value) && !classNode)
2343
+ throw new LangoError("LNG1270", value.line ?? 1, "@this is only valid in a class method");
2276
2344
  if (value.type === "call") {
2277
2345
  const target = names.get(value.name) ?? importedFunctions.get(value.name);
2278
2346
  if (
@@ -2336,7 +2404,7 @@ function validate(program, imports = []) {
2336
2404
  value.line,
2337
2405
  `use / between JavaScript namespace segments in new ${value.name}(...)`,
2338
2406
  );
2339
- if (value.name.includes("/")) validateJavaScriptArguments(value, value.name);
2407
+ if (value.name.includes("/") || hostImportNames.has(value.name)) validateJavaScriptArguments(value, value.name);
2340
2408
  const target = classes.get(value.name);
2341
2409
  const constructor = target && findConstructor(target);
2342
2410
  if (constructor) validateRequiredArguments(constructor, value, value.name);
@@ -2344,15 +2412,16 @@ function validate(program, imports = []) {
2344
2412
  if (value.type === "methodCall" && typeof value.receiver === "string") {
2345
2413
  if (
2346
2414
  value.receiver.includes(".") &&
2347
- jsImportNames.has(value.receiver.split(".")[0])
2415
+ jsModuleNames.has(value.receiver.split(".")[0])
2348
2416
  )
2349
2417
  throw new LangoError(
2350
2418
  "LNG1232",
2351
2419
  value.line,
2352
2420
  `use / between JavaScript namespace segments in ${value.receiver}.${value.name}(...)`,
2353
2421
  );
2422
+ const currentReceiver = value.receiver === "this" || value.receiver === "@this";
2354
2423
  const targetClass =
2355
- value.receiver === "this"
2424
+ currentReceiver
2356
2425
  ? classNode
2357
2426
  : classes.get(value.receiver) ?? null;
2358
2427
  const importedStaticMethod =
@@ -2379,13 +2448,13 @@ function validate(program, imports = []) {
2379
2448
  findMethodWithOwner(
2380
2449
  targetClass,
2381
2450
  value.name,
2382
- value.receiver === "this" ? staticMethod : true,
2451
+ currentReceiver ? staticMethod : true,
2383
2452
  );
2384
2453
  if (targetClass && !target)
2385
2454
  throw new LangoError(
2386
2455
  "LNG1241",
2387
2456
  value.line,
2388
- `${targetClass.name}: unknown ${value.receiver === "this" ? "instance" : "static"} method ${value.name}`,
2457
+ `${targetClass.name}: unknown ${currentReceiver && !staticMethod ? "instance" : "static"} method ${value.name}`,
2389
2458
  );
2390
2459
  if (
2391
2460
  target &&
@@ -2404,6 +2473,8 @@ function validate(program, imports = []) {
2404
2473
  `${targetClass.name}${target.method.static ? "::" : ":"}${value.name}`,
2405
2474
  );
2406
2475
  }
2476
+ if (value.type === "methodCall" && hostImportNames.has(receiverRoot(value.receiver)))
2477
+ validateJavaScriptArguments(value, `${receiverRoot(value.receiver)}.${value.name}`);
2407
2478
  if (value.type === "methodCall" && receiverRoot(value.receiver) === "args")
2408
2479
  validateJavaScriptArguments(value, `args.${value.name}`);
2409
2480
  if (value.type === "namespaceMethodCall") {
@@ -2445,12 +2516,16 @@ function validate(program, imports = []) {
2445
2516
  );
2446
2517
  validateRequiredArguments(target, value, `super.${value.name}`);
2447
2518
  }
2448
- Object.values(value).forEach((item) =>
2519
+ Object.entries(value).forEach(([key, item]) =>
2449
2520
  Array.isArray(item)
2450
- ? item.forEach((child) => walk(child, localCallables, classNode, staticMethod))
2521
+ ? item.forEach((child) => walk(child, key === "catchBody" && value.catchName ? new Set([...localCallables, value.catchName]) : localCallables, classNode, staticMethod))
2451
2522
  : walk(item, localCallables, classNode, staticMethod),
2452
2523
  );
2453
2524
  };
2525
+ program.globals.forEach((global) => walk(global, new Set()));
2526
+ program.classes.forEach((classNode) =>
2527
+ [...classNode.staticValues, ...classNode.enumValues].forEach((value) => walk(value, new Set())),
2528
+ );
2454
2529
  [...program.functions, ...program.tests].forEach((fn) =>
2455
2530
  walk(
2456
2531
  fn,
@@ -2496,6 +2571,7 @@ function validate(program, imports = []) {
2496
2571
  return;
2497
2572
  }
2498
2573
  if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
2574
+ if (node.implicitReceiver) references.add("@this");
2499
2575
  if (typeof node.receiver === "string" && node.receiver !== "this")
2500
2576
  references.add(node.receiver.split(/[./]/)[0]);
2501
2577
  else if (typeof node.receiver !== "string")
@@ -2827,17 +2903,20 @@ function validate(program, imports = []) {
2827
2903
  !hostFunctions.has(name),
2828
2904
  );
2829
2905
  const captures = new Set(closure.captures);
2906
+ const automaticCaptures = new Set(needed.filter((name) =>
2907
+ classScope && (name.startsWith("@") || name.startsWith("_")),
2908
+ ));
2830
2909
  const contextCaptures = new Set(
2831
2910
  closure.captures.filter((name) => availableContexts.has(name)),
2832
2911
  );
2833
2912
  for (const name of needed)
2834
- if (!captures.has(name))
2913
+ if (!captures.has(name) && !automaticCaptures.has(name))
2835
2914
  throw new LangoError(
2836
2915
  "LNG1210",
2837
2916
  closure.line,
2838
2917
  `MissingCapture: ${name}; add capture ${name}`,
2839
2918
  );
2840
- for (const name of captures) {
2919
+ for (const name of new Set([...captures, ...automaticCaptures])) {
2841
2920
  if (!needed.includes(name) && !contextCaptures.has(name))
2842
2921
  throw new LangoError(
2843
2922
  "LNG1211",
@@ -2853,6 +2932,11 @@ function validate(program, imports = []) {
2853
2932
  `capture ${name} is not assigned before this closure`,
2854
2933
  );
2855
2934
  if (propertyCapture) {
2935
+ if (name === "@this") {
2936
+ if (!classScope)
2937
+ throw new LangoError("LNG1213", closure.line, "@this is only valid in a class method");
2938
+ continue;
2939
+ }
2856
2940
  const staticMember = name.startsWith("@@");
2857
2941
  const property = name.startsWith("@")
2858
2942
  ? name.slice(staticMember ? 2 : 1)
@@ -3088,6 +3172,8 @@ function validate(program, imports = []) {
3088
3172
  program.functions.forEach((fn) => validateLoopLevels(fn.body));
3089
3173
  program.tests.forEach((test) => validateLoopLevels(test.body));
3090
3174
  for (const classNode of program.classes) {
3175
+ if (classNode.publicProperties.includes("this"))
3176
+ throw new LangoError("LNG1270", classNode.line, "this is reserved for the current receiver");
3091
3177
  if (classNode.ancestor && !classes.has(classNode.ancestor))
3092
3178
  throw new LangoError(
3093
3179
  "LNG1206",
@@ -3282,6 +3368,7 @@ function validate(program, imports = []) {
3282
3368
  );
3283
3369
  }
3284
3370
  if (name && /^@[A-Za-z_]\w*$/.test(name)) {
3371
+ if (name === "@this") return;
3285
3372
  if (!classScope.instanceMembers.has(name.slice(1)))
3286
3373
  throw new LangoError(
3287
3374
  "LNG1222",
@@ -3312,7 +3399,7 @@ function validate(program, imports = []) {
3312
3399
  : null;
3313
3400
  if (
3314
3401
  name &&
3315
- (/^@[A-Za-z_]\w*$/.test(name) || /^_[A-Za-z_]\w*$/.test(name))
3402
+ ((/^@[A-Za-z_]\w*$/.test(name) && name !== "@this") || /^_[A-Za-z_]\w*$/.test(name))
3316
3403
  )
3317
3404
  throw new LangoError(
3318
3405
  "LNG1231",
@@ -3389,6 +3476,12 @@ function validate(program, imports = []) {
3389
3476
 
3390
3477
  function emit(program, moduleImports = [], includeTests = false) {
3391
3478
  const flags = {
3479
+ classes: new Map([
3480
+ ...moduleImports.flatMap((item) => (item.selections ?? [])
3481
+ .filter((selection) => selection.classMetadata)
3482
+ .map((selection) => [selection.localName, selection.classMetadata])),
3483
+ ...program.classes.map((item) => [item.name, item]),
3484
+ ]),
3392
3485
  print: false,
3393
3486
  translate: false,
3394
3487
  context: false,
@@ -3401,6 +3494,7 @@ function emit(program, moduleImports = [], includeTests = false) {
3401
3494
  item.names.map((name) => name.split(" as ").at(-1)),
3402
3495
  ),
3403
3496
  ),
3497
+ hostGlobals: new Map(program.jsImports.filter((item) => item.globalPath).map((item) => [item.name, item.globalPath])),
3404
3498
  };
3405
3499
  const classes = program.classes
3406
3500
  .map((item) => emitClass(item, flags))
@@ -3424,7 +3518,18 @@ function emit(program, moduleImports = [], includeTests = false) {
3424
3518
  .concat("...__langoOwnTests")
3425
3519
  .join(", ")}];\n`
3426
3520
  : "";
3521
+ const globals = program.globals
3522
+ .map((item) => {
3523
+ const value = emitExpr(item.value, flags);
3524
+ const prefix = item.exported ? "export let" : "let";
3525
+ if (!item.name.startsWith("$")) return `${prefix} ${item.name} = ${value};\n`;
3526
+ flags.decimal = true;
3527
+ return `${prefix} ${item.name} = __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value});\n`;
3528
+ })
3529
+ .join("");
3427
3530
  const helpers = [
3531
+ flags.hostGlobal && "hostGlobal as __hostGlobal",
3532
+ flags.hostGlobalCall && "hostGlobalCall as __hostGlobalCall",
3428
3533
  flags.decimal && "decimal as __decimal",
3429
3534
  flags.print && "println as __println",
3430
3535
  flags.translate && "translate as __translate",
@@ -3482,20 +3587,12 @@ function emit(program, moduleImports = [], includeTests = false) {
3482
3587
  )
3483
3588
  .join("");
3484
3589
  const jsModules = program.jsImports
3590
+ .filter((item) => !item.globalPath)
3485
3591
  .map(
3486
3592
  (item) =>
3487
3593
  `import * as ${item.name} from ${JSON.stringify(item.specifier)};\n`,
3488
3594
  )
3489
3595
  .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
3596
  const testReset = includeTests
3500
3597
  ? `export function __langoResetTests() {\n${program.globals
3501
3598
  .map((item) => ` ${item.name} = ${emitExpr(item.value, flags)};\n`)
@@ -3575,6 +3672,13 @@ function emitClass(item, flags) {
3575
3672
  method.public,
3576
3673
  ]),
3577
3674
  );
3675
+ for (let ancestor = flags.classes.get(item.ancestor); ancestor; ancestor = flags.classes.get(ancestor.ancestor)) {
3676
+ ancestor.publicProperties.forEach((name) => flags.publicProperties.add(name));
3677
+ for (const method of ancestor.methods) {
3678
+ const key = `${method.static ? "static" : "instance"}:${method.name}`;
3679
+ if (method.public && !flags.methods.has(key)) flags.methods.set(key, true);
3680
+ }
3681
+ }
3578
3682
  const fields = [
3579
3683
  ...new Set(
3580
3684
  item.methods
@@ -3585,7 +3689,7 @@ function emitClass(item, flags) {
3585
3689
  .filter((name) => !name.startsWith("@@"))
3586
3690
  .map((name) => name.slice(1)),
3587
3691
  ),
3588
- ].filter((field) => !flags.publicProperties.has(field));
3692
+ ].filter((field) => field !== "this" && !flags.publicProperties.has(field));
3589
3693
  const implicitConstructor =
3590
3694
  item.ancestor &&
3591
3695
  !item.methods.some((method) => method.name === "constructor")
@@ -3665,7 +3769,8 @@ function emitFunction(fn, flags) {
3665
3769
  .map((node) => node.name),
3666
3770
  );
3667
3771
  const hoisted = new Set(
3668
- [...collectEmitLocals(fn.body)].filter((name) => !topLevelAssigned.has(name)),
3772
+ [...collectEmitLocals(fn.body)].filter((name) =>
3773
+ !topLevelAssigned.has(name) && !fn.parameters.some((parameter) => parameter.name === name)),
3669
3774
  );
3670
3775
  const declared = new Set([...fn.parameters.map((p) => p.name), ...hoisted]);
3671
3776
  const priorArrayBindings = flags.arrayBindings;
@@ -3984,6 +4089,12 @@ function emitBoundaryChecks(parameters, depth, flags) {
3984
4089
  .join("");
3985
4090
  }
3986
4091
  function emitReceiverName(name, flags) {
4092
+ if (/^@this(?:$|[.\[?])/.test(name)) return `this${name.slice(5)}`;
4093
+ const host = /^([A-Za-z_]\w*)(.*)$/.exec(name);
4094
+ if (host && flags.hostGlobals?.has(host[1])) {
4095
+ flags.hostGlobal = true;
4096
+ return `__hostGlobal(${JSON.stringify(flags.hostGlobals.get(host[1]))})${emitKebabPath(host[2])}`;
4097
+ }
3987
4098
  const member = /^(@{1,2})(\$?[A-Za-z_]\w*)(.*)$/.exec(name);
3988
4099
  if (member) {
3989
4100
  const [, scope, property, suffix] = member;
@@ -4002,6 +4113,7 @@ function emitReceiverName(name, flags) {
4002
4113
  }
4003
4114
 
4004
4115
  function emitExpr(node, flags) {
4116
+ if (node.type === "raw" && node.value === "@this") return "this";
4005
4117
  const decimalRaw = (value) =>
4006
4118
  value?.type === "raw" &&
4007
4119
  (value.value.startsWith("$") || value.value.includes(".$"));
@@ -4014,6 +4126,13 @@ function emitExpr(node, flags) {
4014
4126
  flags.normalize = true;
4015
4127
  return `__normalize(${value})`;
4016
4128
  };
4129
+ if (node.type === "raw" && flags.hostGlobals.has(node.value.split(/[.\[?]/)[0]))
4130
+ return normalize(emitReceiverName(node.value, flags));
4131
+ if (node.type === "call" && flags.hostGlobals.has(node.name)) {
4132
+ flags.hostGlobalCall = true;
4133
+ const arguments_ = node.arguments.map((item) => item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags)).join(", ");
4134
+ return `__hostGlobalCall(${JSON.stringify(flags.hostGlobals.get(node.name))}${arguments_ ? `, ${arguments_}` : ""})`;
4135
+ }
4017
4136
  if (node.type === "raw")
4018
4137
  if (/^@{1,2}\$?[A-Za-z_]\w*[.\[?]/.test(node.value))
4019
4138
  return normalize(emitReceiverName(node.value, flags));
@@ -4076,7 +4195,7 @@ function emitExpr(node, flags) {
4076
4195
  return `__finite(-${numericLiteral ? emitExpr(node.value, flags) : `__requireNumber(${emitExpr(node.value, flags)})`})`;
4077
4196
  }
4078
4197
  if (node.type === "new")
4079
- return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
4198
+ return `new ${flags.hostGlobals.has(node.name.split("/")[0]) ? `(${emitReceiverName(node.name.replaceAll("/", "."), flags)})` : node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
4080
4199
  item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
4081
4200
  ).join(", ")})`;
4082
4201
  if (node.type === "superMethodCall")
@@ -4092,7 +4211,7 @@ function emitExpr(node, flags) {
4092
4211
  if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
4093
4212
  const namespace = node.type === "namespaceMethodCall";
4094
4213
  const sourceReceiver = namespace
4095
- ? node.receiver.replaceAll("/", ".")
4214
+ ? emitReceiverName(node.receiver.replaceAll("/", "."), flags)
4096
4215
  : typeof node.receiver === "string"
4097
4216
  ? emitReceiverName(node.receiver, flags)
4098
4217
  : emitExpr(node.receiver, flags);
@@ -4101,7 +4220,8 @@ function emitExpr(node, flags) {
4101
4220
  (sourceReceiver === "this" && flags.staticMethod)
4102
4221
  ? "static"
4103
4222
  : "instance";
4104
- const visibility = flags.methods?.get(`${type}:${node.name}`);
4223
+ const ownReceiver = !namespace && (sourceReceiver === "this" || sourceReceiver === flags.className);
4224
+ const visibility = ownReceiver ? flags.methods?.get(`${type}:${node.name}`) : undefined;
4105
4225
  const receiver =
4106
4226
  sourceReceiver === "this" && flags.staticMethod && visibility === false
4107
4227
  ? flags.className
@@ -4329,7 +4449,7 @@ function emitString(parts, flags) {
4329
4449
  function spaces(line) {
4330
4450
  return line.length - line.trimStart().length;
4331
4451
  }
4332
- function collectImports(source, inputPath, outputPath) {
4452
+ function collectImports(source, inputPath, outputPath, layout = null) {
4333
4453
  const lines = source.split(/\r?\n/);
4334
4454
  const items = [];
4335
4455
  for (let index = 0; index < lines.length; index += 1) {
@@ -4378,7 +4498,7 @@ function collectImports(source, inputPath, outputPath) {
4378
4498
  path.dirname(inputPath),
4379
4499
  `${match[1]}.lango`,
4380
4500
  );
4381
- const outputPathForImport = path.resolve(
4501
+ const outputPathForImport = layout?.get(sourcePath) ?? path.resolve(
4382
4502
  path.dirname(outputPath),
4383
4503
  `${match[1]}.mjs`,
4384
4504
  );
@@ -4387,12 +4507,38 @@ function collectImports(source, inputPath, outputPath) {
4387
4507
  selections,
4388
4508
  sourcePath,
4389
4509
  outputPath: outputPathForImport,
4390
- specifier: `./${path.basename(outputPathForImport)}`,
4510
+ specifier: relativeModuleSpecifier(outputPath, outputPathForImport),
4391
4511
  });
4392
4512
  }
4393
4513
  return items;
4394
4514
  }
4395
4515
 
4516
+ function relativeModuleSpecifier(from, to) {
4517
+ const relative = path.relative(path.dirname(from), to).split(path.sep).join("/");
4518
+ return relative.startsWith("../") ? relative : `./${relative}`;
4519
+ }
4520
+
4521
+ function moduleOutputLayout(entryPath, entryOutputPath, modules) {
4522
+ let sourceRoot = path.dirname(entryPath);
4523
+ for (const sourcePath of modules.keys()) {
4524
+ while (true) {
4525
+ const relative = path.relative(sourceRoot, sourcePath);
4526
+ if (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) break;
4527
+ sourceRoot = path.dirname(sourceRoot);
4528
+ }
4529
+ }
4530
+ const outputRoot = path.dirname(entryOutputPath);
4531
+ const dependencyPath = (sourcePath, root) =>
4532
+ path.join(root, path.relative(sourceRoot, sourcePath).replace(/\.lango$/, ".mjs"));
4533
+ const collision = [...modules.keys()].some((sourcePath) =>
4534
+ sourcePath !== entryPath && dependencyPath(sourcePath, outputRoot) === entryOutputPath);
4535
+ const dependencyRoot = collision ? path.join(outputRoot, ".lango/modules") : outputRoot;
4536
+ return new Map([...modules.keys()].map((sourcePath) => [
4537
+ sourcePath,
4538
+ sourcePath === entryPath ? entryOutputPath : dependencyPath(sourcePath, dependencyRoot),
4539
+ ]));
4540
+ }
4541
+
4396
4542
  async function validateModuleInitializerCycles(entryPath, entryOutputPath) {
4397
4543
  const modules = new Map();
4398
4544
  const visit = async (modulePath, outputPath) => {
@@ -4450,6 +4596,7 @@ async function validateModuleInitializerCycles(entryPath, entryOutputPath) {
4450
4596
  `${modulePath}: cyclic module initializer reads imported ${selection.kind} ${selection.localName}`,
4451
4597
  );
4452
4598
  }
4599
+ return modules;
4453
4600
  }
4454
4601
  function collectClassImportInterface(lines, getIndex, setIndex) {
4455
4602
  const api = {
@@ -4494,6 +4641,46 @@ function collectClassImportInterface(lines, getIndex, setIndex) {
4494
4641
  setIndex(index);
4495
4642
  return api;
4496
4643
  }
4644
+ async function importedClassMetadata(sourcePath, name, visiting = new Set()) {
4645
+ const key = `${sourcePath}:${name}`;
4646
+ if (visiting.has(key))
4647
+ throw new LangoError("LNG1207", 1, `cyclic class inheritance involving ${name}`, sourcePath);
4648
+ const chain = new Set(visiting).add(key);
4649
+ const source = await readFile(sourcePath, "utf8");
4650
+ const program = new Parser(source, sourcePath).parse();
4651
+ const target = program.classes.find((item) => item.name === name);
4652
+ if (!target) throw new LangoError("LNG1206", 1, `unknown ancestor ${name}`, sourcePath);
4653
+ let ancestor = null;
4654
+ if (target.ancestor) {
4655
+ if (program.classes.some((item) => item.name === target.ancestor))
4656
+ ancestor = await importedClassMetadata(sourcePath, target.ancestor, chain);
4657
+ else {
4658
+ for (const item of collectImports(source, sourcePath, sourcePath)) {
4659
+ const selection = item.selections.find((entry) => entry.kind === "class" && entry.localName === target.ancestor);
4660
+ if (selection) {
4661
+ ancestor = await importedClassMetadata(item.sourcePath, selection.sourceName, chain);
4662
+ break;
4663
+ }
4664
+ }
4665
+ if (!ancestor) throw new LangoError("LNG1206", target.line, `unknown ancestor ${target.ancestor}`, sourcePath);
4666
+ }
4667
+ }
4668
+ const nearest = (own, inherited, keyOf) => [...new Map([
4669
+ ...inherited.map((item) => [keyOf(item), item]),
4670
+ ...own.map((item) => [keyOf(item), item]),
4671
+ ]).values()];
4672
+ const methodKey = (method) => `${method.static ? "static" : "instance"}:${method.name}`;
4673
+ return {
4674
+ ...target,
4675
+ ancestor: null,
4676
+ publicProperties: [...new Set([...(ancestor?.publicProperties ?? []), ...target.publicProperties])],
4677
+ methods: nearest(target.methods, ancestor?.methods ?? [], methodKey),
4678
+ publicMethods: nearest(target.publicMethods, ancestor?.publicMethods ?? [], methodKey),
4679
+ staticValues: nearest(target.staticValues, ancestor?.staticValues ?? [], (item) => item.name),
4680
+ enumValues: nearest(target.enumValues, ancestor?.enumValues ?? [], (item) => item.name),
4681
+ };
4682
+ }
4683
+
4497
4684
  function validateImportedClassInterface(
4498
4685
  selection,
4499
4686
  target,
package/src/runtime.mjs CHANGED
@@ -427,7 +427,8 @@ export function add(value, ...items) {
427
427
  throw new Error("NotAddable");
428
428
  }
429
429
 
430
- export function remove(value, item) {
430
+ export function remove(value, ...items) {
431
+ const [item] = items;
431
432
  if (Array.isArray(value)) {
432
433
  const index = value.indexOf(item);
433
434
  if (index < 0) return false;
@@ -435,12 +436,13 @@ export function remove(value, item) {
435
436
  return true;
436
437
  }
437
438
  if (value !== null && typeof value?.remove === "function") {
438
- return normalize(value.remove(item));
439
+ return normalize(value.remove(...items));
439
440
  }
440
441
  throw new Error("NotRemovable");
441
442
  }
442
443
 
443
- export function remove_all(value, item) {
444
+ export function remove_all(value, ...items) {
445
+ const [item] = items;
444
446
  if (Array.isArray(value)) {
445
447
  let removed = 0;
446
448
  for (let index = value.length - 1; index >= 0; index -= 1) {
@@ -451,23 +453,24 @@ export function remove_all(value, item) {
451
453
  return removed;
452
454
  }
453
455
  if (value !== null && typeof value?.remove_all === "function") {
454
- return normalize(value.remove_all(item));
456
+ return normalize(value.remove_all(...items));
455
457
  }
456
458
  throw new Error("NotRemovable");
457
459
  }
458
460
 
459
- export function clear(value) {
461
+ export function clear(value, ...items) {
460
462
  if (Array.isArray(value)) {
461
463
  value.length = 0;
462
464
  return null;
463
465
  }
464
466
  if (value !== null && typeof value?.clear === "function") {
465
- return normalize(value.clear());
467
+ return normalize(value.clear(...items));
466
468
  }
467
469
  throw new Error("NotClearable");
468
470
  }
469
471
 
470
- export function remove_at(value, index) {
472
+ export function remove_at(value, ...items) {
473
+ const [index] = items;
471
474
  if (Array.isArray(value)) {
472
475
  if (!Number.isSafeInteger(index) || index < 0 || index >= value.length)
473
476
  return false;
@@ -475,12 +478,13 @@ export function remove_at(value, index) {
475
478
  return true;
476
479
  }
477
480
  if (value !== null && typeof value?.remove_at === "function") {
478
- return normalize(value.remove_at(index));
481
+ return normalize(value.remove_at(...items));
479
482
  }
480
483
  throw new Error("NotRemovable");
481
484
  }
482
485
 
483
- export function insert_at(value, index, item) {
486
+ export function insert_at(value, ...items) {
487
+ const [index, item] = items;
484
488
  if (Array.isArray(value)) {
485
489
  if (!Number.isSafeInteger(index) || index < 0 || index > value.length)
486
490
  return false;
@@ -488,7 +492,7 @@ export function insert_at(value, index, item) {
488
492
  return true;
489
493
  }
490
494
  if (value !== null && typeof value?.insert_at === "function") {
491
- return normalize(value.insert_at(index, item));
495
+ return normalize(value.insert_at(...items));
492
496
  }
493
497
  throw new Error("NotInsertable");
494
498
  }
@@ -548,6 +552,36 @@ export function call(value, ...arguments_) {
548
552
  return normalize(value(...arguments_));
549
553
  }
550
554
 
555
+ export class HostGlobalUnavailable extends Error {
556
+ constructor(path) {
557
+ super("HostGlobalUnavailable: " + path.join("/"));
558
+ this.name = "HostGlobalUnavailable";
559
+ }
560
+ }
561
+
562
+ function resolveHostGlobal(path) {
563
+ let owner = globalThis;
564
+ let value = owner;
565
+ for (const segment of path) {
566
+ owner = value;
567
+ if (owner === null || owner === undefined || !(segment in Object(owner)))
568
+ throw new HostGlobalUnavailable(path);
569
+ value = owner[segment];
570
+ }
571
+ if (value === undefined) throw new HostGlobalUnavailable(path);
572
+ return { owner, value };
573
+ }
574
+
575
+ export function hostGlobal(path) {
576
+ return resolveHostGlobal(path).value;
577
+ }
578
+
579
+ export function hostGlobalCall(path, ...arguments_) {
580
+ const { owner, value } = resolveHostGlobal(path);
581
+ if (typeof value !== "function") throw new Error("NotCallable");
582
+ return normalize(Reflect.apply(value, owner, arguments_));
583
+ }
584
+
551
585
  export function callMethod(receiver, name, ...arguments_) {
552
586
  const value = receiver?.[name];
553
587
  if (typeof value !== "function") throw new Error("NotCallable");