@lango-project/lango-js 0.1.0 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lango-project/lango-js",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "src/*.mjs",
19
+ "src/vendor/**",
19
20
  "src/template/**",
20
21
  "README.md",
21
22
  "LICENSE",
package/src/compiler.mjs CHANGED
@@ -231,7 +231,7 @@ function formatLine(line) {
231
231
 
232
232
  class Parser {
233
233
  constructor(source, name) {
234
- this.lines = source.split(/\r?\n/).map((line) => stripComment(line).trimEnd());
234
+ this.lines = normalizeKebabIdentifiers(source).split(/\r?\n/).map((line) => stripComment(line).trimEnd());
235
235
  this.name = name;
236
236
  this.i = 0;
237
237
  }
@@ -518,7 +518,16 @@ class Parser {
518
518
  `expected ${indent} spaces`,
519
519
  );
520
520
  const line = this.line();
521
- const text = line.text.slice(indent);
521
+ const initialText = line.text.slice(indent);
522
+ const text = /^([A-Za-z_]\w*) = \{$/.test(initialText)
523
+ ? initialText
524
+ : this.continuedStatement(initialText, line, indent);
525
+ if (text === "else")
526
+ this.fail(
527
+ line,
528
+ "LNG1259",
529
+ "else must be indented 2 spaces more than its if",
530
+ );
522
531
  if (text.startsWith("assert ")) {
523
532
  body.push({
524
533
  type: "assert",
@@ -540,7 +549,16 @@ class Parser {
540
549
  this.blank();
541
550
  let elseBody = null;
542
551
  if (this.peek() === `${" ".repeat(indent + 2)}else`) {
552
+ const elseLine = { text: this.peek(), number: this.i + 1 };
543
553
  this.i += 1;
554
+ this.blank();
555
+ const elseStart = this.peek();
556
+ if (!elseStart || spaces(elseStart) !== indent + 6)
557
+ this.fail(
558
+ elseStart ? { text: elseStart, number: this.i + 1 } : elseLine,
559
+ "LNG1260",
560
+ "else body must be indented 4 spaces more than else",
561
+ );
544
562
  elseBody = this.block(indent + 6);
545
563
  }
546
564
  body.push({
@@ -739,6 +757,52 @@ class Parser {
739
757
  });
740
758
  else if (/^[A-Za-z_]\w*\+\+$/.test(text))
741
759
  body.push({ type: "increment", name: text.slice(0, -2) });
760
+ else if (/^(\$?[A-Za-z_]\w*) = new ([A-Za-z_]\w*(?:[./][A-Za-z_]\w*)*)\(\(([^)]*)\)$/.test(text)) {
761
+ const [, name, constructor, parameters] =
762
+ /^(\$?[A-Za-z_]\w*) = new ([A-Za-z_]\w*(?:[./][A-Za-z_]\w*)*)\(\(([^)]*)\)$/.exec(text);
763
+ const closure = {
764
+ type: "closure",
765
+ parameters: this.parameters(parameters, line),
766
+ captures: this.captures(),
767
+ line: line.number,
768
+ body: this.block(indent + 4),
769
+ };
770
+ this.blank();
771
+ if (this.peek() !== `${" ".repeat(indent)})`)
772
+ this.fail(line, "LNG1263", "constructor callback requires a closing parenthesis");
773
+ this.i += 1;
774
+ body.push({
775
+ type: "assign",
776
+ name,
777
+ value: { type: "new", name: constructor, arguments: [closure], line: line.number },
778
+ });
779
+ }
780
+ else if (/^(.+)\.([A-Za-z_]\w*)\((.*), \(([^)]*)\)$/.test(text)) {
781
+ const [, receiver, name, argumentsText, parameters] =
782
+ /^(.+)\.([A-Za-z_]\w*)\((.*), \(([^)]*)\)$/.exec(text);
783
+ const closure = {
784
+ type: "closure",
785
+ parameters: this.parameters(parameters, line),
786
+ captures: this.captures(),
787
+ line: line.number,
788
+ body: this.block(indent + 4),
789
+ };
790
+ this.blank();
791
+ if (this.peek() !== `${" ".repeat(indent)})`)
792
+ this.fail(line, "LNG1264", "method callback requires a closing parenthesis");
793
+ this.i += 1;
794
+ body.push({
795
+ type: "expression",
796
+ value: {
797
+ type: "methodCall",
798
+ receiver,
799
+ optional: false,
800
+ name,
801
+ arguments: [...splitTopLevel(argumentsText).map((item) => expr(item, line, this)), closure],
802
+ line: line.number,
803
+ },
804
+ });
805
+ }
742
806
  else if (/^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.test(text)) {
743
807
  const [, first, name, parameters] = /^(.+)\.([A-Za-z_]\w*)\(\(([^)]*)\)$/.exec(text);
744
808
  const steps = [];
@@ -937,6 +1001,17 @@ class Parser {
937
1001
  xml += `\n${this.lines[this.i++]}`;
938
1002
  }
939
1003
  }
1004
+ continuedStatement(source, line, indent) {
1005
+ let depth = curlyDelimiterDepth(source);
1006
+ while (depth > 0) {
1007
+ const next = this.peek();
1008
+ if (next === undefined || spaces(next) < indent)
1009
+ this.fail(line, "LNG1262", "unterminated multi-line expression");
1010
+ source += ` ${this.line().text.trim()}`;
1011
+ depth = curlyDelimiterDepth(source);
1012
+ }
1013
+ return source;
1014
+ }
940
1015
  captures() {
941
1016
  if (!(this.peek() ?? "").trimStart().startsWith("capture ")) return [];
942
1017
  const line = this.line();
@@ -1072,6 +1147,66 @@ function topLevelBinary(text, operators, rightAssociative = false) {
1072
1147
  return match;
1073
1148
  }
1074
1149
 
1150
+ function curlyDelimiterDepth(text) {
1151
+ let quote = null;
1152
+ let depth = 0;
1153
+ for (let index = 0; index < text.length; index += 1) {
1154
+ const character = text[index];
1155
+ if (quote !== null) {
1156
+ if (character === "\\") { index += 1; continue; }
1157
+ if (character === quote) quote = null;
1158
+ continue;
1159
+ }
1160
+ if (character === "'" || character === '"') { quote = character; continue; }
1161
+ if (character === "{") depth += 1;
1162
+ if (character === "}") depth -= 1;
1163
+ }
1164
+ return depth;
1165
+ }
1166
+
1167
+ const KEBAB_MARKER = "__lango_dash__";
1168
+
1169
+ function normalizeKebabIdentifiers(source) {
1170
+ let result = "";
1171
+ let quote = null;
1172
+ for (let index = 0; index < source.length; index += 1) {
1173
+ const character = source[index];
1174
+ if (quote !== null) {
1175
+ result += character;
1176
+ if (character === "\\") result += source[++index] ?? "";
1177
+ else if (character === quote) quote = null;
1178
+ continue;
1179
+ }
1180
+ if (character === "'" || character === '"') { quote = character; result += character; continue; }
1181
+ const match = /^[A-Za-z_]\w*(?:-[A-Za-z_]\w*)+/.exec(source.slice(index));
1182
+ if (match) {
1183
+ result += match[0].replaceAll("-", KEBAB_MARKER);
1184
+ index += match[0].length - 1;
1185
+ continue;
1186
+ }
1187
+ result += character;
1188
+ }
1189
+ return result;
1190
+ }
1191
+
1192
+ function kebabName(name) {
1193
+ return name.replaceAll(KEBAB_MARKER, "-");
1194
+ }
1195
+
1196
+ function emitKebabMember(receiver, operator, name) {
1197
+ return name.includes(KEBAB_MARKER)
1198
+ ? `${receiver}${operator === "?." ? "?." : ""}[${JSON.stringify(kebabName(name))}]`
1199
+ : `${receiver}${operator}${name}`;
1200
+ }
1201
+
1202
+ function emitKebabPath(value) {
1203
+ return value.replace(/(\?\.|\.)([A-Za-z_]\w*)/g, (all, operator, name) =>
1204
+ name.includes(KEBAB_MARKER)
1205
+ ? emitKebabMember("", operator, name)
1206
+ : all,
1207
+ );
1208
+ }
1209
+
1075
1210
  function splitTopLevel(text, separator = ", ") {
1076
1211
  const parts = [];
1077
1212
  let start = 0;
@@ -1200,6 +1335,13 @@ function expr(source, line, parser) {
1200
1335
  const localize = /^_\((.*)\)$/.exec(text);
1201
1336
  if (localize)
1202
1337
  return { type: "translate", literal: literal(localize[1], line, parser) };
1338
+ const decimal = /^\$\((.*)\)$/.exec(text);
1339
+ if (decimal) {
1340
+ const arguments_ = decimal[1] === "" ? [] : splitTopLevel(decimal[1]);
1341
+ if (arguments_.length !== 1)
1342
+ parser.fail(line, "LNG1261", "$() requires exactly one value");
1343
+ return { type: "decimal", value: expr(arguments_[0], line, parser) };
1344
+ }
1203
1345
  const construct =
1204
1346
  /^new ([A-Za-z_]\w*(?:[./][A-Za-z_]\w*)*)\((.*)\)$/.exec(text);
1205
1347
  if (construct)
@@ -1623,7 +1765,8 @@ function validate(program, imports = []) {
1623
1765
  const hostFunctions = new Set(["fetch"]);
1624
1766
  const isDecimalValue = (value) =>
1625
1767
  value &&
1626
- ((value.type === "raw" && (value.value.startsWith("$") || value.value.includes(".$"))) ||
1768
+ (value.type === "decimal" ||
1769
+ (value.type === "raw" && (value.value.startsWith("$") || value.value.includes(".$"))) ||
1627
1770
  (value.type === "unary" &&
1628
1771
  value.operator === "negate" &&
1629
1772
  isDecimalValue(value.value)) ||
@@ -1635,6 +1778,21 @@ function validate(program, imports = []) {
1635
1778
  ((value.left.type === "raw" && value.left.value === "null" && isDecimalValue(value.right)) ||
1636
1779
  (value.right.type === "raw" && value.right.value === "null" && isDecimalValue(value.left)) ||
1637
1780
  (isDecimalValue(value.left) && isDecimalValue(value.right)))));
1781
+ const decimalParameters = (fn) => {
1782
+ const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
1783
+ const accepted = new Set();
1784
+ const visit = (node) => {
1785
+ if (!node || typeof node !== "object") return;
1786
+ if (node.type === "decimal" && node.value?.type === "raw" && parameterNames.has(node.value.value))
1787
+ accepted.add(node.value.value);
1788
+ Object.values(node).forEach((child) => {
1789
+ if (Array.isArray(child)) child.forEach(visit);
1790
+ else visit(child);
1791
+ });
1792
+ };
1793
+ visit(fn.body);
1794
+ return accepted;
1795
+ };
1638
1796
  const returns = (body, result = []) => {
1639
1797
  for (const node of body) {
1640
1798
  if (node.type === "return") result.push(node.value);
@@ -2057,11 +2215,13 @@ function validate(program, imports = []) {
2057
2215
  );
2058
2216
  if (target) {
2059
2217
  validateRequiredArguments(target, value, value.name);
2218
+ const decimalArguments = decimalParameters(target);
2060
2219
  value.arguments.forEach((argument, index) => {
2061
2220
  const parameter = target.parameters[index];
2062
2221
  if (
2063
2222
  parameter &&
2064
2223
  !parameter.name.startsWith("$") &&
2224
+ !decimalArguments.has(parameter.name) &&
2065
2225
  isDecimalValue(argument)
2066
2226
  )
2067
2227
  throw new LangoError(
@@ -2543,6 +2703,10 @@ function validate(program, imports = []) {
2543
2703
  ? node
2544
2704
  : node.type === "return" && node.value?.type === "closure"
2545
2705
  ? node.value
2706
+ : node.type === "assign" && node.value?.type === "new" && node.value.arguments.length === 1 && node.value.arguments[0].type === "closure"
2707
+ ? node.value.arguments[0]
2708
+ : node.type === "expression" && node.value?.type === "methodCall" && node.value.arguments.at(-1)?.type === "closure"
2709
+ ? node.value.arguments.at(-1)
2546
2710
  : null;
2547
2711
  if (closure) {
2548
2712
  const localBindings = new Set([
@@ -2615,7 +2779,7 @@ function validate(program, imports = []) {
2615
2779
  classScope,
2616
2780
  expiredCatchBindings,
2617
2781
  );
2618
- if (node.type === "closureAssign") available.add(node.name);
2782
+ if (node.type === "closureAssign" || node.type === "assign") available.add(node.name);
2619
2783
  continue;
2620
2784
  }
2621
2785
  if (node.type === "assign" && !node.name.startsWith("@"))
@@ -3442,6 +3606,7 @@ function collectEmitLocals(body, names = new Set(), nested = false) {
3442
3606
  }
3443
3607
  function emitDecimalExpression(node, flags) {
3444
3608
  const operand = (value) => {
3609
+ if (value.type === "decimal") return emitExpr(value, flags);
3445
3610
  if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
3446
3611
  return emitDecimalExpression(value, flags);
3447
3612
  if (value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)) {
@@ -3700,6 +3865,7 @@ function emitExpr(node, flags) {
3700
3865
  value?.type === "raw" &&
3701
3866
  (value.value.startsWith("$") || value.value.includes(".$"));
3702
3867
  const decimalValue = (value) =>
3868
+ value?.type === "decimal" ||
3703
3869
  decimalRaw(value) ||
3704
3870
  (value?.type === "binary" && (decimalValue(value.left) || decimalValue(value.right))) ||
3705
3871
  (value?.type === "unary" && decimalValue(value.value));
@@ -3731,9 +3897,13 @@ function emitExpr(node, flags) {
3731
3897
  : normalize(`this.#${node.value.slice(1)}`)
3732
3898
  : flags.className && node.value.startsWith("_")
3733
3899
  ? 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;
3900
+ : (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))
3901
+ ? normalize(emitKebabPath(node.value))
3902
+ : emitKebabPath(node.value);
3903
+ if (node.type === "decimal") {
3904
+ flags.decimal = true;
3905
+ return `__decimal(${emitExpr(node.value, flags)})`;
3906
+ }
3737
3907
  if (node.type === "sql") {
3738
3908
  flags.sql = true;
3739
3909
  const text = node.parts
@@ -3758,7 +3928,9 @@ function emitExpr(node, flags) {
3758
3928
  return `__finite(-${numericLiteral ? emitExpr(node.value, flags) : `__requireNumber(${emitExpr(node.value, flags)})`})`;
3759
3929
  }
3760
3930
  if (node.type === "new")
3761
- return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3931
+ return `new ${node.name.replaceAll("/", ".")}(${node.arguments.map((item) =>
3932
+ item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
3933
+ ).join(", ")})`;
3762
3934
  if (node.type === "superMethodCall")
3763
3935
  return `super.${node.name}(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")})`;
3764
3936
  if (node.type === "index")
@@ -3767,7 +3939,7 @@ function emitExpr(node, flags) {
3767
3939
  );
3768
3940
  if (node.type === "property")
3769
3941
  return normalize(
3770
- `${emitExpr(node.receiver, flags)}${node.optional ? "?." : "."}${node.name}`,
3942
+ emitKebabMember(emitExpr(node.receiver, flags), node.optional ? "?." : ".", node.name),
3771
3943
  );
3772
3944
  if (node.type === "methodCall" || node.type === "namespaceMethodCall") {
3773
3945
  const namespace = node.type === "namespaceMethodCall";
@@ -3793,7 +3965,9 @@ function emitExpr(node, flags) {
3793
3965
  flags.arrayBindings?.has(node.receiver)
3794
3966
  ? "push"
3795
3967
  : node.name;
3796
- const arguments_ = node.arguments.map((item) => emitExpr(item, flags)).join(", ");
3968
+ const arguments_ = node.arguments.map((item) =>
3969
+ item.type === "closure" ? emitClosure(item, 0, flags) : emitExpr(item, flags),
3970
+ ).join(", ");
3797
3971
  const helperCall = (helper) => {
3798
3972
  const call = `${helper}(${receiver}${arguments_ ? `, ${arguments_}` : ""})`;
3799
3973
  if (!node.optional) return call;
@@ -3824,7 +3998,9 @@ function emitExpr(node, flags) {
3824
3998
  flags.insertAt = true;
3825
3999
  return helperCall("__insertAt");
3826
4000
  }
3827
- const call = `${receiver}${visibility === false ? ".#" : node.optional ? "?." : "."}${method}(${arguments_})`;
4001
+ const call = visibility === false
4002
+ ? `${receiver}.#${method}(${arguments_})`
4003
+ : emitKebabMember(receiver, node.optional ? "?." : ".", method) + `(${arguments_})`;
3828
4004
  if (node.optional) return normalize(call);
3829
4005
  if (
3830
4006
  visibility === undefined &&
@@ -3840,7 +4016,8 @@ function emitExpr(node, flags) {
3840
4016
  if (node.type === "object")
3841
4017
  return `{ ${node.fields.map((item) => {
3842
4018
  const value = emitExpr(item.value, flags);
3843
- if (!item.key.startsWith("$")) return `${item.key}: ${value}`;
4019
+ if (!item.key.startsWith("$"))
4020
+ return `${item.key.includes(KEBAB_MARKER) ? JSON.stringify(kebabName(item.key)) : item.key}: ${value}`;
3844
4021
  flags.decimal = true;
3845
4022
  return `${JSON.stringify(item.key)}: __decimal(${item.value.type === "raw" ? JSON.stringify(item.value.value) : value})`;
3846
4023
  }).join(", ")} }`;
package/src/runtime.mjs CHANGED
@@ -31,7 +31,16 @@ Decimal.Rounding = Object.freeze(${JSON.stringify(Decimal.Rounding)});
31
31
  export { Decimal };
32
32
 
33
33
  export function decimal(value) {
34
- return new Decimal(value);
34
+ if (value instanceof Decimal) return value;
35
+ let result;
36
+ try {
37
+ result = new Decimal(value);
38
+ } catch {
39
+ throw new TypeError("Decimal value must be finite");
40
+ }
41
+ if (!result.isFinite())
42
+ throw new TypeError("Decimal value must be finite");
43
+ return result;
35
44
  }
36
45
 
37
46
  export function isDecimal(value) {
@@ -0,0 +1,26 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © `<2026>` `Michael Mclaughlin`
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
26
+