@lango-project/lango-js 0.1.6 → 0.1.7

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.6",
3
+ "version": "0.1.7",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/compiler.mjs CHANGED
@@ -1181,18 +1181,42 @@ function collectSqlParameters(sql) {
1181
1181
  return [...new Set(names)];
1182
1182
  }
1183
1183
 
1184
+ function interpolationEnd(text, start) {
1185
+ let depth = 1;
1186
+ for (let index = start; index < text.length; index += 1) {
1187
+ const character = text[index];
1188
+ if (character === "'" || character === '"') {
1189
+ index = quotedStringEnd(text, index);
1190
+ if (index < 0) return -1;
1191
+ } else if (character === "{") depth += 1;
1192
+ else if (character === "}" && --depth === 0) return index;
1193
+ }
1194
+ return -1;
1195
+ }
1196
+
1197
+ function quotedStringEnd(text, start) {
1198
+ const quote = text[start];
1199
+ for (let index = start + 1; index < text.length; index += 1) {
1200
+ if (text[index] === "\\") { index += 1; continue; }
1201
+ if (text.startsWith("${", index)) {
1202
+ index = interpolationEnd(text, index + 2);
1203
+ if (index < 0) return -1;
1204
+ } else if (text[index] === quote) return index;
1205
+ }
1206
+ return -1;
1207
+ }
1208
+
1209
+ function skipString(text, start) {
1210
+ const end = quotedStringEnd(text, start);
1211
+ return end < 0 ? text.length : end;
1212
+ }
1213
+
1184
1214
  function topLevelBinary(text, operators, rightAssociative = false) {
1185
- let quote = null;
1186
1215
  let depth = 0;
1187
1216
  let match = null;
1188
1217
  for (let index = 0; index < text.length; index += 1) {
1189
1218
  const character = text[index];
1190
- if (quote !== null) {
1191
- if (character === "\\") { index += 1; continue; }
1192
- if (character === quote) quote = null;
1193
- continue;
1194
- }
1195
- if (character === "'" || character === '"') { quote = character; continue; }
1219
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1196
1220
  if ("([{".includes(character)) { depth += 1; continue; }
1197
1221
  if (")]}".includes(character)) { depth -= 1; continue; }
1198
1222
  if (depth !== 0) continue;
@@ -1208,16 +1232,10 @@ function topLevelBinary(text, operators, rightAssociative = false) {
1208
1232
  }
1209
1233
 
1210
1234
  function curlyDelimiterDepth(text) {
1211
- let quote = null;
1212
1235
  let depth = 0;
1213
1236
  for (let index = 0; index < text.length; index += 1) {
1214
1237
  const character = text[index];
1215
- if (quote !== null) {
1216
- if (character === "\\") { index += 1; continue; }
1217
- if (character === quote) quote = null;
1218
- continue;
1219
- }
1220
- if (character === "'" || character === '"') { quote = character; continue; }
1238
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1221
1239
  if (character === "{") depth += 1;
1222
1240
  if (character === "}") depth -= 1;
1223
1241
  }
@@ -1228,16 +1246,14 @@ const KEBAB_MARKER = "__lango_dash__";
1228
1246
 
1229
1247
  function normalizeKebabIdentifiers(source) {
1230
1248
  let result = "";
1231
- let quote = null;
1232
1249
  for (let index = 0; index < source.length; index += 1) {
1233
1250
  const character = source[index];
1234
- if (quote !== null) {
1235
- result += character;
1236
- if (character === "\\") result += source[++index] ?? "";
1237
- else if (character === quote) quote = null;
1251
+ if (character === "'" || character === '"') {
1252
+ const end = skipString(source, index);
1253
+ result += source.slice(index, end + 1);
1254
+ index = end;
1238
1255
  continue;
1239
1256
  }
1240
- if (character === "'" || character === '"') { quote = character; result += character; continue; }
1241
1257
  const match = /^[A-Za-z_]\w*(?:-[A-Za-z_]\w*)+/.exec(source.slice(index));
1242
1258
  if (match) {
1243
1259
  result += match[0].replaceAll("-", KEBAB_MARKER);
@@ -1270,16 +1286,10 @@ function emitKebabPath(value) {
1270
1286
  function splitTopLevel(text, separator = ", ") {
1271
1287
  const parts = [];
1272
1288
  let start = 0;
1273
- let quote = null;
1274
1289
  let depth = 0;
1275
1290
  for (let index = 0; index < text.length; index += 1) {
1276
1291
  const character = text[index];
1277
- if (quote !== null) {
1278
- if (character === "\\") { index += 1; continue; }
1279
- if (character === quote) quote = null;
1280
- continue;
1281
- }
1282
- if (character === "'" || character === '"') { quote = character; continue; }
1292
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1283
1293
  if ("([{".includes(character)) { depth += 1; continue; }
1284
1294
  if (")]}".includes(character)) { depth -= 1; continue; }
1285
1295
  if (depth === 0 && text.startsWith(separator, index)) {
@@ -1294,16 +1304,10 @@ function splitTopLevel(text, separator = ", ") {
1294
1304
 
1295
1305
  function parenthesizedExpression(text) {
1296
1306
  if (!text.startsWith("(") || !text.endsWith(")")) return null;
1297
- let quote = null;
1298
1307
  let depth = 0;
1299
1308
  for (let index = 0; index < text.length; index += 1) {
1300
1309
  const character = text[index];
1301
- if (quote !== null) {
1302
- if (character === "\\") { index += 1; continue; }
1303
- if (character === quote) quote = null;
1304
- continue;
1305
- }
1306
- if (character === "'" || character === '"') { quote = character; continue; }
1310
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1307
1311
  if (character === "(") depth += 1;
1308
1312
  if (character === ")") {
1309
1313
  depth -= 1;
@@ -1744,23 +1748,13 @@ function xmlTagEnd(source, start) {
1744
1748
  }
1745
1749
 
1746
1750
  function stripComment(line) {
1747
- let quote = null;
1748
- let escaped = false;
1749
1751
  for (let index = 0; index < line.length; index += 1) {
1750
1752
  const character = line[index];
1751
- if (escaped) {
1752
- escaped = false;
1753
- continue;
1754
- }
1755
- if (quote && character === "\\") {
1756
- escaped = true;
1757
- continue;
1758
- }
1759
1753
  if (character === '"' || character === "'") {
1760
- quote = quote === character ? null : quote ?? character;
1754
+ index = skipString(line, index);
1761
1755
  continue;
1762
1756
  }
1763
- if (!quote && character === "#") return line.slice(0, index).trimEnd();
1757
+ if (character === "#") return line.slice(0, index).trimEnd();
1764
1758
  }
1765
1759
  return line;
1766
1760
  }
@@ -1769,20 +1763,25 @@ function literal(value, line, parser) {
1769
1763
  const quote = value[0];
1770
1764
  if (!((quote === "'" || quote === '"') && value.at(-1) === quote))
1771
1765
  parser.fail(line, "LNG1109", "expected a string literal");
1772
- const decoded = value
1773
- .slice(1, -1)
1766
+ const decode = (text) => text
1774
1767
  .replace(/\\n/g, "\n")
1775
1768
  .replace(/\\t/g, "\t")
1776
- .replace(/\\(['"\\])/g, "$1");
1769
+ .replace(/\\(['"\\$])/g, "$1");
1777
1770
  const parts = [];
1778
- let at = 0;
1779
- const regex = /\$\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[\d+\])?)\}/g;
1780
- let match;
1781
- while ((match = regex.exec(decoded))) {
1782
- parts.push({ text: decoded.slice(at, match.index), value: match[1] });
1783
- at = match.index + match[0].length;
1771
+ let at = 1;
1772
+ for (let index = 1; index < value.length - 1; index += 1) {
1773
+ if (value[index] === "\\") { index += 1; continue; }
1774
+ if (!value.startsWith("${", index)) continue;
1775
+ const end = interpolationEnd(value, index + 2);
1776
+ if (end < 0 || end >= value.length - 1)
1777
+ parser.fail(line, "LNG1267", "unterminated string interpolation");
1778
+ const source = value.slice(index + 2, end).trim();
1779
+ if (!source) parser.fail(line, "LNG1267", "string interpolation requires an expression");
1780
+ parts.push({ text: decode(value.slice(at, index)), value: expr(normalizeKebabIdentifiers(source), line, parser) });
1781
+ index = end;
1782
+ at = end + 1;
1784
1783
  }
1785
- parts.push({ text: decoded.slice(at), value: null });
1784
+ parts.push({ text: decode(value.slice(at, -1)), value: null });
1786
1785
  return parts;
1787
1786
  }
1788
1787
 
@@ -2482,6 +2481,7 @@ function validate(program, imports = []) {
2482
2481
  const expressionReferences = (node, references) => {
2483
2482
  if (!node || typeof node !== "object") return;
2484
2483
  if (node.type === "raw") {
2484
+ if (["true", "false", "null", "this"].includes(node.value)) return;
2485
2485
  const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(node.value);
2486
2486
  if (match) references.add(match[1]);
2487
2487
  return;
@@ -2519,10 +2519,7 @@ function validate(program, imports = []) {
2519
2519
  if (node.type === "string" || node.type === "translate") {
2520
2520
  node.literal
2521
2521
  .filter((part) => part.value)
2522
- .forEach((part) => {
2523
- const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(part.value);
2524
- if (match) references.add(match[1]);
2525
- });
2522
+ .forEach((part) => expressionReferences(part.value, references));
2526
2523
  return;
2527
2524
  }
2528
2525
  Object.values(node).forEach((value) => {
@@ -3461,6 +3458,8 @@ function emit(program, moduleImports = [], includeTests = false) {
3461
3458
  flags.normalize && "normalize as __normalize",
3462
3459
  flags.finite && "finite as __finite",
3463
3460
  flags.number && "requireNumber as __requireNumber",
3461
+ flags.numeric && "requireNumeric as __requireNumeric",
3462
+ flags.numericAdd && "numericAdd as __numericAdd",
3464
3463
  flags.call && "call as __call",
3465
3464
  flags.callMethod && "callMethod as __callMethod",
3466
3465
  flags.boundary && "boundary as __boundary",
@@ -3712,7 +3711,7 @@ function collectEmitLocals(body, names = new Set(), nested = false) {
3712
3711
  return names;
3713
3712
  }
3714
3713
  function emitDecimalExpression(node, flags) {
3715
- const operand = (value) => {
3714
+ const operand = (value, numericOnly = false) => {
3716
3715
  if (value.type === "decimal") return emitExpr(value, flags);
3717
3716
  if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
3718
3717
  return emitDecimalExpression(value, flags);
@@ -3722,13 +3721,15 @@ function emitDecimalExpression(node, flags) {
3722
3721
  }
3723
3722
  if (value.type === "raw" && value.value.startsWith("$")) return emitExpr(value, flags);
3724
3723
  flags.decimal = true;
3725
- return `__decimal(${emitExpr(value, flags)})`;
3724
+ if (numericOnly) flags.numeric = true;
3725
+ const emitted = emitExpr(value, flags);
3726
+ return `__decimal(${numericOnly ? `__requireNumeric(${emitted})` : emitted})`;
3726
3727
  };
3727
3728
  if (node.type !== "binary" || !["+", "-", "*", "/", "%", "**"].includes(node.operator))
3728
3729
  return operand(node);
3729
3730
  flags.decimal = true;
3730
3731
  const methods = { "+": "plus", "-": "minus", "*": "times", "/": "div", "%": "mod", "**": "pow" };
3731
- return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
3732
+ return `${operand(node.left, node.operator === "+")}.${methods[node.operator]}(${operand(node.right, node.operator === "+")})`;
3732
3733
  }
3733
3734
  function needsLoopLabel(body, nestedLoopDepth = 0) {
3734
3735
  for (const node of body) {
@@ -3800,6 +3801,10 @@ function emitStatement(node, depth, declared, flags) {
3800
3801
  if (node.type === "indexAssign") {
3801
3802
  const target = `${emitReceiverName(node.receiver, flags)}${(node.indexes ?? [node.index]).map((index) => `[${emitExpr(index, flags)}]`).join("")}`;
3802
3803
  const value = emitExpr(node.value, flags);
3804
+ if (node.operator === "+") {
3805
+ flags.numericAdd = true;
3806
+ return `${pad}${target} = __numericAdd(${target}, ${value});\n`;
3807
+ }
3803
3808
  if (node.operator) flags.finite = true;
3804
3809
  return `${pad}${target} = ${node.operator ? `__finite(${target} ${node.operator} ${value})` : value};\n`;
3805
3810
  }
@@ -3813,7 +3818,10 @@ function emitStatement(node, depth, declared, flags) {
3813
3818
  }
3814
3819
  if (node.type === "superConstructor")
3815
3820
  return `${pad}super(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")});\n`;
3816
- if (node.type === "increment") return `${pad}${node.name} += 1;\n`;
3821
+ if (node.type === "increment") {
3822
+ flags.numericAdd = true;
3823
+ return `${pad}${node.name} = __numericAdd(${node.name}, 1);\n`;
3824
+ }
3817
3825
  if (node.type === "add")
3818
3826
  return `${pad}${node.target}.push(${emitExpr(node.value, flags)});\n`;
3819
3827
  if (node.type === "return")
@@ -4209,8 +4217,16 @@ function emitExpr(node, flags) {
4209
4217
  : value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
4210
4218
  ? `__decimal(${JSON.stringify(value.value)})`
4211
4219
  : emitExpr(value, flags);
4220
+ if (node.operator === "+") {
4221
+ flags.numericAdd = true;
4222
+ return `__numericAdd(${operand(node.left)}, ${operand(node.right)})`;
4223
+ }
4212
4224
  return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
4213
4225
  }
4226
+ if (node.operator === "+") {
4227
+ flags.numericAdd = true;
4228
+ return `__numericAdd(${emitExpr(node.left, flags)}, ${emitExpr(node.right, flags)})`;
4229
+ }
4214
4230
  const value = `${emitExpr(node.left, flags)} ${operators[node.operator] ?? node.operator} ${emitExpr(node.right, flags)}`;
4215
4231
  if (["+", "-", "*", "/", "%", "**"].includes(node.operator)) {
4216
4232
  flags.finite = true;
@@ -4299,7 +4315,7 @@ function emitExpr(node, flags) {
4299
4315
  .join("");
4300
4316
  const values = node.literal
4301
4317
  .filter((item) => item.value)
4302
- .map((item) => item.value);
4318
+ .map((item) => emitExpr(item.value, flags));
4303
4319
  return `__translate(${JSON.stringify(template)}, [${values.join(", ")}])`;
4304
4320
  }
4305
4321
  function emitBoolean(node, flags) {
@@ -4308,7 +4324,7 @@ function emitBoolean(node, flags) {
4308
4324
  }
4309
4325
  function emitString(parts, flags) {
4310
4326
  if (parts.length === 1) return JSON.stringify(parts[0].text);
4311
- return `\`${parts.map((item) => `${item.text.replaceAll("`", "\\`")}${item.value === null ? "" : `\${${emitReceiverName(item.value, flags)}}`}`).join("")}\``;
4327
+ return `\`${parts.map((item) => `${JSON.stringify(item.text).slice(1, -1).replaceAll("`", "\\`").replaceAll("${", "\\${")}${item.value === null ? "" : `\${${emitExpr(item.value, flags)}}`}`).join("")}\``;
4312
4328
  }
4313
4329
  function spaces(line) {
4314
4330
  return line.length - line.trimStart().length;
package/src/runtime.mjs CHANGED
@@ -530,6 +530,19 @@ export function requireNumber(value) {
530
530
  return finite(value);
531
531
  }
532
532
 
533
+ export function requireNumeric(value) {
534
+ if (isDecimal(value) && value.isFinite()) return value;
535
+ return requireNumber(value);
536
+ }
537
+
538
+ export function numericAdd(left, right) {
539
+ requireNumeric(left);
540
+ requireNumeric(right);
541
+ if (isDecimal(left) || isDecimal(right))
542
+ return decimal(left).plus(right);
543
+ return finite(left + right);
544
+ }
545
+
533
546
  export function call(value, ...arguments_) {
534
547
  if (typeof value !== "function") throw new Error("NotCallable");
535
548
  return normalize(value(...arguments_));