@lango-project/lango-js 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/compiler.mjs +64 -17
- package/src/runtime.mjs +30 -0
package/package.json
CHANGED
package/src/compiler.mjs
CHANGED
|
@@ -283,6 +283,17 @@ class Parser {
|
|
|
283
283
|
this.blank();
|
|
284
284
|
continue;
|
|
285
285
|
}
|
|
286
|
+
const hostImport = /^import js global ([A-Za-z_]\w*(?:\/[A-Za-z_]\w*)*)(?: as ([A-Za-z_]\w*))?$/.exec(line.text);
|
|
287
|
+
if (hostImport) {
|
|
288
|
+
if (declarationSeen)
|
|
289
|
+
this.fail(line, "LNG1121", "imports must precede class and function declarations");
|
|
290
|
+
const name = hostImport[2] ?? hostImport[1].split("/").at(-1);
|
|
291
|
+
if (jsImports.some((item) => item.name === name))
|
|
292
|
+
this.fail(line, "LNG1122", `duplicate JavaScript import name ${name}`);
|
|
293
|
+
jsImports.push({ name, globalPath: hostImport[1].split("/").map(kebabName) });
|
|
294
|
+
this.blank();
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
286
297
|
const jsImport = /^import js "([^"]+)" as ([A-Za-z_]\w*)$/.exec(line.text);
|
|
287
298
|
if (jsImport) {
|
|
288
299
|
if (declarationSeen)
|
|
@@ -1074,8 +1085,12 @@ class Parser {
|
|
|
1074
1085
|
}
|
|
1075
1086
|
captures() {
|
|
1076
1087
|
if (!(this.peek() ?? "").trimStart().startsWith("capture ")) return [];
|
|
1077
|
-
const line = this.
|
|
1078
|
-
const
|
|
1088
|
+
const line = { text: this.peek(), number: this.i + 1 };
|
|
1089
|
+
const indent = spaces(line.text);
|
|
1090
|
+
const names = [];
|
|
1091
|
+
while (spaces(this.peek() ?? "") === indent && (this.peek() ?? "").trimStart().startsWith("capture ")) {
|
|
1092
|
+
names.push(...splitTopLevel(this.line().text.trim().slice(8)));
|
|
1093
|
+
}
|
|
1079
1094
|
if (
|
|
1080
1095
|
names.some(
|
|
1081
1096
|
(name) =>
|
|
@@ -1116,7 +1131,7 @@ class Parser {
|
|
|
1116
1131
|
value:
|
|
1117
1132
|
match[2] === "{"
|
|
1118
1133
|
? this.object(indent + 4)
|
|
1119
|
-
: expr(match[2], line, this),
|
|
1134
|
+
: expr(this.continuedStatement(match[2], line, indent + 4), line, this),
|
|
1120
1135
|
});
|
|
1121
1136
|
}
|
|
1122
1137
|
return { type: "object", fields };
|
|
@@ -1803,6 +1818,8 @@ function validate(program, imports = []) {
|
|
|
1803
1818
|
),
|
|
1804
1819
|
);
|
|
1805
1820
|
const jsImportNames = new Set(program.jsImports.map((item) => item.name));
|
|
1821
|
+
const jsModuleNames = new Set(program.jsImports.filter((item) => !item.globalPath).map((item) => item.name));
|
|
1822
|
+
const hostImportNames = new Set(program.jsImports.filter((item) => item.globalPath).map((item) => item.name));
|
|
1806
1823
|
const importedFunctions = new Map(
|
|
1807
1824
|
imports.flatMap((item) =>
|
|
1808
1825
|
(item.selections ?? [])
|
|
@@ -1836,7 +1853,7 @@ function validate(program, imports = []) {
|
|
|
1836
1853
|
["xml2obj", 1],
|
|
1837
1854
|
["start_trans", 0], ["commit_trans", 0], ["rollback_trans", 0], ["trans_state", 0],
|
|
1838
1855
|
]);
|
|
1839
|
-
const hostFunctions = new Set(["fetch"]);
|
|
1856
|
+
const hostFunctions = new Set(["fetch", ...hostImportNames]);
|
|
1840
1857
|
const isDecimalValue = (value) =>
|
|
1841
1858
|
value &&
|
|
1842
1859
|
(value.type === "decimal" ||
|
|
@@ -2260,6 +2277,18 @@ function validate(program, imports = []) {
|
|
|
2260
2277
|
findMethod(classNode, "constructor", false);
|
|
2261
2278
|
const walk = (value, localCallables, classNode = null, staticMethod = false) => {
|
|
2262
2279
|
if (!value || typeof value !== "object") return;
|
|
2280
|
+
if (value.type === "closure" || value.type === "closureAssign")
|
|
2281
|
+
localCallables = new Set([...localCallables, ...value.parameters.map((parameter) => parameter.name), ...dynamicBindingNames(value.body)]);
|
|
2282
|
+
const bindings = [
|
|
2283
|
+
...(["assign", "closureAssign", "increment", "contextSet", "contextDeclare", "globalDeclare"].includes(value.type) ? [value.name] : []),
|
|
2284
|
+
...(value.parameters ?? []).map((parameter) => parameter.name),
|
|
2285
|
+
...(value.type === "for" ? [value.item] : []),
|
|
2286
|
+
...(value.type === "forObject" ? [value.key, value.value] : []),
|
|
2287
|
+
...(value.catchName ? [value.catchName] : []),
|
|
2288
|
+
];
|
|
2289
|
+
const readonly = bindings.find((name) => hostImportNames.has(name));
|
|
2290
|
+
if (readonly)
|
|
2291
|
+
throw new LangoError("LNG1268", value.line ?? 1, `${readonly} is a readonly host import and cannot be rebound or shadowed`);
|
|
2263
2292
|
if (value.type === "binary" && ["+", "-", "*", "/", "%", "**", ">", "<", ">=", "<="].includes(value.operator)) {
|
|
2264
2293
|
const string = (item) => item.type === "string";
|
|
2265
2294
|
const number = (item) => item.type === "raw" && /^\d/.test(item.value);
|
|
@@ -2336,7 +2365,7 @@ function validate(program, imports = []) {
|
|
|
2336
2365
|
value.line,
|
|
2337
2366
|
`use / between JavaScript namespace segments in new ${value.name}(...)`,
|
|
2338
2367
|
);
|
|
2339
|
-
if (value.name.includes("/")) validateJavaScriptArguments(value, value.name);
|
|
2368
|
+
if (value.name.includes("/") || hostImportNames.has(value.name)) validateJavaScriptArguments(value, value.name);
|
|
2340
2369
|
const target = classes.get(value.name);
|
|
2341
2370
|
const constructor = target && findConstructor(target);
|
|
2342
2371
|
if (constructor) validateRequiredArguments(constructor, value, value.name);
|
|
@@ -2344,7 +2373,7 @@ function validate(program, imports = []) {
|
|
|
2344
2373
|
if (value.type === "methodCall" && typeof value.receiver === "string") {
|
|
2345
2374
|
if (
|
|
2346
2375
|
value.receiver.includes(".") &&
|
|
2347
|
-
|
|
2376
|
+
jsModuleNames.has(value.receiver.split(".")[0])
|
|
2348
2377
|
)
|
|
2349
2378
|
throw new LangoError(
|
|
2350
2379
|
"LNG1232",
|
|
@@ -2404,6 +2433,8 @@ function validate(program, imports = []) {
|
|
|
2404
2433
|
`${targetClass.name}${target.method.static ? "::" : ":"}${value.name}`,
|
|
2405
2434
|
);
|
|
2406
2435
|
}
|
|
2436
|
+
if (value.type === "methodCall" && hostImportNames.has(receiverRoot(value.receiver)))
|
|
2437
|
+
validateJavaScriptArguments(value, `${receiverRoot(value.receiver)}.${value.name}`);
|
|
2407
2438
|
if (value.type === "methodCall" && receiverRoot(value.receiver) === "args")
|
|
2408
2439
|
validateJavaScriptArguments(value, `args.${value.name}`);
|
|
2409
2440
|
if (value.type === "namespaceMethodCall") {
|
|
@@ -3401,6 +3432,7 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3401
3432
|
item.names.map((name) => name.split(" as ").at(-1)),
|
|
3402
3433
|
),
|
|
3403
3434
|
),
|
|
3435
|
+
hostGlobals: new Map(program.jsImports.filter((item) => item.globalPath).map((item) => [item.name, item.globalPath])),
|
|
3404
3436
|
};
|
|
3405
3437
|
const classes = program.classes
|
|
3406
3438
|
.map((item) => emitClass(item, flags))
|
|
@@ -3424,7 +3456,18 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3424
3456
|
.concat("...__langoOwnTests")
|
|
3425
3457
|
.join(", ")}];\n`
|
|
3426
3458
|
: "";
|
|
3459
|
+
const globals = program.globals
|
|
3460
|
+
.map((item) => {
|
|
3461
|
+
const value = emitExpr(item.value, flags);
|
|
3462
|
+
const prefix = item.exported ? "export let" : "let";
|
|
3463
|
+
if (!item.name.startsWith("$")) return `${prefix} ${item.name} = ${value};\n`;
|
|
3464
|
+
flags.decimal = true;
|
|
3465
|
+
return `${prefix} ${item.name} = __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value});\n`;
|
|
3466
|
+
})
|
|
3467
|
+
.join("");
|
|
3427
3468
|
const helpers = [
|
|
3469
|
+
flags.hostGlobal && "hostGlobal as __hostGlobal",
|
|
3470
|
+
flags.hostGlobalCall && "hostGlobalCall as __hostGlobalCall",
|
|
3428
3471
|
flags.decimal && "decimal as __decimal",
|
|
3429
3472
|
flags.print && "println as __println",
|
|
3430
3473
|
flags.translate && "translate as __translate",
|
|
@@ -3482,20 +3525,12 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3482
3525
|
)
|
|
3483
3526
|
.join("");
|
|
3484
3527
|
const jsModules = program.jsImports
|
|
3528
|
+
.filter((item) => !item.globalPath)
|
|
3485
3529
|
.map(
|
|
3486
3530
|
(item) =>
|
|
3487
3531
|
`import * as ${item.name} from ${JSON.stringify(item.specifier)};\n`,
|
|
3488
3532
|
)
|
|
3489
3533
|
.join("");
|
|
3490
|
-
const globals = program.globals
|
|
3491
|
-
.map((item) => {
|
|
3492
|
-
const value = emitExpr(item.value, flags);
|
|
3493
|
-
const prefix = item.exported ? "export let" : "let";
|
|
3494
|
-
if (!item.name.startsWith("$")) return `${prefix} ${item.name} = ${value};\n`;
|
|
3495
|
-
flags.decimal = true;
|
|
3496
|
-
return `${prefix} ${item.name} = __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value});\n`;
|
|
3497
|
-
})
|
|
3498
|
-
.join("");
|
|
3499
3534
|
const testReset = includeTests
|
|
3500
3535
|
? `export function __langoResetTests() {\n${program.globals
|
|
3501
3536
|
.map((item) => ` ${item.name} = ${emitExpr(item.value, flags)};\n`)
|
|
@@ -3984,6 +4019,11 @@ function emitBoundaryChecks(parameters, depth, flags) {
|
|
|
3984
4019
|
.join("");
|
|
3985
4020
|
}
|
|
3986
4021
|
function emitReceiverName(name, flags) {
|
|
4022
|
+
const host = /^([A-Za-z_]\w*)(.*)$/.exec(name);
|
|
4023
|
+
if (host && flags.hostGlobals?.has(host[1])) {
|
|
4024
|
+
flags.hostGlobal = true;
|
|
4025
|
+
return `__hostGlobal(${JSON.stringify(flags.hostGlobals.get(host[1]))})${emitKebabPath(host[2])}`;
|
|
4026
|
+
}
|
|
3987
4027
|
const member = /^(@{1,2})(\$?[A-Za-z_]\w*)(.*)$/.exec(name);
|
|
3988
4028
|
if (member) {
|
|
3989
4029
|
const [, scope, property, suffix] = member;
|
|
@@ -4014,6 +4054,13 @@ function emitExpr(node, flags) {
|
|
|
4014
4054
|
flags.normalize = true;
|
|
4015
4055
|
return `__normalize(${value})`;
|
|
4016
4056
|
};
|
|
4057
|
+
if (node.type === "raw" && flags.hostGlobals.has(node.value.split(/[.\[?]/)[0]))
|
|
4058
|
+
return normalize(emitReceiverName(node.value, flags));
|
|
4059
|
+
if (node.type === "call" && flags.hostGlobals.has(node.name)) {
|
|
4060
|
+
flags.hostGlobalCall = true;
|
|
4061
|
+
const arguments_ = node.arguments.map((item) => item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags)).join(", ");
|
|
4062
|
+
return `__hostGlobalCall(${JSON.stringify(flags.hostGlobals.get(node.name))}${arguments_ ? `, ${arguments_}` : ""})`;
|
|
4063
|
+
}
|
|
4017
4064
|
if (node.type === "raw")
|
|
4018
4065
|
if (/^@{1,2}\$?[A-Za-z_]\w*[.\[?]/.test(node.value))
|
|
4019
4066
|
return normalize(emitReceiverName(node.value, flags));
|
|
@@ -4076,7 +4123,7 @@ function emitExpr(node, flags) {
|
|
|
4076
4123
|
return `__finite(-${numericLiteral ? emitExpr(node.value, flags) : `__requireNumber(${emitExpr(node.value, flags)})`})`;
|
|
4077
4124
|
}
|
|
4078
4125
|
if (node.type === "new")
|
|
4079
|
-
return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
|
|
4126
|
+
return `new ${flags.hostGlobals.has(node.name.split("/")[0]) ? `(${emitReceiverName(node.name.replaceAll("/", "."), flags)})` : node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
|
|
4080
4127
|
item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
|
|
4081
4128
|
).join(", ")})`;
|
|
4082
4129
|
if (node.type === "superMethodCall")
|
|
@@ -4092,7 +4139,7 @@ function emitExpr(node, flags) {
|
|
|
4092
4139
|
if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
|
|
4093
4140
|
const namespace = node.type === "namespaceMethodCall";
|
|
4094
4141
|
const sourceReceiver = namespace
|
|
4095
|
-
? node.receiver.replaceAll("/", ".")
|
|
4142
|
+
? emitReceiverName(node.receiver.replaceAll("/", "."), flags)
|
|
4096
4143
|
: typeof node.receiver === "string"
|
|
4097
4144
|
? emitReceiverName(node.receiver, flags)
|
|
4098
4145
|
: emitExpr(node.receiver, flags);
|
package/src/runtime.mjs
CHANGED
|
@@ -548,6 +548,36 @@ export function call(value, ...arguments_) {
|
|
|
548
548
|
return normalize(value(...arguments_));
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
export class HostGlobalUnavailable extends Error {
|
|
552
|
+
constructor(path) {
|
|
553
|
+
super("HostGlobalUnavailable: " + path.join("/"));
|
|
554
|
+
this.name = "HostGlobalUnavailable";
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function resolveHostGlobal(path) {
|
|
559
|
+
let owner = globalThis;
|
|
560
|
+
let value = owner;
|
|
561
|
+
for (const segment of path) {
|
|
562
|
+
owner = value;
|
|
563
|
+
if (owner === null || owner === undefined || !(segment in Object(owner)))
|
|
564
|
+
throw new HostGlobalUnavailable(path);
|
|
565
|
+
value = owner[segment];
|
|
566
|
+
}
|
|
567
|
+
if (value === undefined) throw new HostGlobalUnavailable(path);
|
|
568
|
+
return { owner, value };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function hostGlobal(path) {
|
|
572
|
+
return resolveHostGlobal(path).value;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function hostGlobalCall(path, ...arguments_) {
|
|
576
|
+
const { owner, value } = resolveHostGlobal(path);
|
|
577
|
+
if (typeof value !== "function") throw new Error("NotCallable");
|
|
578
|
+
return normalize(Reflect.apply(value, owner, arguments_));
|
|
579
|
+
}
|
|
580
|
+
|
|
551
581
|
export function callMethod(receiver, name, ...arguments_) {
|
|
552
582
|
const value = receiver?.[name];
|
|
553
583
|
if (typeof value !== "function") throw new Error("NotCallable");
|