@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 +57 -3
- package/package.json +1 -1
- package/src/cli.mjs +1 -1
- package/src/compiler.mjs +164 -24
- package/src/runtime.mjs +14 -10
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
|
|
236
|
-
|
|
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
|
|
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
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 (
|
|
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
|
-
|
|
84
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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.
|
|
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) =>
|
|
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
|
|
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:
|
|
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,
|
|
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(
|
|
439
|
+
return normalize(value.remove(...items));
|
|
439
440
|
}
|
|
440
441
|
throw new Error("NotRemovable");
|
|
441
442
|
}
|
|
442
443
|
|
|
443
|
-
export function remove_all(value,
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
495
|
+
return normalize(value.insert_at(...items));
|
|
492
496
|
}
|
|
493
497
|
throw new Error("NotInsertable");
|
|
494
498
|
}
|