@lango-project/lango-js 0.1.1 → 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/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 = {}) {
@@ -231,7 +254,7 @@ function formatLine(line) {
231
254
 
232
255
  class Parser {
233
256
  constructor(source, name) {
234
- this.lines = source.split(/\r?\n/).map((line) => stripComment(line).trimEnd());
257
+ this.lines = normalizeKebabIdentifiers(source).split(/\r?\n/).map((line) => stripComment(line).trimEnd());
235
258
  this.name = name;
236
259
  this.i = 0;
237
260
  }
@@ -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,
@@ -518,7 +537,16 @@ class Parser {
518
537
  `expected ${indent} spaces`,
519
538
  );
520
539
  const line = this.line();
521
- const text = line.text.slice(indent);
540
+ const initialText = line.text.slice(indent);
541
+ const text = /^([A-Za-z_]\w*) = \{$/.test(initialText)
542
+ ? initialText
543
+ : this.continuedStatement(initialText, line, indent);
544
+ if (text === "else")
545
+ this.fail(
546
+ line,
547
+ "LNG1259",
548
+ "else must be indented 2 spaces more than its if",
549
+ );
522
550
  if (text.startsWith("assert ")) {
523
551
  body.push({
524
552
  type: "assert",
@@ -540,7 +568,16 @@ class Parser {
540
568
  this.blank();
541
569
  let elseBody = null;
542
570
  if (this.peek() === `${" ".repeat(indent + 2)}else`) {
571
+ const elseLine = { text: this.peek(), number: this.i + 1 };
543
572
  this.i += 1;
573
+ this.blank();
574
+ const elseStart = this.peek();
575
+ if (!elseStart || spaces(elseStart) !== indent + 6)
576
+ this.fail(
577
+ elseStart ? { text: elseStart, number: this.i + 1 } : elseLine,
578
+ "LNG1260",
579
+ "else body must be indented 4 spaces more than else",
580
+ );
544
581
  elseBody = this.block(indent + 6);
545
582
  }
546
583
  body.push({
@@ -739,7 +776,80 @@ class Parser {
739
776
  });
740
777
  else if (/^[A-Za-z_]\w*\+\+$/.test(text))
741
778
  body.push({ type: "increment", name: text.slice(0, -2) });
742
- else if (/^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.test(text)) {
779
+ else if (/^(\$?[A-Za-z_]\w*) = new ([A-Za-z_]\w*(?:[./][A-Za-z_]\w*)*)\(\(([^)]*)\)$/.test(text)) {
780
+ const [, name, constructor, parameters] =
781
+ /^(\$?[A-Za-z_]\w*) = new ([A-Za-z_]\w*(?:[./][A-Za-z_]\w*)*)\(\(([^)]*)\)$/.exec(text);
782
+ const closure = {
783
+ type: "closure",
784
+ parameters: this.parameters(parameters, line),
785
+ captures: this.captures(),
786
+ line: line.number,
787
+ body: this.block(indent + 4),
788
+ };
789
+ this.blank();
790
+ if (this.peek() !== `${" ".repeat(indent)})`)
791
+ this.fail(line, "LNG1263", "constructor callback requires a closing parenthesis");
792
+ this.i += 1;
793
+ body.push({
794
+ type: "assign",
795
+ name,
796
+ value: { type: "new", name: constructor, arguments: [closure], line: line.number },
797
+ });
798
+ }
799
+ else if (!text.includes(" = ") && /^(.+)\.([A-Za-z_]\w*)\((.*), \(([^)]*)\)$/.test(text)) {
800
+ const [, receiver, name, argumentsText, parameters] =
801
+ /^(.+)\.([A-Za-z_]\w*)\((.*), \(([^)]*)\)$/.exec(text);
802
+ const closure = {
803
+ type: "closure",
804
+ parameters: this.parameters(parameters, line),
805
+ captures: this.captures(),
806
+ line: line.number,
807
+ body: this.block(indent + 4),
808
+ };
809
+ this.blank();
810
+ if (this.peek() !== `${" ".repeat(indent)})`)
811
+ this.fail(line, "LNG1264", "method callback requires a closing parenthesis");
812
+ this.i += 1;
813
+ body.push({
814
+ type: "expression",
815
+ value: {
816
+ type: "methodCall",
817
+ receiver,
818
+ optional: false,
819
+ name,
820
+ arguments: [...splitTopLevel(argumentsText).map((item) => expr(item, line, this)), closure],
821
+ line: line.number,
822
+ },
823
+ });
824
+ }
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)) {
743
853
  const [, first, name, parameters] = /^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.exec(text);
744
854
  const steps = [];
745
855
  let step = { name, parameters: this.parameters(parameters, line), captures: this.captures(), line: line.number, body: this.block(indent + 4) };
@@ -895,7 +1005,7 @@ class Parser {
895
1005
  operator: suffix === "++" ? "+" : "-",
896
1006
  });
897
1007
  } else {
898
- 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);
899
1009
  if (!assignment)
900
1010
  this.fail(line, "LNG1106", `unsupported Faz 2 statement: ${text}`);
901
1011
  body.push({
@@ -937,6 +1047,17 @@ class Parser {
937
1047
  xml += `\n${this.lines[this.i++]}`;
938
1048
  }
939
1049
  }
1050
+ continuedStatement(source, line, indent) {
1051
+ let depth = curlyDelimiterDepth(source);
1052
+ while (depth > 0) {
1053
+ const next = this.peek();
1054
+ if (next === undefined || spaces(next) < indent)
1055
+ this.fail(line, "LNG1262", "unterminated multi-line expression");
1056
+ source += ` ${this.line().text.trim()}`;
1057
+ depth = curlyDelimiterDepth(source);
1058
+ }
1059
+ return source;
1060
+ }
940
1061
  captures() {
941
1062
  if (!(this.peek() ?? "").trimStart().startsWith("capture ")) return [];
942
1063
  const line = this.line();
@@ -1072,6 +1193,66 @@ function topLevelBinary(text, operators, rightAssociative = false) {
1072
1193
  return match;
1073
1194
  }
1074
1195
 
1196
+ function curlyDelimiterDepth(text) {
1197
+ let quote = null;
1198
+ let depth = 0;
1199
+ for (let index = 0; index < text.length; index += 1) {
1200
+ const character = text[index];
1201
+ if (quote !== null) {
1202
+ if (character === "\\") { index += 1; continue; }
1203
+ if (character === quote) quote = null;
1204
+ continue;
1205
+ }
1206
+ if (character === "'" || character === '"') { quote = character; continue; }
1207
+ if (character === "{") depth += 1;
1208
+ if (character === "}") depth -= 1;
1209
+ }
1210
+ return depth;
1211
+ }
1212
+
1213
+ const KEBAB_MARKER = "__lango_dash__";
1214
+
1215
+ function normalizeKebabIdentifiers(source) {
1216
+ let result = "";
1217
+ let quote = null;
1218
+ for (let index = 0; index < source.length; index += 1) {
1219
+ const character = source[index];
1220
+ if (quote !== null) {
1221
+ result += character;
1222
+ if (character === "\\") result += source[++index] ?? "";
1223
+ else if (character === quote) quote = null;
1224
+ continue;
1225
+ }
1226
+ if (character === "'" || character === '"') { quote = character; result += character; continue; }
1227
+ const match = /^[A-Za-z_]\w*(?:-[A-Za-z_]\w*)+/.exec(source.slice(index));
1228
+ if (match) {
1229
+ result += match[0].replaceAll("-", KEBAB_MARKER);
1230
+ index += match[0].length - 1;
1231
+ continue;
1232
+ }
1233
+ result += character;
1234
+ }
1235
+ return result;
1236
+ }
1237
+
1238
+ function kebabName(name) {
1239
+ return name.replaceAll(KEBAB_MARKER, "-");
1240
+ }
1241
+
1242
+ function emitKebabMember(receiver, operator, name) {
1243
+ return name.includes(KEBAB_MARKER)
1244
+ ? `${receiver}${operator === "?." ? "?." : ""}[${JSON.stringify(kebabName(name))}]`
1245
+ : `${receiver}${operator}${name}`;
1246
+ }
1247
+
1248
+ function emitKebabPath(value) {
1249
+ return value.replace(/(\?\.|\.)([A-Za-z_]\w*)/g, (all, operator, name) =>
1250
+ name.includes(KEBAB_MARKER)
1251
+ ? emitKebabMember("", operator, name)
1252
+ : all,
1253
+ );
1254
+ }
1255
+
1075
1256
  function splitTopLevel(text, separator = ", ") {
1076
1257
  const parts = [];
1077
1258
  let start = 0;
@@ -1153,10 +1334,18 @@ function expr(source, line, parser) {
1153
1334
  }),
1154
1335
  };
1155
1336
  }
1337
+ if (text.startsWith("not "))
1338
+ return {
1339
+ type: "unary",
1340
+ operator: "not",
1341
+ value: expr(text.slice(4), line, parser),
1342
+ };
1156
1343
  for (const operators of [
1157
1344
  ["??"],
1158
- ["or", "||"],
1159
- ["and", "&&"],
1345
+ ["or"],
1346
+ ["and"],
1347
+ ["||"],
1348
+ ["&&"],
1160
1349
  ["==", "!="],
1161
1350
  [">=", "<=", ">", "<", "not in", "in"],
1162
1351
  ["+", "-"],
@@ -1179,12 +1368,6 @@ function expr(source, line, parser) {
1179
1368
  left: expr(text.slice(0, exponent.index), line, parser),
1180
1369
  right: expr(text.slice(exponent.index + exponent.operator.length + 2), line, parser),
1181
1370
  };
1182
- if (text.startsWith("not "))
1183
- return {
1184
- type: "unary",
1185
- operator: "not",
1186
- value: expr(text.slice(4), line, parser),
1187
- };
1188
1371
  if (/^-[^\s]/.test(text))
1189
1372
  return {
1190
1373
  type: "unary",
@@ -1200,6 +1383,13 @@ function expr(source, line, parser) {
1200
1383
  const localize = /^_\((.*)\)$/.exec(text);
1201
1384
  if (localize)
1202
1385
  return { type: "translate", literal: literal(localize[1], line, parser) };
1386
+ const decimal = /^\$\((.*)\)$/.exec(text);
1387
+ if (decimal) {
1388
+ const arguments_ = decimal[1] === "" ? [] : splitTopLevel(decimal[1]);
1389
+ if (arguments_.length !== 1)
1390
+ parser.fail(line, "LNG1261", "$() requires exactly one value");
1391
+ return { type: "decimal", value: expr(arguments_[0], line, parser) };
1392
+ }
1203
1393
  const construct =
1204
1394
  /^new ([A-Za-z_]\w*(?:[./][A-Za-z_]\w*)*)\((.*)\)$/.exec(text);
1205
1395
  if (construct)
@@ -1313,6 +1503,17 @@ function expr(source, line, parser) {
1313
1503
  : [],
1314
1504
  line: line.number,
1315
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
+ };
1316
1517
  const methodCall =
1317
1518
  /^([A-Za-z_]\w*(?:(?:\.|\?\.)[A-Za-z_]\w*)*?)(\?\.|\.)([A-Za-z_]\w*)\((.*)\)$/.exec(
1318
1519
  text,
@@ -1618,12 +1819,15 @@ function validate(program, imports = []) {
1618
1819
  ["is_array", 1],
1619
1820
  ["range", [1, 2, 3]],
1620
1821
  ["to_js_number", 1],
1822
+ ["import_module", 1],
1823
+ ["xml2obj", 1],
1621
1824
  ["start_trans", 0], ["commit_trans", 0], ["rollback_trans", 0], ["trans_state", 0],
1622
1825
  ]);
1623
1826
  const hostFunctions = new Set(["fetch"]);
1624
1827
  const isDecimalValue = (value) =>
1625
1828
  value &&
1626
- ((value.type === "raw" && (value.value.startsWith("$") || value.value.includes(".$"))) ||
1829
+ (value.type === "decimal" ||
1830
+ (value.type === "raw" && (value.value.startsWith("$") || value.value.includes(".$"))) ||
1627
1831
  (value.type === "unary" &&
1628
1832
  value.operator === "negate" &&
1629
1833
  isDecimalValue(value.value)) ||
@@ -1635,6 +1839,21 @@ function validate(program, imports = []) {
1635
1839
  ((value.left.type === "raw" && value.left.value === "null" && isDecimalValue(value.right)) ||
1636
1840
  (value.right.type === "raw" && value.right.value === "null" && isDecimalValue(value.left)) ||
1637
1841
  (isDecimalValue(value.left) && isDecimalValue(value.right)))));
1842
+ const decimalParameters = (fn) => {
1843
+ const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
1844
+ const accepted = new Set();
1845
+ const visit = (node) => {
1846
+ if (!node || typeof node !== "object") return;
1847
+ if (node.type === "decimal" && node.value?.type === "raw" && parameterNames.has(node.value.value))
1848
+ accepted.add(node.value.value);
1849
+ Object.values(node).forEach((child) => {
1850
+ if (Array.isArray(child)) child.forEach(visit);
1851
+ else visit(child);
1852
+ });
1853
+ };
1854
+ visit(fn.body);
1855
+ return accepted;
1856
+ };
1638
1857
  const returns = (body, result = []) => {
1639
1858
  for (const node of body) {
1640
1859
  if (node.type === "return") result.push(node.value);
@@ -2057,11 +2276,13 @@ function validate(program, imports = []) {
2057
2276
  );
2058
2277
  if (target) {
2059
2278
  validateRequiredArguments(target, value, value.name);
2279
+ const decimalArguments = decimalParameters(target);
2060
2280
  value.arguments.forEach((argument, index) => {
2061
2281
  const parameter = target.parameters[index];
2062
2282
  if (
2063
2283
  parameter &&
2064
2284
  !parameter.name.startsWith("$") &&
2285
+ !decimalArguments.has(parameter.name) &&
2065
2286
  isDecimalValue(argument)
2066
2287
  )
2067
2288
  throw new LangoError(
@@ -2348,6 +2569,15 @@ function validate(program, imports = []) {
2348
2569
  return names;
2349
2570
  };
2350
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
+
2351
2581
  const globalDeclarations = (body, names = new Set()) => {
2352
2582
  for (const node of body) {
2353
2583
  if (node.type === "globalDeclare") names.add(node.name);
@@ -2403,9 +2633,14 @@ function validate(program, imports = []) {
2403
2633
  outerBindings,
2404
2634
  classScope = null,
2405
2635
  inheritedExpiredCatchBindings = new Set(),
2636
+ outerContextBindings = new Set(),
2406
2637
  ) => {
2407
2638
  const available = new Set(outerBindings);
2408
2639
  const expiredCatchBindings = new Set(inheritedExpiredCatchBindings);
2640
+ const availableContexts = new Set([
2641
+ ...outerContextBindings,
2642
+ ...contextBindings(body),
2643
+ ]);
2409
2644
  for (const node of body) {
2410
2645
  const references = new Set();
2411
2646
  [node.value, node.condition, node.collection, node.arguments, node.tests].forEach((value) => {
@@ -2425,6 +2660,7 @@ function validate(program, imports = []) {
2425
2660
  new Set(available),
2426
2661
  classScope,
2427
2662
  expiredCatchBindings,
2663
+ availableContexts,
2428
2664
  );
2429
2665
  const right = node.elseBody
2430
2666
  ? validateClosures(
@@ -2432,6 +2668,7 @@ function validate(program, imports = []) {
2432
2668
  new Set(available),
2433
2669
  classScope,
2434
2670
  expiredCatchBindings,
2671
+ availableContexts,
2435
2672
  )
2436
2673
  : available;
2437
2674
  const exits = [];
@@ -2450,6 +2687,7 @@ function validate(program, imports = []) {
2450
2687
  new Set(available),
2451
2688
  classScope,
2452
2689
  expiredCatchBindings,
2690
+ availableContexts,
2453
2691
  ),
2454
2692
  );
2455
2693
  if (!node.elseBody || reachesEnd(node.elseBody))
@@ -2460,6 +2698,7 @@ function validate(program, imports = []) {
2460
2698
  new Set(available),
2461
2699
  classScope,
2462
2700
  expiredCatchBindings,
2701
+ availableContexts,
2463
2702
  )
2464
2703
  : new Set(available),
2465
2704
  );
@@ -2475,6 +2714,7 @@ function validate(program, imports = []) {
2475
2714
  new Set(available),
2476
2715
  classScope,
2477
2716
  expiredCatchBindings,
2717
+ availableContexts,
2478
2718
  );
2479
2719
  if (reachesEnd(node.tryBody)) exits.push(tryExit);
2480
2720
  if (node.catchBody) {
@@ -2485,6 +2725,7 @@ function validate(program, imports = []) {
2485
2725
  new Set(
2486
2726
  [...expiredCatchBindings].filter((name) => name !== node.catchName),
2487
2727
  ),
2728
+ availableContexts,
2488
2729
  );
2489
2730
  if (reachesEnd(node.catchBody)) exits.push(catchExit);
2490
2731
  }
@@ -2498,6 +2739,7 @@ function validate(program, imports = []) {
2498
2739
  certain,
2499
2740
  classScope,
2500
2741
  new Set([...expiredCatchBindings, node.catchName].filter(Boolean)),
2742
+ availableContexts,
2501
2743
  )
2502
2744
  : certain;
2503
2745
  if (!node.finallyBody || reachesEnd(node.finallyBody))
@@ -2511,6 +2753,7 @@ function validate(program, imports = []) {
2511
2753
  new Set([...available, node.item]),
2512
2754
  classScope,
2513
2755
  expiredCatchBindings,
2756
+ availableContexts,
2514
2757
  );
2515
2758
  continue;
2516
2759
  }
@@ -2520,6 +2763,7 @@ function validate(program, imports = []) {
2520
2763
  new Set([...available, node.key, node.value]),
2521
2764
  classScope,
2522
2765
  expiredCatchBindings,
2766
+ availableContexts,
2523
2767
  );
2524
2768
  continue;
2525
2769
  }
@@ -2535,6 +2779,7 @@ function validate(program, imports = []) {
2535
2779
  new Set(available),
2536
2780
  classScope,
2537
2781
  expiredCatchBindings,
2782
+ availableContexts,
2538
2783
  );
2539
2784
  continue;
2540
2785
  }
@@ -2543,6 +2788,10 @@ function validate(program, imports = []) {
2543
2788
  ? node
2544
2789
  : node.type === "return" && node.value?.type === "closure"
2545
2790
  ? node.value
2791
+ : node.type === "assign" && node.value?.type === "new" && node.value.arguments.length === 1 && node.value.arguments[0].type === "closure"
2792
+ ? node.value.arguments[0]
2793
+ : node.type === "expression" && node.value?.type === "methodCall" && node.value.arguments.at(-1)?.type === "closure"
2794
+ ? node.value.arguments.at(-1)
2546
2795
  : null;
2547
2796
  if (closure) {
2548
2797
  const localBindings = new Set([
@@ -2562,9 +2811,14 @@ function validate(program, imports = []) {
2562
2811
  (name) =>
2563
2812
  !localBindings.has(name) &&
2564
2813
  !contextNames.has(name) &&
2565
- !globalNames.has(name),
2814
+ !globalNames.has(name) &&
2815
+ !standardFunctions.has(name) &&
2816
+ !hostFunctions.has(name),
2566
2817
  );
2567
2818
  const captures = new Set(closure.captures);
2819
+ const contextCaptures = new Set(
2820
+ closure.captures.filter((name) => availableContexts.has(name)),
2821
+ );
2568
2822
  for (const name of needed)
2569
2823
  if (!captures.has(name))
2570
2824
  throw new LangoError(
@@ -2573,7 +2827,7 @@ function validate(program, imports = []) {
2573
2827
  `MissingCapture: ${name}; add capture ${name}`,
2574
2828
  );
2575
2829
  for (const name of captures) {
2576
- if (!needed.includes(name))
2830
+ if (!needed.includes(name) && !contextCaptures.has(name))
2577
2831
  throw new LangoError(
2578
2832
  "LNG1211",
2579
2833
  closure.line,
@@ -2581,7 +2835,7 @@ function validate(program, imports = []) {
2581
2835
  );
2582
2836
  const protectedMember = Boolean(classScope && name.startsWith("_"));
2583
2837
  const propertyCapture = name.startsWith("@") || protectedMember;
2584
- if (!propertyCapture && !available.has(name))
2838
+ if (!propertyCapture && !contextCaptures.has(name) && !available.has(name))
2585
2839
  throw new LangoError(
2586
2840
  "LNG1212",
2587
2841
  closure.line,
@@ -2614,8 +2868,9 @@ function validate(program, imports = []) {
2614
2868
  new Set([...available, ...localBindings]),
2615
2869
  classScope,
2616
2870
  expiredCatchBindings,
2871
+ new Set([...availableContexts, ...contextCaptures]),
2617
2872
  );
2618
- if (node.type === "closureAssign") available.add(node.name);
2873
+ if (node.type === "closureAssign" || node.type === "assign") available.add(node.name);
2619
2874
  continue;
2620
2875
  }
2621
2876
  if (node.type === "assign" && !node.name.startsWith("@"))
@@ -3163,6 +3418,7 @@ function emit(program, moduleImports = [], includeTests = false) {
3163
3418
  flags.print && "println as __println",
3164
3419
  flags.translate && "translate as __translate",
3165
3420
  flags.xml && "xmlEscape as __xmlEscape",
3421
+ flags.xmlObject && "xml2obj as __xml2obj",
3166
3422
  flags.context && "contextGet as __contextGet",
3167
3423
  flags.context && "contextSet as __contextSet",
3168
3424
  flags.context && "contextRestore as __contextRestore",
@@ -3192,6 +3448,7 @@ function emit(program, moduleImports = [], includeTests = false) {
3192
3448
  flags.finite && "finite as __finite",
3193
3449
  flags.number && "requireNumber as __requireNumber",
3194
3450
  flags.call && "call as __call",
3451
+ flags.callMethod && "callMethod as __callMethod",
3195
3452
  flags.boundary && "boundary as __boundary",
3196
3453
  flags.database && "startTrans as __startTrans",
3197
3454
  flags.database && "commitTrans as __commitTrans",
@@ -3442,6 +3699,7 @@ function collectEmitLocals(body, names = new Set(), nested = false) {
3442
3699
  }
3443
3700
  function emitDecimalExpression(node, flags) {
3444
3701
  const operand = (value) => {
3702
+ if (value.type === "decimal") return emitExpr(value, flags);
3445
3703
  if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
3446
3704
  return emitDecimalExpression(value, flags);
3447
3705
  if (value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)) {
@@ -3497,7 +3755,7 @@ function emitStatement(node, depth, declared, flags) {
3497
3755
  if (node.type === "assign") {
3498
3756
  const decimalBinding = node.name.startsWith("$") || /^@{1,2}\$/.test(node.name);
3499
3757
  const property = node.name.slice(node.name.startsWith("@@") ? 2 : 1);
3500
- 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)
3501
3759
  ? node.name
3502
3760
  : node.name.startsWith("@@")
3503
3761
  ? `${flags.className}.${property}`
@@ -3509,7 +3767,7 @@ function emitStatement(node, depth, declared, flags) {
3509
3767
  ? `this.${node.name}`
3510
3768
  : node.name;
3511
3769
  const prefix =
3512
- /^[A-Za-z_]\w*\.\$?[A-Za-z_]\w*$/.test(node.name)
3770
+ /^[A-Za-z_]\w*(?:\.\$?[A-Za-z_]\w*)+$/.test(node.name)
3513
3771
  ? ""
3514
3772
  : node.name.startsWith("@") ||
3515
3773
  (flags.className && node.name.startsWith("_")) ||
@@ -3643,7 +3901,11 @@ function emitClosure(node, depth, flags) {
3643
3901
  const priorContextNames = flags.contextNames;
3644
3902
  const priorGlobalNames = flags.globalNames;
3645
3903
  const priorArrayBindings = flags.arrayBindings;
3646
- 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
+ ]);
3647
3909
  flags.contextNames = contextNames;
3648
3910
  flags.globalNames = collectGlobalNames(node.body);
3649
3911
  flags.arrayBindings = new Set();
@@ -3671,7 +3933,16 @@ function emitClosure(node, depth, flags) {
3671
3933
  if (!contextNames.size)
3672
3934
  return wrap(`(${parameters}) => {\n${boundary}${body}${pad} return null;\n${pad}}`);
3673
3935
  flags.context = true;
3674
- 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);
3675
3946
  }
3676
3947
  function emitArityGuard(parameters, actual, name, depth, flags) {
3677
3948
  const positions = parameters
@@ -3695,11 +3966,18 @@ function emitBoundaryChecks(parameters, depth, flags) {
3695
3966
  })
3696
3967
  .join("");
3697
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
+
3698
3975
  function emitExpr(node, flags) {
3699
3976
  const decimalRaw = (value) =>
3700
3977
  value?.type === "raw" &&
3701
3978
  (value.value.startsWith("$") || value.value.includes(".$"));
3702
3979
  const decimalValue = (value) =>
3980
+ value?.type === "decimal" ||
3703
3981
  decimalRaw(value) ||
3704
3982
  (value?.type === "binary" && (decimalValue(value.left) || decimalValue(value.right))) ||
3705
3983
  (value?.type === "unary" && decimalValue(value.value));
@@ -3731,9 +4009,13 @@ function emitExpr(node, flags) {
3731
4009
  : normalize(`this.#${node.value.slice(1)}`)
3732
4010
  : flags.className && node.value.startsWith("_")
3733
4011
  ? normalize(`this.${node.value}`)
3734
- : (node.value.includes("?.") || /^(?:[A-Za-z_]\w*\.[A-Za-z_]\w*|[A-Za-z_]\w*\[(?:\d+|[A-Za-z_]\w*|['"][^'"]*['"])\])$/.test(node.value))
3735
- ? normalize(node.value)
3736
- : node.value;
4012
+ : (node.value.includes("?.") || node.value.includes(KEBAB_MARKER) || /^(?:[A-Za-z_]\w*\.[A-Za-z_]\w*|[A-Za-z_]\w*\[(?:\d+|[A-Za-z_]\w*|['"][^'"]*['"])\])$/.test(node.value))
4013
+ ? normalize(emitKebabPath(node.value))
4014
+ : emitKebabPath(node.value);
4015
+ if (node.type === "decimal") {
4016
+ flags.decimal = true;
4017
+ return `__decimal(${emitExpr(node.value, flags)})`;
4018
+ }
3737
4019
  if (node.type === "sql") {
3738
4020
  flags.sql = true;
3739
4021
  const text = node.parts
@@ -3758,23 +4040,25 @@ function emitExpr(node, flags) {
3758
4040
  return `__finite(-${numericLiteral ? emitExpr(node.value, flags) : `__requireNumber(${emitExpr(node.value, flags)})`})`;
3759
4041
  }
3760
4042
  if (node.type === "new")
3761
- return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
4043
+ return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
4044
+ item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
4045
+ ).join(", ")})`;
3762
4046
  if (node.type === "superMethodCall")
3763
4047
  return `super.${node.name}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3764
4048
  if (node.type === "index")
3765
4049
  return normalize(
3766
- `${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)}]`,
3767
4051
  );
3768
4052
  if (node.type === "property")
3769
4053
  return normalize(
3770
- `${emitExpr(node.receiver, flags)}${node.optional ? "?." : "."}${node.name}`,
4054
+ emitKebabMember(emitExpr(node.receiver, flags), node.optional ? "?." : ".", node.name),
3771
4055
  );
3772
4056
  if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
3773
4057
  const namespace = node.type === "namespaceMethodCall";
3774
4058
  const sourceReceiver = namespace
3775
4059
  ? node.receiver.replaceAll("/", ".")
3776
4060
  : typeof node.receiver === "string"
3777
- ? node.receiver
4061
+ ? emitReceiverName(node.receiver, flags)
3778
4062
  : emitExpr(node.receiver, flags);
3779
4063
  const type =
3780
4064
  sourceReceiver === flags.className ||
@@ -3793,7 +4077,9 @@ function emitExpr(node, flags) {
3793
4077
  flags.arrayBindings?.has(node.receiver)
3794
4078
  ? "push"
3795
4079
  : node.name;
3796
- const arguments_ = node.arguments.map((item) => emitExpr(item, flags)).join(", ");
4080
+ const arguments_ = node.arguments.map((item) =>
4081
+ item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
4082
+ ).join(", ");
3797
4083
  const helperCall = (helper) => {
3798
4084
  const call = `${helper}(${receiver}${arguments_ ? `, ${arguments_}` : ""})`;
3799
4085
  if (!node.optional) return call;
@@ -3824,7 +4110,9 @@ function emitExpr(node, flags) {
3824
4110
  flags.insertAt = true;
3825
4111
  return helperCall("__insertAt");
3826
4112
  }
3827
- const call = `${receiver}${visibility === false ? ".#" : node.optional ? "?." : "."}${method}(${arguments_})`;
4113
+ const call = visibility === false
4114
+ ? `${receiver}.#${method}(${arguments_})`
4115
+ : emitKebabMember(receiver, node.optional ? "?." : ".", method) + `(${arguments_})`;
3828
4116
  if (node.optional) return normalize(call);
3829
4117
  if (
3830
4118
  visibility === undefined &&
@@ -3840,7 +4128,8 @@ function emitExpr(node, flags) {
3840
4128
  if (node.type === "object")
3841
4129
  return `{ ${node.fields.map((item) => {
3842
4130
  const value = emitExpr(item.value, flags);
3843
- if (!item.key.startsWith("$")) return `${item.key}: ${value}`;
4131
+ if (!item.key.startsWith("$"))
4132
+ return `${item.key.includes(KEBAB_MARKER) ? JSON.stringify(kebabName(item.key)) : item.key}: ${value}`;
3844
4133
  flags.decimal = true;
3845
4134
  return `${JSON.stringify(item.key)}: __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value})`;
3846
4135
  }).join(", ")} }`;
@@ -3912,6 +4201,14 @@ function emitExpr(node, flags) {
3912
4201
  flags.toJsNumber = true;
3913
4202
  return `__toJsNumber(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3914
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
+ }
3915
4212
  if (node.type === "call")
3916
4213
  if (node.name === "from_json" || node.name === "to_json") {
3917
4214
  flags.json = true;
@@ -3951,6 +4248,10 @@ function emitExpr(node, flags) {
3951
4248
  return `__call(${node.name}, ${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3952
4249
  } else
3953
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
+ }
3954
4255
  if (node.type === "string") return emitString(node.literal, flags);
3955
4256
  if (node.type === "xml") {
3956
4257
  flags.xml = true;