@lango-project/lango-js 0.1.8 → 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.8",
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
 
@@ -1565,6 +1572,8 @@ function expr(source, line, parser) {
1565
1572
  : [],
1566
1573
  line: line.number,
1567
1574
  };
1575
+ if (methodCall?.[1] === "this")
1576
+ parser.fail(line, "LNG1270", `use ${methodCall[3]}(...) without this.`);
1568
1577
  if (methodCall)
1569
1578
  return {
1570
1579
  type: "methodCall",
@@ -1602,6 +1611,8 @@ function expr(source, line, parser) {
1602
1611
  return { type: "raw", value: text };
1603
1612
  }
1604
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");
1605
1616
  if (/^@{0,2}\$?[A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w*)*(?:\.|\?\.)_[A-Za-z_]\w*$/.test(text))
1606
1617
  parser.fail(
1607
1618
  line,
@@ -2005,7 +2016,12 @@ function validate(program, imports = []) {
2005
2016
  1,
2006
2017
  `imported local name ${name} conflicts with a Lango declaration`,
2007
2018
  ));
2008
- 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
+ ]);
2009
2025
  duplicateDiagnostics(program.classes, "LNG1205", "class name");
2010
2026
  if (nameDiagnostics.length) {
2011
2027
  const [primary] = nameDiagnostics;
@@ -2129,6 +2145,8 @@ function validate(program, imports = []) {
2129
2145
  if (node.type === "assign" && !node.name.startsWith("@"))
2130
2146
  result.add(node.name);
2131
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);
2132
2150
  if (node.type === "for") result.add(node.item);
2133
2151
  if (node.type === "forObject") {
2134
2152
  result.add(node.key);
@@ -2277,7 +2295,7 @@ function validate(program, imports = []) {
2277
2295
  findMethod(classNode, "constructor", false);
2278
2296
  const walk = (value, localCallables, classNode = null, staticMethod = false) => {
2279
2297
  if (!value || typeof value !== "object") return;
2280
- if (value.type === "closure" || value.type === "closureAssign")
2298
+ if (value.parameters && value.body)
2281
2299
  localCallables = new Set([...localCallables, ...value.parameters.map((parameter) => parameter.name), ...dynamicBindingNames(value.body)]);
2282
2300
  const bindings = [
2283
2301
  ...(["assign", "closureAssign", "increment", "contextSet", "contextDeclare", "globalDeclare"].includes(value.type) ? [value.name] : []),
@@ -2289,6 +2307,12 @@ function validate(program, imports = []) {
2289
2307
  const readonly = bindings.find((name) => hostImportNames.has(name));
2290
2308
  if (readonly)
2291
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");
2292
2316
  if (value.type === "binary" && ["+", "-", "*", "/", "%", "**", ">", "<", ">=", "<="].includes(value.operator)) {
2293
2317
  const string = (item) => item.type === "string";
2294
2318
  const number = (item) => item.type === "raw" && /^\d/.test(item.value);
@@ -2302,6 +2326,21 @@ function validate(program, imports = []) {
2302
2326
  (leftKind === "string" && value.operator !== "+")))
2303
2327
  throw new LangoError("LNG1254", value.line ?? 1, "primitive values do not coerce in operators");
2304
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");
2305
2344
  if (value.type === "call") {
2306
2345
  const target = names.get(value.name) ?? importedFunctions.get(value.name);
2307
2346
  if (
@@ -2380,8 +2419,9 @@ function validate(program, imports = []) {
2380
2419
  value.line,
2381
2420
  `use / between JavaScript namespace segments in ${value.receiver}.${value.name}(...)`,
2382
2421
  );
2422
+ const currentReceiver = value.receiver === "this" || value.receiver === "@this";
2383
2423
  const targetClass =
2384
- value.receiver === "this"
2424
+ currentReceiver
2385
2425
  ? classNode
2386
2426
  : classes.get(value.receiver) ?? null;
2387
2427
  const importedStaticMethod =
@@ -2408,13 +2448,13 @@ function validate(program, imports = []) {
2408
2448
  findMethodWithOwner(
2409
2449
  targetClass,
2410
2450
  value.name,
2411
- value.receiver === "this" ? staticMethod : true,
2451
+ currentReceiver ? staticMethod : true,
2412
2452
  );
2413
2453
  if (targetClass && !target)
2414
2454
  throw new LangoError(
2415
2455
  "LNG1241",
2416
2456
  value.line,
2417
- `${targetClass.name}: unknown ${value.receiver === "this" ? "instance" : "static"} method ${value.name}`,
2457
+ `${targetClass.name}: unknown ${currentReceiver && !staticMethod ? "instance" : "static"} method ${value.name}`,
2418
2458
  );
2419
2459
  if (
2420
2460
  target &&
@@ -2476,12 +2516,16 @@ function validate(program, imports = []) {
2476
2516
  );
2477
2517
  validateRequiredArguments(target, value, `super.${value.name}`);
2478
2518
  }
2479
- Object.values(value).forEach((item) =>
2519
+ Object.entries(value).forEach(([key, item]) =>
2480
2520
  Array.isArray(item)
2481
- ? 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))
2482
2522
  : walk(item, localCallables, classNode, staticMethod),
2483
2523
  );
2484
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
+ );
2485
2529
  [...program.functions, ...program.tests].forEach((fn) =>
2486
2530
  walk(
2487
2531
  fn,
@@ -2527,6 +2571,7 @@ function validate(program, imports = []) {
2527
2571
  return;
2528
2572
  }
2529
2573
  if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
2574
+ if (node.implicitReceiver) references.add("@this");
2530
2575
  if (typeof node.receiver === "string" && node.receiver !== "this")
2531
2576
  references.add(node.receiver.split(/[./]/)[0]);
2532
2577
  else if (typeof node.receiver !== "string")
@@ -2858,17 +2903,20 @@ function validate(program, imports = []) {
2858
2903
  !hostFunctions.has(name),
2859
2904
  );
2860
2905
  const captures = new Set(closure.captures);
2906
+ const automaticCaptures = new Set(needed.filter((name) =>
2907
+ classScope && (name.startsWith("@") || name.startsWith("_")),
2908
+ ));
2861
2909
  const contextCaptures = new Set(
2862
2910
  closure.captures.filter((name) => availableContexts.has(name)),
2863
2911
  );
2864
2912
  for (const name of needed)
2865
- if (!captures.has(name))
2913
+ if (!captures.has(name) && !automaticCaptures.has(name))
2866
2914
  throw new LangoError(
2867
2915
  "LNG1210",
2868
2916
  closure.line,
2869
2917
  `MissingCapture: ${name}; add capture ${name}`,
2870
2918
  );
2871
- for (const name of captures) {
2919
+ for (const name of new Set([...captures, ...automaticCaptures])) {
2872
2920
  if (!needed.includes(name) && !contextCaptures.has(name))
2873
2921
  throw new LangoError(
2874
2922
  "LNG1211",
@@ -2884,6 +2932,11 @@ function validate(program, imports = []) {
2884
2932
  `capture ${name} is not assigned before this closure`,
2885
2933
  );
2886
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
+ }
2887
2940
  const staticMember = name.startsWith("@@");
2888
2941
  const property = name.startsWith("@")
2889
2942
  ? name.slice(staticMember ? 2 : 1)
@@ -3119,6 +3172,8 @@ function validate(program, imports = []) {
3119
3172
  program.functions.forEach((fn) => validateLoopLevels(fn.body));
3120
3173
  program.tests.forEach((test) => validateLoopLevels(test.body));
3121
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");
3122
3177
  if (classNode.ancestor && !classes.has(classNode.ancestor))
3123
3178
  throw new LangoError(
3124
3179
  "LNG1206",
@@ -3313,6 +3368,7 @@ function validate(program, imports = []) {
3313
3368
  );
3314
3369
  }
3315
3370
  if (name && /^@[A-Za-z_]\w*$/.test(name)) {
3371
+ if (name === "@this") return;
3316
3372
  if (!classScope.instanceMembers.has(name.slice(1)))
3317
3373
  throw new LangoError(
3318
3374
  "LNG1222",
@@ -3343,7 +3399,7 @@ function validate(program, imports = []) {
3343
3399
  : null;
3344
3400
  if (
3345
3401
  name &&
3346
- (/^@[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))
3347
3403
  )
3348
3404
  throw new LangoError(
3349
3405
  "LNG1231",
@@ -3420,6 +3476,12 @@ function validate(program, imports = []) {
3420
3476
 
3421
3477
  function emit(program, moduleImports = [], includeTests = false) {
3422
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
+ ]),
3423
3485
  print: false,
3424
3486
  translate: false,
3425
3487
  context: false,
@@ -3610,6 +3672,13 @@ function emitClass(item, flags) {
3610
3672
  method.public,
3611
3673
  ]),
3612
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
+ }
3613
3682
  const fields = [
3614
3683
  ...new Set(
3615
3684
  item.methods
@@ -3620,7 +3689,7 @@ function emitClass(item, flags) {
3620
3689
  .filter((name) => !name.startsWith("@@"))
3621
3690
  .map((name) => name.slice(1)),
3622
3691
  ),
3623
- ].filter((field) => !flags.publicProperties.has(field));
3692
+ ].filter((field) => field !== "this" && !flags.publicProperties.has(field));
3624
3693
  const implicitConstructor =
3625
3694
  item.ancestor &&
3626
3695
  !item.methods.some((method) => method.name === "constructor")
@@ -3700,7 +3769,8 @@ function emitFunction(fn, flags) {
3700
3769
  .map((node) => node.name),
3701
3770
  );
3702
3771
  const hoisted = new Set(
3703
- [...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)),
3704
3774
  );
3705
3775
  const declared = new Set([...fn.parameters.map((p) => p.name), ...hoisted]);
3706
3776
  const priorArrayBindings = flags.arrayBindings;
@@ -4019,6 +4089,7 @@ function emitBoundaryChecks(parameters, depth, flags) {
4019
4089
  .join("");
4020
4090
  }
4021
4091
  function emitReceiverName(name, flags) {
4092
+ if (/^@this(?:$|[.\[?])/.test(name)) return `this${name.slice(5)}`;
4022
4093
  const host = /^([A-Za-z_]\w*)(.*)$/.exec(name);
4023
4094
  if (host && flags.hostGlobals?.has(host[1])) {
4024
4095
  flags.hostGlobal = true;
@@ -4042,6 +4113,7 @@ function emitReceiverName(name, flags) {
4042
4113
  }
4043
4114
 
4044
4115
  function emitExpr(node, flags) {
4116
+ if (node.type === "raw" && node.value === "@this") return "this";
4045
4117
  const decimalRaw = (value) =>
4046
4118
  value?.type === "raw" &&
4047
4119
  (value.value.startsWith("$") || value.value.includes(".$"));
@@ -4148,7 +4220,8 @@ function emitExpr(node, flags) {
4148
4220
  (sourceReceiver === "this" && flags.staticMethod)
4149
4221
  ? "static"
4150
4222
  : "instance";
4151
- 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;
4152
4225
  const receiver =
4153
4226
  sourceReceiver === "this" && flags.staticMethod && visibility === false
4154
4227
  ? flags.className
@@ -4376,7 +4449,7 @@ function emitString(parts, flags) {
4376
4449
  function spaces(line) {
4377
4450
  return line.length - line.trimStart().length;
4378
4451
  }
4379
- function collectImports(source, inputPath, outputPath) {
4452
+ function collectImports(source, inputPath, outputPath, layout = null) {
4380
4453
  const lines = source.split(/\r?\n/);
4381
4454
  const items = [];
4382
4455
  for (let index = 0; index < lines.length; index += 1) {
@@ -4425,7 +4498,7 @@ function collectImports(source, inputPath, outputPath) {
4425
4498
  path.dirname(inputPath),
4426
4499
  `${match[1]}.lango`,
4427
4500
  );
4428
- const outputPathForImport = path.resolve(
4501
+ const outputPathForImport = layout?.get(sourcePath) ?? path.resolve(
4429
4502
  path.dirname(outputPath),
4430
4503
  `${match[1]}.mjs`,
4431
4504
  );
@@ -4434,12 +4507,38 @@ function collectImports(source, inputPath, outputPath) {
4434
4507
  selections,
4435
4508
  sourcePath,
4436
4509
  outputPath: outputPathForImport,
4437
- specifier: `./${path.basename(outputPathForImport)}`,
4510
+ specifier: relativeModuleSpecifier(outputPath, outputPathForImport),
4438
4511
  });
4439
4512
  }
4440
4513
  return items;
4441
4514
  }
4442
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
+
4443
4542
  async function validateModuleInitializerCycles(entryPath, entryOutputPath) {
4444
4543
  const modules = new Map();
4445
4544
  const visit = async (modulePath, outputPath) => {
@@ -4497,6 +4596,7 @@ async function validateModuleInitializerCycles(entryPath, entryOutputPath) {
4497
4596
  `${modulePath}: cyclic module initializer reads imported ${selection.kind} ${selection.localName}`,
4498
4597
  );
4499
4598
  }
4599
+ return modules;
4500
4600
  }
4501
4601
  function collectClassImportInterface(lines, getIndex, setIndex) {
4502
4602
  const api = {
@@ -4541,6 +4641,46 @@ function collectClassImportInterface(lines, getIndex, setIndex) {
4541
4641
  setIndex(index);
4542
4642
  return api;
4543
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
+
4544
4684
  function validateImportedClassInterface(
4545
4685
  selection,
4546
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
  }