@lango-project/lango-js 0.1.2 → 0.1.3

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/NOTICE CHANGED
@@ -1,2 +1,7 @@
1
1
  Lango
2
2
  Copyright 2026 Savas Alparslan
3
+
4
+ This product includes @xmldom/xmldom 0.9.12.
5
+ Copyright 2019-present Christopher J. Brody and other contributors.
6
+ Copyright 2012-2017 @jindw <jindw@xidea.org> and other contributors.
7
+ Licensed under the MIT License; see src/vendor/xmldom.LICENSE.md.
package/README.md CHANGED
@@ -77,7 +77,9 @@ test:lango -- input.lango` when Lango test blocks need host services.
77
77
 
78
78
  `buildFile()` writes a matched private runtime next to the generated module.
79
79
  Before calling exported Lango entry points, import that runtime and configure
80
- it. `configure()` accepts an object with optional `output`, `catalog`, and
80
+ it. Runtimes generated into separate packages share dynamic Lango contexts
81
+ within one JavaScript process, so an application host can provide a context to
82
+ an exported function in a compiled dependency. `configure()` accepts an object with optional `output`, `catalog`, and
81
83
  `database_adapter` values; it returns `null`, resets dynamic context and
82
84
  transaction-failure state, and rejects an invalid configuration with
83
85
  `RuntimeConfigurationError`. `output` is a function (or `null`), `catalog` is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lango-project/lango-js",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/cli.mjs CHANGED
@@ -15,8 +15,13 @@ const displayError = (error) => error instanceof LangoError
15
15
  const buildOptions = (arguments_) => {
16
16
  if (arguments_[0] !== "--out" || !arguments_[1]) return null;
17
17
  const profile = {};
18
- for (let index = 2; index < arguments_.length; index += 2) {
18
+ for (let index = 2; index < arguments_.length;) {
19
19
  const option = arguments_[index];
20
+ if (option === "--executable") {
21
+ profile.executable = true;
22
+ index += 1;
23
+ continue;
24
+ }
20
25
  const value = arguments_[index + 1];
21
26
  if (!value) return null;
22
27
  if (option === "--decimal-division-scale")
@@ -24,6 +29,7 @@ const buildOptions = (arguments_) => {
24
29
  else if (option === "--decimal-division-rounding")
25
30
  profile.decimal_division_rounding = Decimal.Rounding[value] ?? value;
26
31
  else return null;
32
+ index += 2;
27
33
  }
28
34
  return { outputPath: arguments_[1], profile };
29
35
  };
@@ -167,6 +173,6 @@ if (command === "init" && inputPath && rest.length === 0) {
167
173
  process.exitCode = error instanceof LangoError ? 1 : 2;
168
174
  }
169
175
  } else {
170
- console.error("Usage: node src/cli.mjs init <directory>\n node src/cli.mjs build <input.lango> --out <output.mjs> [--decimal-division-scale <n>] [--decimal-division-rounding <EnumName>]\n node src/cli.mjs run <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>] [-- args...]\n node src/cli.mjs test <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>]\n node src/cli.mjs format <input.lango>");
176
+ console.error("Usage: node src/cli.mjs init <directory>\n node src/cli.mjs build <input.lango> --out <output.mjs> [--executable] [--decimal-division-scale <n>] [--decimal-division-rounding <EnumName>]\n node src/cli.mjs run <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>] [-- args...]\n node src/cli.mjs test <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>]\n node src/cli.mjs format <input.lango>");
171
177
  process.exitCode = 2;
172
178
  }
package/src/compiler.mjs CHANGED
@@ -32,7 +32,8 @@ export function formatDiagnostics(error) {
32
32
 
33
33
  export async function buildFile(inputPath, outputPath, seen = new Set(), includeTests = false, profile = {}) {
34
34
  const decimalProfile = validateDecimalProfile(profile);
35
- if (seen.size === 0)
35
+ const isEntryPoint = seen.size === 0;
36
+ if (isEntryPoint)
36
37
  await validateModuleInitializerCycles(inputPath, outputPath);
37
38
  if (seen.has(inputPath)) return;
38
39
  seen.add(inputPath);
@@ -86,10 +87,19 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
86
87
  }
87
88
 
88
89
  const compiled = compile(source, inputPath, imports, includeTests, decimalProfile);
89
- const output = compiled.javascript.replace(
90
+ let output = compiled.javascript.replace(
90
91
  "//# sourceMappingURL=main.mjs.map",
91
92
  `//# sourceMappingURL=${path.basename(outputPath)}.map`,
92
93
  );
94
+ if (isEntryPoint && profile.executable) {
95
+ const main = compiled.program.functions.find((item) => item.name === "main" && item.exported);
96
+ if (!main)
97
+ throw new LangoError("LNG1265", 1, "an executable build requires export main(args)", inputPath);
98
+ output = output.replace(
99
+ `//# sourceMappingURL=${path.basename(outputPath)}.map`,
100
+ `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`,
101
+ );
102
+ }
93
103
  const directory = path.dirname(outputPath);
94
104
 
95
105
  await mkdir(path.join(directory, ".lango"), { recursive: true });
@@ -111,6 +121,19 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
111
121
  await readFile(new URL("./vendor/bignumber.mjs", import.meta.url), "utf8"),
112
122
  "utf8",
113
123
  );
124
+ const xmlVendor = new URL("./vendor/xmldom/", import.meta.url);
125
+ await mkdir(path.join(directory, ".lango/xmldom"), { recursive: true });
126
+ for (const name of ["conventions.js", "dom-parser.js", "dom.js", "entities.js", "errors.js", "grammar.js", "index.js", "sax.js"])
127
+ await writeFile(
128
+ path.join(directory, ".lango/xmldom", name),
129
+ await readFile(new URL(name, xmlVendor), "utf8"),
130
+ "utf8",
131
+ );
132
+ await writeFile(
133
+ path.join(directory, ".lango/xmldom.LICENSE.md"),
134
+ await readFile(new URL("./vendor/xmldom.LICENSE.md", import.meta.url), "utf8"),
135
+ "utf8",
136
+ );
114
137
  }
115
138
 
116
139
  export function compile(source, sourceName = "<source>", imports = [], includeTests = false, profile = {}) {
@@ -243,7 +266,6 @@ class Parser {
243
266
  const tests = [];
244
267
  const jsImports = [];
245
268
  let declarationSeen = false;
246
- let exportedClassSeen = false;
247
269
  this.blank();
248
270
  while (this.i < this.lines.length) {
249
271
  const line = this.line();
@@ -293,9 +315,6 @@ class Parser {
293
315
  );
294
316
  if (classHeader) {
295
317
  declarationSeen = true;
296
- if (classHeader[1] && exportedClassSeen)
297
- this.fail(line, "LNG1114", "a file may export one class");
298
- exportedClassSeen ||= Boolean(classHeader[1]);
299
318
  const classNode = {
300
319
  name: classHeader[2],
301
320
  ancestor: classHeader[3] ?? null,
@@ -777,7 +796,7 @@ class Parser {
777
796
  value: { type: "new", name: constructor, arguments: [closure], line: line.number },
778
797
  });
779
798
  }
780
- else if (/^(.+)\.([A-Za-z_]\w*)\((.*), \(([^)]*)\)$/.test(text)) {
799
+ else if (!text.includes(" = ") && /^(.+)\.([A-Za-z_]\w*)\((.*), \(([^)]*)\)$/.test(text)) {
781
800
  const [, receiver, name, argumentsText, parameters] =
782
801
  /^(.+)\.([A-Za-z_]\w*)\((.*), \(([^)]*)\)$/.exec(text);
783
802
  const closure = {
@@ -803,7 +822,34 @@ class Parser {
803
822
  },
804
823
  });
805
824
  }
806
- else if (/^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.test(text)) {
825
+ else if (/^(\$?[A-Za-z_]\w*) = (.+)\.([A-Za-z_]\w*)\((?:(.*), )?\(([^)]*)\)$/.test(text)) {
826
+ const [, name, receiver, method, argumentsText, parameters] =
827
+ /^(\$?[A-Za-z_]\w*) = (.+)\.([A-Za-z_]\w*)\((?:(.*), )?\(([^)]*)\)$/.exec(text);
828
+ const closure = {
829
+ type: "closure",
830
+ parameters: this.parameters(parameters, line),
831
+ captures: this.captures(),
832
+ line: line.number,
833
+ body: this.block(indent + 4),
834
+ };
835
+ this.blank();
836
+ if (this.peek() !== `${" ".repeat(indent)})`)
837
+ this.fail(line, "LNG1266", "assigned method callback requires a closing parenthesis");
838
+ this.i += 1;
839
+ body.push({
840
+ type: "assign",
841
+ name,
842
+ value: {
843
+ type: "methodCall",
844
+ receiver,
845
+ optional: false,
846
+ name: method,
847
+ arguments: [...(argumentsText ? splitTopLevel(argumentsText).map((item) => expr(item, line, this)) : []), closure],
848
+ line: line.number,
849
+ },
850
+ });
851
+ }
852
+ else if (!text.includes(" = ") && /^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.test(text)) {
807
853
  const [, first, name, parameters] = /^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.exec(text);
808
854
  const steps = [];
809
855
  let step = { name, parameters: this.parameters(parameters, line), captures: this.captures(), line: line.number, body: this.block(indent + 4) };
@@ -959,7 +1005,7 @@ class Parser {
959
1005
  operator: suffix === "++" ? "+" : "-",
960
1006
  });
961
1007
  } else {
962
- const assignment = /^(\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*\.\$?[A-Za-z_]\w*) = (.+)$/.exec(text);
1008
+ const assignment = /^(\$[A-Za-z_]\w*|@{0,2}\$?[A-Za-z_]\w*|[A-Za-z_]\w*(?:\.\$?[A-Za-z_]\w*)+) = (.+)$/.exec(text);
963
1009
  if (!assignment)
964
1010
  this.fail(line, "LNG1106", `unsupported Faz 2 statement: ${text}`);
965
1011
  body.push({
@@ -1288,10 +1334,18 @@ function expr(source, line, parser) {
1288
1334
  }),
1289
1335
  };
1290
1336
  }
1337
+ if (text.startsWith("not "))
1338
+ return {
1339
+ type: "unary",
1340
+ operator: "not",
1341
+ value: expr(text.slice(4), line, parser),
1342
+ };
1291
1343
  for (const operators of [
1292
1344
  ["??"],
1293
- ["or", "||"],
1294
- ["and", "&&"],
1345
+ ["or"],
1346
+ ["and"],
1347
+ ["||"],
1348
+ ["&&"],
1295
1349
  ["==", "!="],
1296
1350
  [">=", "<=", ">", "<", "not in", "in"],
1297
1351
  ["+", "-"],
@@ -1314,12 +1368,6 @@ function expr(source, line, parser) {
1314
1368
  left: expr(text.slice(0, exponent.index), line, parser),
1315
1369
  right: expr(text.slice(exponent.index + exponent.operator.length + 2), line, parser),
1316
1370
  };
1317
- if (text.startsWith("not "))
1318
- return {
1319
- type: "unary",
1320
- operator: "not",
1321
- value: expr(text.slice(4), line, parser),
1322
- };
1323
1371
  if (/^-[^\s]/.test(text))
1324
1372
  return {
1325
1373
  type: "unary",
@@ -1455,6 +1503,17 @@ function expr(source, line, parser) {
1455
1503
  : [],
1456
1504
  line: line.number,
1457
1505
  };
1506
+ const indexedCall = /^(.+)\[(.+)\]\((.*)\)$/.exec(text);
1507
+ if (indexedCall)
1508
+ return {
1509
+ type: "indexedCall",
1510
+ receiver: expr(indexedCall[1], line, parser),
1511
+ index: expr(indexedCall[2], line, parser),
1512
+ arguments: indexedCall[3]
1513
+ ? splitTopLevel(indexedCall[3]).map((item) => expr(item, line, parser))
1514
+ : [],
1515
+ line: line.number,
1516
+ };
1458
1517
  const methodCall =
1459
1518
  /^([A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w*)*?)(\?\.|\.)([A-Za-z_]\w*)\((.*)\)$/.exec(
1460
1519
  text,
@@ -1760,6 +1819,8 @@ function validate(program, imports = []) {
1760
1819
  ["is_array", 1],
1761
1820
  ["range", [1, 2, 3]],
1762
1821
  ["to_js_number", 1],
1822
+ ["import_module", 1],
1823
+ ["xml2obj", 1],
1763
1824
  ["start_trans", 0], ["commit_trans", 0], ["rollback_trans", 0], ["trans_state", 0],
1764
1825
  ]);
1765
1826
  const hostFunctions = new Set(["fetch"]);
@@ -2508,6 +2569,15 @@ function validate(program, imports = []) {
2508
2569
  return names;
2509
2570
  };
2510
2571
 
2572
+ const contextBindings = (body, names = new Set()) => {
2573
+ for (const node of body) {
2574
+ if (node.type === "contextDeclare" || node.type === "contextSet") names.add(node.name);
2575
+ if (node.type !== "closureAssign")
2576
+ nestedBodies(node).forEach((child) => contextBindings(child, names));
2577
+ }
2578
+ return names;
2579
+ };
2580
+
2511
2581
  const globalDeclarations = (body, names = new Set()) => {
2512
2582
  for (const node of body) {
2513
2583
  if (node.type === "globalDeclare") names.add(node.name);
@@ -2563,9 +2633,14 @@ function validate(program, imports = []) {
2563
2633
  outerBindings,
2564
2634
  classScope = null,
2565
2635
  inheritedExpiredCatchBindings = new Set(),
2636
+ outerContextBindings = new Set(),
2566
2637
  ) => {
2567
2638
  const available = new Set(outerBindings);
2568
2639
  const expiredCatchBindings = new Set(inheritedExpiredCatchBindings);
2640
+ const availableContexts = new Set([
2641
+ ...outerContextBindings,
2642
+ ...contextBindings(body),
2643
+ ]);
2569
2644
  for (const node of body) {
2570
2645
  const references = new Set();
2571
2646
  [node.value, node.condition, node.collection, node.arguments, node.tests].forEach((value) => {
@@ -2585,6 +2660,7 @@ function validate(program, imports = []) {
2585
2660
  new Set(available),
2586
2661
  classScope,
2587
2662
  expiredCatchBindings,
2663
+ availableContexts,
2588
2664
  );
2589
2665
  const right = node.elseBody
2590
2666
  ? validateClosures(
@@ -2592,6 +2668,7 @@ function validate(program, imports = []) {
2592
2668
  new Set(available),
2593
2669
  classScope,
2594
2670
  expiredCatchBindings,
2671
+ availableContexts,
2595
2672
  )
2596
2673
  : available;
2597
2674
  const exits = [];
@@ -2610,6 +2687,7 @@ function validate(program, imports = []) {
2610
2687
  new Set(available),
2611
2688
  classScope,
2612
2689
  expiredCatchBindings,
2690
+ availableContexts,
2613
2691
  ),
2614
2692
  );
2615
2693
  if (!node.elseBody || reachesEnd(node.elseBody))
@@ -2620,6 +2698,7 @@ function validate(program, imports = []) {
2620
2698
  new Set(available),
2621
2699
  classScope,
2622
2700
  expiredCatchBindings,
2701
+ availableContexts,
2623
2702
  )
2624
2703
  : new Set(available),
2625
2704
  );
@@ -2635,6 +2714,7 @@ function validate(program, imports = []) {
2635
2714
  new Set(available),
2636
2715
  classScope,
2637
2716
  expiredCatchBindings,
2717
+ availableContexts,
2638
2718
  );
2639
2719
  if (reachesEnd(node.tryBody)) exits.push(tryExit);
2640
2720
  if (node.catchBody) {
@@ -2645,6 +2725,7 @@ function validate(program, imports = []) {
2645
2725
  new Set(
2646
2726
  [...expiredCatchBindings].filter((name) => name !== node.catchName),
2647
2727
  ),
2728
+ availableContexts,
2648
2729
  );
2649
2730
  if (reachesEnd(node.catchBody)) exits.push(catchExit);
2650
2731
  }
@@ -2658,6 +2739,7 @@ function validate(program, imports = []) {
2658
2739
  certain,
2659
2740
  classScope,
2660
2741
  new Set([...expiredCatchBindings, node.catchName].filter(Boolean)),
2742
+ availableContexts,
2661
2743
  )
2662
2744
  : certain;
2663
2745
  if (!node.finallyBody || reachesEnd(node.finallyBody))
@@ -2671,6 +2753,7 @@ function validate(program, imports = []) {
2671
2753
  new Set([...available, node.item]),
2672
2754
  classScope,
2673
2755
  expiredCatchBindings,
2756
+ availableContexts,
2674
2757
  );
2675
2758
  continue;
2676
2759
  }
@@ -2680,6 +2763,7 @@ function validate(program, imports = []) {
2680
2763
  new Set([...available, node.key, node.value]),
2681
2764
  classScope,
2682
2765
  expiredCatchBindings,
2766
+ availableContexts,
2683
2767
  );
2684
2768
  continue;
2685
2769
  }
@@ -2695,6 +2779,7 @@ function validate(program, imports = []) {
2695
2779
  new Set(available),
2696
2780
  classScope,
2697
2781
  expiredCatchBindings,
2782
+ availableContexts,
2698
2783
  );
2699
2784
  continue;
2700
2785
  }
@@ -2726,9 +2811,14 @@ function validate(program, imports = []) {
2726
2811
  (name) =>
2727
2812
  !localBindings.has(name) &&
2728
2813
  !contextNames.has(name) &&
2729
- !globalNames.has(name),
2814
+ !globalNames.has(name) &&
2815
+ !standardFunctions.has(name) &&
2816
+ !hostFunctions.has(name),
2730
2817
  );
2731
2818
  const captures = new Set(closure.captures);
2819
+ const contextCaptures = new Set(
2820
+ closure.captures.filter((name) => availableContexts.has(name)),
2821
+ );
2732
2822
  for (const name of needed)
2733
2823
  if (!captures.has(name))
2734
2824
  throw new LangoError(
@@ -2737,7 +2827,7 @@ function validate(program, imports = []) {
2737
2827
  `MissingCapture: ${name}; add capture ${name}`,
2738
2828
  );
2739
2829
  for (const name of captures) {
2740
- if (!needed.includes(name))
2830
+ if (!needed.includes(name) && !contextCaptures.has(name))
2741
2831
  throw new LangoError(
2742
2832
  "LNG1211",
2743
2833
  closure.line,
@@ -2745,7 +2835,7 @@ function validate(program, imports = []) {
2745
2835
  );
2746
2836
  const protectedMember = Boolean(classScope && name.startsWith("_"));
2747
2837
  const propertyCapture = name.startsWith("@") || protectedMember;
2748
- if (!propertyCapture && !available.has(name))
2838
+ if (!propertyCapture && !contextCaptures.has(name) && !available.has(name))
2749
2839
  throw new LangoError(
2750
2840
  "LNG1212",
2751
2841
  closure.line,
@@ -2778,6 +2868,7 @@ function validate(program, imports = []) {
2778
2868
  new Set([...available, ...localBindings]),
2779
2869
  classScope,
2780
2870
  expiredCatchBindings,
2871
+ new Set([...availableContexts, ...contextCaptures]),
2781
2872
  );
2782
2873
  if (node.type === "closureAssign" || node.type === "assign") available.add(node.name);
2783
2874
  continue;
@@ -3327,6 +3418,7 @@ function emit(program, moduleImports = [], includeTests = false) {
3327
3418
  flags.print && "println as __println",
3328
3419
  flags.translate && "translate as __translate",
3329
3420
  flags.xml && "xmlEscape as __xmlEscape",
3421
+ flags.xmlObject && "xml2obj as __xml2obj",
3330
3422
  flags.context && "contextGet as __contextGet",
3331
3423
  flags.context && "contextSet as __contextSet",
3332
3424
  flags.context && "contextRestore as __contextRestore",
@@ -3356,6 +3448,7 @@ function emit(program, moduleImports = [], includeTests = false) {
3356
3448
  flags.finite && "finite as __finite",
3357
3449
  flags.number && "requireNumber as __requireNumber",
3358
3450
  flags.call && "call as __call",
3451
+ flags.callMethod && "callMethod as __callMethod",
3359
3452
  flags.boundary && "boundary as __boundary",
3360
3453
  flags.database && "startTrans as __startTrans",
3361
3454
  flags.database && "commitTrans as __commitTrans",
@@ -3662,7 +3755,7 @@ function emitStatement(node, depth, declared, flags) {
3662
3755
  if (node.type === "assign") {
3663
3756
  const decimalBinding = node.name.startsWith("$") || /^@{1,2}\$/.test(node.name);
3664
3757
  const property = node.name.slice(node.name.startsWith("@@") ? 2 : 1);
3665
- const target = /^[A-Za-z_]\w*\.\$?[A-Za-z_]\w*$/.test(node.name)
3758
+ const target = /^[A-Za-z_]\w*(?:\.\$?[A-Za-z_]\w*)+$/.test(node.name)
3666
3759
  ? node.name
3667
3760
  : node.name.startsWith("@@")
3668
3761
  ? `${flags.className}.${property}`
@@ -3674,7 +3767,7 @@ function emitStatement(node, depth, declared, flags) {
3674
3767
  ? `this.${node.name}`
3675
3768
  : node.name;
3676
3769
  const prefix =
3677
- /^[A-Za-z_]\w*\.\$?[A-Za-z_]\w*$/.test(node.name)
3770
+ /^[A-Za-z_]\w*(?:\.\$?[A-Za-z_]\w*)+$/.test(node.name)
3678
3771
  ? ""
3679
3772
  : node.name.startsWith("@") ||
3680
3773
  (flags.className && node.name.startsWith("_")) ||
@@ -3808,7 +3901,11 @@ function emitClosure(node, depth, flags) {
3808
3901
  const priorContextNames = flags.contextNames;
3809
3902
  const priorGlobalNames = flags.globalNames;
3810
3903
  const priorArrayBindings = flags.arrayBindings;
3811
- const contextNames = collectContextNames(node.body);
3904
+ const contextCaptures = node.captures.filter((name) => priorContextNames?.has(name));
3905
+ const contextNames = new Set([
3906
+ ...collectContextNames(node.body),
3907
+ ...contextCaptures,
3908
+ ]);
3812
3909
  flags.contextNames = contextNames;
3813
3910
  flags.globalNames = collectGlobalNames(node.body);
3814
3911
  flags.arrayBindings = new Set();
@@ -3836,7 +3933,16 @@ function emitClosure(node, depth, flags) {
3836
3933
  if (!contextNames.size)
3837
3934
  return wrap(`(${parameters}) => {\n${boundary}${body}${pad} return null;\n${pad}}`);
3838
3935
  flags.context = true;
3839
- return wrap(`(${parameters}) => {\n${pad} const __contextScope = [];\n${pad} try {\n${boundary}${body}${pad} return null;\n${pad} } finally {\n${pad} __contextRestore(__contextScope);\n${pad} }\n${pad}}`);
3936
+ const capturedParameters = contextCaptures.map((_, index) => `__capturedContext${index}`);
3937
+ const capturedValues = contextCaptures.map((name) => `__contextGet(${JSON.stringify(name)})`);
3938
+ const capturedSetup = contextCaptures
3939
+ .map((name, index) => `${pad} __contextScope.push(__contextSet(${JSON.stringify(name)}, __capturedContext${index}));\n`)
3940
+ .join("");
3941
+ const callback = `(${parameters}) => {\n${pad} const __contextScope = [];\n${pad} try {\n${capturedSetup}${boundary}${body}${pad} return null;\n${pad} } finally {\n${pad} __contextRestore(__contextScope);\n${pad} }\n${pad}}`;
3942
+ const wrappedCallback = contextCaptures.length
3943
+ ? `((${capturedParameters.join(", ")}) => ${callback})(${capturedValues.join(", ")})`
3944
+ : callback;
3945
+ return wrap(wrappedCallback);
3840
3946
  }
3841
3947
  function emitArityGuard(parameters, actual, name, depth, flags) {
3842
3948
  const positions = parameters
@@ -3860,6 +3966,12 @@ function emitBoundaryChecks(parameters, depth, flags) {
3860
3966
  })
3861
3967
  .join("");
3862
3968
  }
3969
+ function emitReceiverName(name, flags) {
3970
+ if (!flags.contextNames?.has(name)) return name;
3971
+ flags.context = true;
3972
+ return `__contextGet(${JSON.stringify(name)})`;
3973
+ }
3974
+
3863
3975
  function emitExpr(node, flags) {
3864
3976
  const decimalRaw = (value) =>
3865
3977
  value?.type === "raw" &&
@@ -3935,7 +4047,7 @@ function emitExpr(node, flags) {
3935
4047
  return `super.${node.name}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3936
4048
  if (node.type === "index")
3937
4049
  return normalize(
3938
- `${typeof node.receiver === "string" ? node.receiver : emitExpr(node.receiver, flags)}${node.optional ? "?." : ""}[${emitExpr(node.index, flags)}]`,
4050
+ `${typeof node.receiver === "string" ? emitReceiverName(node.receiver, flags) : emitExpr(node.receiver, flags)}${node.optional ? "?." : ""}[${emitExpr(node.index, flags)}]`,
3939
4051
  );
3940
4052
  if (node.type === "property")
3941
4053
  return normalize(
@@ -3946,7 +4058,7 @@ function emitExpr(node, flags) {
3946
4058
  const sourceReceiver = namespace
3947
4059
  ? node.receiver.replaceAll("/", ".")
3948
4060
  : typeof node.receiver === "string"
3949
- ? node.receiver
4061
+ ? emitReceiverName(node.receiver, flags)
3950
4062
  : emitExpr(node.receiver, flags);
3951
4063
  const type =
3952
4064
  sourceReceiver === flags.className ||
@@ -4089,6 +4201,14 @@ function emitExpr(node, flags) {
4089
4201
  flags.toJsNumber = true;
4090
4202
  return `__toJsNumber(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
4091
4203
  }
4204
+ if (node.type === "call")
4205
+ if (node.name === "import_module")
4206
+ return `import(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
4207
+ if (node.type === "call")
4208
+ if (node.name === "xml2obj") {
4209
+ flags.xmlObject = true;
4210
+ return `__xml2obj(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
4211
+ }
4092
4212
  if (node.type === "call")
4093
4213
  if (node.name === "from_json" || node.name === "to_json") {
4094
4214
  flags.json = true;
@@ -4128,6 +4248,10 @@ function emitExpr(node, flags) {
4128
4248
  return `__call(${node.name}, ${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
4129
4249
  } else
4130
4250
  return `${node.name}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
4251
+ if (node.type === "indexedCall") {
4252
+ flags.callMethod = true;
4253
+ return `__callMethod(${emitExpr(node.receiver, flags)}, ${emitExpr(node.index, flags)}, ${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
4254
+ }
4131
4255
  if (node.type === "string") return emitString(node.literal, flags);
4132
4256
  if (node.type === "xml") {
4133
4257
  flags.xml = true;
package/src/runtime.mjs CHANGED
@@ -22,6 +22,9 @@ export function runtimeSource(profile = {}) {
22
22
  if (!(rounding in ROUNDING_MODES))
23
23
  throw new Error(`unknown decimal_division_rounding ${rounding}`);
24
24
  return `import BigNumber from "./bignumber.mjs";
25
+ import { createRequire } from "node:module";
26
+ const require = createRequire(import.meta.url);
27
+ const { DOMParser } = require("./xmldom/index.js");
25
28
 
26
29
  const Decimal = BigNumber.clone({
27
30
  DECIMAL_PLACES: ${scale},
@@ -56,7 +59,9 @@ let output = null;
56
59
  let database = null;
57
60
  let failedTransaction = false;
58
61
  export const DB = Object.freeze({ Active: "active", RollbackOnly: "rollback_only", Failed: "failed" });
59
- const contexts = new Map();
62
+ const contextStore = Symbol.for("lango.contexts");
63
+ const contexts = globalThis[contextStore] ?? new Map();
64
+ globalThis[contextStore] = contexts;
60
65
 
61
66
  export class RuntimeConfigurationError extends Error {
62
67
  constructor(message) {
@@ -188,6 +193,73 @@ export function xmlEscape(value) {
188
193
  .replaceAll("'", "&apos;");
189
194
  }
190
195
 
196
+ export function xml2obj(xml) {
197
+ if (typeof xml !== "string") throw new Error("XmlParseError: XML input must be a string");
198
+ const errors = [];
199
+ let document;
200
+ try {
201
+ document = new DOMParser({
202
+ onError(level, message) {
203
+ if (level !== "warning") errors.push(message);
204
+ },
205
+ }).parseFromString(xml, "application/xml");
206
+ } catch (error) {
207
+ throw new Error(\`XmlParseError: \${error.message}\`);
208
+ }
209
+ if (errors.length) throw new Error(\`XmlParseError: \${errors[0]}\`);
210
+ if (!document.documentElement)
211
+ throw new Error("XmlParseError: XML must contain one root tag");
212
+ return new Tag(document.documentElement);
213
+ }
214
+
215
+ export class Tag {
216
+ constructor(element) {
217
+ this.element = element;
218
+ return new Proxy(this, {
219
+ get: (target, name, receiver) => target.#property(name, receiver),
220
+ });
221
+ }
222
+
223
+ tag() {
224
+ return this.element.tagName;
225
+ }
226
+
227
+ innerText() {
228
+ let text = "";
229
+ for (const child of this.element.childNodes) {
230
+ if (child.nodeType === 3 || child.nodeType === 4) text += child.data;
231
+ }
232
+ return text;
233
+ }
234
+
235
+ children() {
236
+ return Array.from(this.element.childNodes)
237
+ .filter((child) => child.nodeType === 1)
238
+ .map((child) => new Tag(child));
239
+ }
240
+
241
+ attribute(name) {
242
+ const value = this.element.getAttribute(String(name));
243
+ return value === null ? null : value;
244
+ }
245
+
246
+ attributes() {
247
+ const values = {};
248
+ for (const attribute of Array.from(this.element.attributes)) values[attribute.name] = attribute.value;
249
+ return values;
250
+ }
251
+
252
+ #property(name, receiver) {
253
+ if (typeof name !== "string") return Reflect.get(this, name, receiver);
254
+ if (name === "tag" || name === "innerText" || name === "children" || name === "attribute" || name === "attributes" || name === "element")
255
+ return Reflect.get(this, name, receiver);
256
+ if (this.element.hasAttribute(name)) return this.element.getAttribute(name);
257
+ const matching = Array.from(this.element.childNodes)
258
+ .filter((child) => child.nodeType === 1 && child.tagName === name);
259
+ return matching.length === 1 ? new Tag(matching[0]) : undefined;
260
+ }
261
+ }
262
+
191
263
  export function println(value) {
192
264
  if (output === null) {
193
265
  throw new Error("Lango runtime output is not configured");
@@ -197,6 +269,18 @@ export function println(value) {
197
269
  return null;
198
270
  }
199
271
 
272
+ const defaultLog = Object.freeze({
273
+ debug(value) { return writeLog(process.stdout, "DEBUG", value); },
274
+ info(value) { return writeLog(process.stdout, "INFO", value); },
275
+ warn(value) { return writeLog(process.stderr, "WARN", value); },
276
+ err(value) { return writeLog(process.stderr, "ERROR", value); },
277
+ });
278
+
279
+ function writeLog(stream, level, value) {
280
+ stream.write(\`[\${level}] \${String(value)}\\n\`);
281
+ return null;
282
+ }
283
+
200
284
  export function fromJson(text) {
201
285
  try {
202
286
  return decimalJson(JSON.parse(text));
@@ -448,6 +532,12 @@ export function call(value, ...arguments_) {
448
532
  return normalize(value(...arguments_));
449
533
  }
450
534
 
535
+ export function callMethod(receiver, name, ...arguments_) {
536
+ const value = receiver?.[name];
537
+ if (typeof value !== "function") throw new Error("NotCallable");
538
+ return normalize(value.call(receiver, ...arguments_));
539
+ }
540
+
451
541
  export function boundary(value) {
452
542
  return finite(normalize(value));
453
543
  }
@@ -469,6 +559,7 @@ export function withArity(callback, requiredPositions, name) {
469
559
 
470
560
  export function contextGet(name) {
471
561
  const stack = contexts.get(name);
562
+ if ((!stack || stack.length === 0) && name === "log") return defaultLog;
472
563
  if (!stack || stack.length === 0) {
473
564
  throw new Error(\`Lango context is unavailable: \${name}\`);
474
565
  }
@@ -9,6 +9,10 @@
9
9
  "test": "lango test test/main.lango",
10
10
  "format": "lango format src/main.lango"
11
11
  },
12
+ "lango": {
13
+ "source": "src/main.lango",
14
+ "output": "dist/main.mjs"
15
+ },
12
16
  "devDependencies": {
13
17
  "@lango-project/lango-js": "{{LANGO_VERSION}}"
14
18
  },