@lango-project/lango-js 0.1.5 → 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 +1 -1
- package/src/cli.mjs +13 -8
- package/src/compiler.mjs +93 -68
- package/src/runtime.mjs +22 -6
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -24,7 +24,9 @@ const buildOptions = (arguments_) => {
|
|
|
24
24
|
}
|
|
25
25
|
const value = arguments_[index + 1];
|
|
26
26
|
if (!value) return null;
|
|
27
|
-
if (option === "--
|
|
27
|
+
if (option === "--target")
|
|
28
|
+
profile.target = value;
|
|
29
|
+
else if (option === "--decimal-division-scale")
|
|
28
30
|
profile.decimal_division_scale = Number(value);
|
|
29
31
|
else if (option === "--decimal-division-rounding")
|
|
30
32
|
profile.decimal_division_rounding = Decimal.Rounding[value] ?? value;
|
|
@@ -34,21 +36,22 @@ const buildOptions = (arguments_) => {
|
|
|
34
36
|
return { outputPath: arguments_[1], profile };
|
|
35
37
|
};
|
|
36
38
|
const build = command === "build" && inputPath ? buildOptions(rest) : null;
|
|
37
|
-
const runOptions = (arguments_) => {
|
|
38
|
-
const options = { database: null, catalog: null };
|
|
39
|
+
const runOptions = (arguments_, allowBuildDir = false) => {
|
|
40
|
+
const options = { database: null, catalog: null, buildDir: null };
|
|
39
41
|
let index = 0;
|
|
40
|
-
while (arguments_[index] === "--database" || arguments_[index] === "--catalog") {
|
|
42
|
+
while (arguments_[index] === "--database" || arguments_[index] === "--catalog" || (allowBuildDir && arguments_[index] === "--build-dir")) {
|
|
41
43
|
const option = arguments_[index];
|
|
42
44
|
const value = arguments_[index + 1];
|
|
43
45
|
if (!value) return null;
|
|
44
46
|
if (option === "--database") options.database = value;
|
|
45
|
-
else options.catalog = value;
|
|
47
|
+
else if (option === "--catalog") options.catalog = value;
|
|
48
|
+
else options.buildDir = value;
|
|
46
49
|
index += 2;
|
|
47
50
|
}
|
|
48
51
|
if (arguments_[index] === "--") index += 1;
|
|
49
52
|
return { ...options, args: arguments_.slice(index) };
|
|
50
53
|
};
|
|
51
|
-
const testOptions = command === "test" && inputPath ? runOptions(rest) : null;
|
|
54
|
+
const testOptions = command === "test" && inputPath ? runOptions(rest, true) : null;
|
|
52
55
|
|
|
53
56
|
async function initProject(directory) {
|
|
54
57
|
const target = path.resolve(directory);
|
|
@@ -129,7 +132,9 @@ if (command === "init" && inputPath && rest.length === 0) {
|
|
|
129
132
|
try {
|
|
130
133
|
if (testOptions.args.length)
|
|
131
134
|
throw new Error("Lango test does not accept program arguments");
|
|
132
|
-
const directory =
|
|
135
|
+
const directory = testOptions.buildDir
|
|
136
|
+
? path.resolve(testOptions.buildDir)
|
|
137
|
+
: await mkdtemp(path.join(os.tmpdir(), "lango-test-"));
|
|
133
138
|
const outputPath = path.join(directory, "main.mjs");
|
|
134
139
|
await buildFile(path.resolve(inputPath), outputPath, new Set(), true);
|
|
135
140
|
const runtime = await import(pathToFileURL(path.join(directory, ".lango/runtime.mjs")).href);
|
|
@@ -173,6 +178,6 @@ if (command === "init" && inputPath && rest.length === 0) {
|
|
|
173
178
|
process.exitCode = error instanceof LangoError ? 1 : 2;
|
|
174
179
|
}
|
|
175
180
|
} else {
|
|
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>");
|
|
181
|
+
console.error("Usage: node src/cli.mjs init <directory>\n node src/cli.mjs build <input.lango> --out <output.mjs> [--target node|browser] [--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> [--build-dir <directory>] [--database <sqlite-file>] [--catalog <catalog.json>]\n node src/cli.mjs format <input.lango>");
|
|
177
182
|
process.exitCode = 2;
|
|
178
183
|
}
|
package/src/compiler.mjs
CHANGED
|
@@ -32,6 +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 (decimalProfile.target === "browser" && profile.executable)
|
|
36
|
+
throw new LangoError("LNG1266", 1, "browser builds cannot use --executable", inputPath);
|
|
35
37
|
const isEntryPoint = seen.size === 0;
|
|
36
38
|
if (isEntryPoint)
|
|
37
39
|
await validateModuleInitializerCycles(inputPath, outputPath);
|
|
@@ -87,6 +89,8 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
const compiled = compile(source, inputPath, imports, includeTests, decimalProfile);
|
|
92
|
+
if (decimalProfile.target === "browser" && compiled.program.jsImports.some((item) => item.specifier?.startsWith("node:")))
|
|
93
|
+
throw new LangoError("LNG1266", 1, "browser builds cannot import node: modules", inputPath);
|
|
90
94
|
let output = compiled.javascript.replace(
|
|
91
95
|
"//# sourceMappingURL=main.mjs.map",
|
|
92
96
|
`//# sourceMappingURL=${path.basename(outputPath)}.map`,
|
|
@@ -122,7 +126,9 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
|
|
|
122
126
|
"utf8",
|
|
123
127
|
);
|
|
124
128
|
const xmlVendor = new URL("./vendor/xmldom/", import.meta.url);
|
|
129
|
+
if (decimalProfile.target === "browser") return;
|
|
125
130
|
await mkdir(path.join(directory, ".lango/xmldom"), { recursive: true });
|
|
131
|
+
await writeFile(path.join(directory, ".lango/xmldom/package.json"), '{"type":"commonjs"}\n', "utf8");
|
|
126
132
|
for (const name of ["conventions.js", "dom-parser.js", "dom.js", "entities.js", "errors.js", "grammar.js", "index.js", "sax.js"])
|
|
127
133
|
await writeFile(
|
|
128
134
|
path.join(directory, ".lango/xmldom", name),
|
|
@@ -185,13 +191,16 @@ function tabDiagnostics(source, sourceName) {
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
function validateDecimalProfile(profile) {
|
|
194
|
+
const target = profile.target ?? "node";
|
|
195
|
+
if (target !== "node" && target !== "browser")
|
|
196
|
+
throw new LangoError("LNG1266", 1, `unknown build target ${target}`);
|
|
188
197
|
const decimal_division_scale = profile.decimal_division_scale ?? 6;
|
|
189
198
|
const decimal_division_rounding = profile.decimal_division_rounding ?? Decimal.Rounding.HalfEven;
|
|
190
199
|
if (!Number.isInteger(decimal_division_scale) || decimal_division_scale < 0)
|
|
191
200
|
throw new LangoError("LNG1262", 1, "decimal_division_scale must be a non-negative integer");
|
|
192
201
|
if (!Object.values(Decimal.Rounding).includes(decimal_division_rounding))
|
|
193
202
|
throw new LangoError("LNG1263", 1, `unknown decimal_division_rounding ${decimal_division_rounding}`);
|
|
194
|
-
return { decimal_division_scale, decimal_division_rounding };
|
|
203
|
+
return { decimal_division_scale, decimal_division_rounding, target };
|
|
195
204
|
}
|
|
196
205
|
|
|
197
206
|
const BASE64_VLQ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -1172,18 +1181,42 @@ function collectSqlParameters(sql) {
|
|
|
1172
1181
|
return [...new Set(names)];
|
|
1173
1182
|
}
|
|
1174
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
|
+
|
|
1175
1214
|
function topLevelBinary(text, operators, rightAssociative = false) {
|
|
1176
|
-
let quote = null;
|
|
1177
1215
|
let depth = 0;
|
|
1178
1216
|
let match = null;
|
|
1179
1217
|
for (let index = 0; index < text.length; index += 1) {
|
|
1180
1218
|
const character = text[index];
|
|
1181
|
-
if (
|
|
1182
|
-
if (character === "\\") { index += 1; continue; }
|
|
1183
|
-
if (character === quote) quote = null;
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1219
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1187
1220
|
if ("([{".includes(character)) { depth += 1; continue; }
|
|
1188
1221
|
if (")]}".includes(character)) { depth -= 1; continue; }
|
|
1189
1222
|
if (depth !== 0) continue;
|
|
@@ -1199,16 +1232,10 @@ function topLevelBinary(text, operators, rightAssociative = false) {
|
|
|
1199
1232
|
}
|
|
1200
1233
|
|
|
1201
1234
|
function curlyDelimiterDepth(text) {
|
|
1202
|
-
let quote = null;
|
|
1203
1235
|
let depth = 0;
|
|
1204
1236
|
for (let index = 0; index < text.length; index += 1) {
|
|
1205
1237
|
const character = text[index];
|
|
1206
|
-
if (
|
|
1207
|
-
if (character === "\\") { index += 1; continue; }
|
|
1208
|
-
if (character === quote) quote = null;
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1238
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1212
1239
|
if (character === "{") depth += 1;
|
|
1213
1240
|
if (character === "}") depth -= 1;
|
|
1214
1241
|
}
|
|
@@ -1219,16 +1246,14 @@ const KEBAB_MARKER = "__lango_dash__";
|
|
|
1219
1246
|
|
|
1220
1247
|
function normalizeKebabIdentifiers(source) {
|
|
1221
1248
|
let result = "";
|
|
1222
|
-
let quote = null;
|
|
1223
1249
|
for (let index = 0; index < source.length; index += 1) {
|
|
1224
1250
|
const character = source[index];
|
|
1225
|
-
if (
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1251
|
+
if (character === "'" || character === '"') {
|
|
1252
|
+
const end = skipString(source, index);
|
|
1253
|
+
result += source.slice(index, end + 1);
|
|
1254
|
+
index = end;
|
|
1229
1255
|
continue;
|
|
1230
1256
|
}
|
|
1231
|
-
if (character === "'" || character === '"') { quote = character; result += character; continue; }
|
|
1232
1257
|
const match = /^[A-Za-z_]\w*(?:-[A-Za-z_]\w*)+/.exec(source.slice(index));
|
|
1233
1258
|
if (match) {
|
|
1234
1259
|
result += match[0].replaceAll("-", KEBAB_MARKER);
|
|
@@ -1261,16 +1286,10 @@ function emitKebabPath(value) {
|
|
|
1261
1286
|
function splitTopLevel(text, separator = ", ") {
|
|
1262
1287
|
const parts = [];
|
|
1263
1288
|
let start = 0;
|
|
1264
|
-
let quote = null;
|
|
1265
1289
|
let depth = 0;
|
|
1266
1290
|
for (let index = 0; index < text.length; index += 1) {
|
|
1267
1291
|
const character = text[index];
|
|
1268
|
-
if (
|
|
1269
|
-
if (character === "\\") { index += 1; continue; }
|
|
1270
|
-
if (character === quote) quote = null;
|
|
1271
|
-
continue;
|
|
1272
|
-
}
|
|
1273
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1292
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1274
1293
|
if ("([{".includes(character)) { depth += 1; continue; }
|
|
1275
1294
|
if (")]}".includes(character)) { depth -= 1; continue; }
|
|
1276
1295
|
if (depth === 0 && text.startsWith(separator, index)) {
|
|
@@ -1285,16 +1304,10 @@ function splitTopLevel(text, separator = ", ") {
|
|
|
1285
1304
|
|
|
1286
1305
|
function parenthesizedExpression(text) {
|
|
1287
1306
|
if (!text.startsWith("(") || !text.endsWith(")")) return null;
|
|
1288
|
-
let quote = null;
|
|
1289
1307
|
let depth = 0;
|
|
1290
1308
|
for (let index = 0; index < text.length; index += 1) {
|
|
1291
1309
|
const character = text[index];
|
|
1292
|
-
if (
|
|
1293
|
-
if (character === "\\") { index += 1; continue; }
|
|
1294
|
-
if (character === quote) quote = null;
|
|
1295
|
-
continue;
|
|
1296
|
-
}
|
|
1297
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1310
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1298
1311
|
if (character === "(") depth += 1;
|
|
1299
1312
|
if (character === ")") {
|
|
1300
1313
|
depth -= 1;
|
|
@@ -1735,23 +1748,13 @@ function xmlTagEnd(source, start) {
|
|
|
1735
1748
|
}
|
|
1736
1749
|
|
|
1737
1750
|
function stripComment(line) {
|
|
1738
|
-
let quote = null;
|
|
1739
|
-
let escaped = false;
|
|
1740
1751
|
for (let index = 0; index < line.length; index += 1) {
|
|
1741
1752
|
const character = line[index];
|
|
1742
|
-
if (escaped) {
|
|
1743
|
-
escaped = false;
|
|
1744
|
-
continue;
|
|
1745
|
-
}
|
|
1746
|
-
if (quote && character === "\\") {
|
|
1747
|
-
escaped = true;
|
|
1748
|
-
continue;
|
|
1749
|
-
}
|
|
1750
1753
|
if (character === '"' || character === "'") {
|
|
1751
|
-
|
|
1754
|
+
index = skipString(line, index);
|
|
1752
1755
|
continue;
|
|
1753
1756
|
}
|
|
1754
|
-
if (
|
|
1757
|
+
if (character === "#") return line.slice(0, index).trimEnd();
|
|
1755
1758
|
}
|
|
1756
1759
|
return line;
|
|
1757
1760
|
}
|
|
@@ -1760,20 +1763,25 @@ function literal(value, line, parser) {
|
|
|
1760
1763
|
const quote = value[0];
|
|
1761
1764
|
if (!((quote === "'" || quote === '"') && value.at(-1) === quote))
|
|
1762
1765
|
parser.fail(line, "LNG1109", "expected a string literal");
|
|
1763
|
-
const
|
|
1764
|
-
.slice(1, -1)
|
|
1766
|
+
const decode = (text) => text
|
|
1765
1767
|
.replace(/\\n/g, "\n")
|
|
1766
1768
|
.replace(/\\t/g, "\t")
|
|
1767
|
-
.replace(/\\(['"
|
|
1769
|
+
.replace(/\\(['"\\$])/g, "$1");
|
|
1768
1770
|
const parts = [];
|
|
1769
|
-
let at =
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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;
|
|
1775
1783
|
}
|
|
1776
|
-
parts.push({ text:
|
|
1784
|
+
parts.push({ text: decode(value.slice(at, -1)), value: null });
|
|
1777
1785
|
return parts;
|
|
1778
1786
|
}
|
|
1779
1787
|
|
|
@@ -2473,6 +2481,7 @@ function validate(program, imports = []) {
|
|
|
2473
2481
|
const expressionReferences = (node, references) => {
|
|
2474
2482
|
if (!node || typeof node !== "object") return;
|
|
2475
2483
|
if (node.type === "raw") {
|
|
2484
|
+
if (["true", "false", "null", "this"].includes(node.value)) return;
|
|
2476
2485
|
const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(node.value);
|
|
2477
2486
|
if (match) references.add(match[1]);
|
|
2478
2487
|
return;
|
|
@@ -2510,10 +2519,7 @@ function validate(program, imports = []) {
|
|
|
2510
2519
|
if (node.type === "string" || node.type === "translate") {
|
|
2511
2520
|
node.literal
|
|
2512
2521
|
.filter((part) => part.value)
|
|
2513
|
-
.forEach((part) =>
|
|
2514
|
-
const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(part.value);
|
|
2515
|
-
if (match) references.add(match[1]);
|
|
2516
|
-
});
|
|
2522
|
+
.forEach((part) => expressionReferences(part.value, references));
|
|
2517
2523
|
return;
|
|
2518
2524
|
}
|
|
2519
2525
|
Object.values(node).forEach((value) => {
|
|
@@ -3452,6 +3458,8 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3452
3458
|
flags.normalize && "normalize as __normalize",
|
|
3453
3459
|
flags.finite && "finite as __finite",
|
|
3454
3460
|
flags.number && "requireNumber as __requireNumber",
|
|
3461
|
+
flags.numeric && "requireNumeric as __requireNumeric",
|
|
3462
|
+
flags.numericAdd && "numericAdd as __numericAdd",
|
|
3455
3463
|
flags.call && "call as __call",
|
|
3456
3464
|
flags.callMethod && "callMethod as __callMethod",
|
|
3457
3465
|
flags.boundary && "boundary as __boundary",
|
|
@@ -3703,7 +3711,7 @@ function collectEmitLocals(body, names = new Set(), nested = false) {
|
|
|
3703
3711
|
return names;
|
|
3704
3712
|
}
|
|
3705
3713
|
function emitDecimalExpression(node, flags) {
|
|
3706
|
-
const operand = (value) => {
|
|
3714
|
+
const operand = (value, numericOnly = false) => {
|
|
3707
3715
|
if (value.type === "decimal") return emitExpr(value, flags);
|
|
3708
3716
|
if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
|
|
3709
3717
|
return emitDecimalExpression(value, flags);
|
|
@@ -3713,13 +3721,15 @@ function emitDecimalExpression(node, flags) {
|
|
|
3713
3721
|
}
|
|
3714
3722
|
if (value.type === "raw" && value.value.startsWith("$")) return emitExpr(value, flags);
|
|
3715
3723
|
flags.decimal = true;
|
|
3716
|
-
|
|
3724
|
+
if (numericOnly) flags.numeric = true;
|
|
3725
|
+
const emitted = emitExpr(value, flags);
|
|
3726
|
+
return `__decimal(${numericOnly ? `__requireNumeric(${emitted})` : emitted})`;
|
|
3717
3727
|
};
|
|
3718
3728
|
if (node.type !== "binary" || !["+", "-", "*", "/", "%", "**"].includes(node.operator))
|
|
3719
3729
|
return operand(node);
|
|
3720
3730
|
flags.decimal = true;
|
|
3721
3731
|
const methods = { "+": "plus", "-": "minus", "*": "times", "/": "div", "%": "mod", "**": "pow" };
|
|
3722
|
-
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 === "+")})`;
|
|
3723
3733
|
}
|
|
3724
3734
|
function needsLoopLabel(body, nestedLoopDepth = 0) {
|
|
3725
3735
|
for (const node of body) {
|
|
@@ -3791,6 +3801,10 @@ function emitStatement(node, depth, declared, flags) {
|
|
|
3791
3801
|
if (node.type === "indexAssign") {
|
|
3792
3802
|
const target = `${emitReceiverName(node.receiver, flags)}${(node.indexes ?? [node.index]).map((index) => `[${emitExpr(index, flags)}]`).join("")}`;
|
|
3793
3803
|
const value = emitExpr(node.value, flags);
|
|
3804
|
+
if (node.operator === "+") {
|
|
3805
|
+
flags.numericAdd = true;
|
|
3806
|
+
return `${pad}${target} = __numericAdd(${target}, ${value});\n`;
|
|
3807
|
+
}
|
|
3794
3808
|
if (node.operator) flags.finite = true;
|
|
3795
3809
|
return `${pad}${target} = ${node.operator ? `__finite(${target} ${node.operator} ${value})` : value};\n`;
|
|
3796
3810
|
}
|
|
@@ -3804,7 +3818,10 @@ function emitStatement(node, depth, declared, flags) {
|
|
|
3804
3818
|
}
|
|
3805
3819
|
if (node.type === "superConstructor")
|
|
3806
3820
|
return `${pad}super(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")});\n`;
|
|
3807
|
-
if (node.type === "increment")
|
|
3821
|
+
if (node.type === "increment") {
|
|
3822
|
+
flags.numericAdd = true;
|
|
3823
|
+
return `${pad}${node.name} = __numericAdd(${node.name}, 1);\n`;
|
|
3824
|
+
}
|
|
3808
3825
|
if (node.type === "add")
|
|
3809
3826
|
return `${pad}${node.target}.push(${emitExpr(node.value, flags)});\n`;
|
|
3810
3827
|
if (node.type === "return")
|
|
@@ -4200,8 +4217,16 @@ function emitExpr(node, flags) {
|
|
|
4200
4217
|
: value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
|
|
4201
4218
|
? `__decimal(${JSON.stringify(value.value)})`
|
|
4202
4219
|
: emitExpr(value, flags);
|
|
4220
|
+
if (node.operator === "+") {
|
|
4221
|
+
flags.numericAdd = true;
|
|
4222
|
+
return `__numericAdd(${operand(node.left)}, ${operand(node.right)})`;
|
|
4223
|
+
}
|
|
4203
4224
|
return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
|
|
4204
4225
|
}
|
|
4226
|
+
if (node.operator === "+") {
|
|
4227
|
+
flags.numericAdd = true;
|
|
4228
|
+
return `__numericAdd(${emitExpr(node.left, flags)}, ${emitExpr(node.right, flags)})`;
|
|
4229
|
+
}
|
|
4205
4230
|
const value = `${emitExpr(node.left, flags)} ${operators[node.operator] ?? node.operator} ${emitExpr(node.right, flags)}`;
|
|
4206
4231
|
if (["+", "-", "*", "/", "%", "**"].includes(node.operator)) {
|
|
4207
4232
|
flags.finite = true;
|
|
@@ -4290,16 +4315,16 @@ function emitExpr(node, flags) {
|
|
|
4290
4315
|
.join("");
|
|
4291
4316
|
const values = node.literal
|
|
4292
4317
|
.filter((item) => item.value)
|
|
4293
|
-
.map((item) => item.value);
|
|
4318
|
+
.map((item) => emitExpr(item.value, flags));
|
|
4294
4319
|
return `__translate(${JSON.stringify(template)}, [${values.join(", ")}])`;
|
|
4295
4320
|
}
|
|
4296
4321
|
function emitBoolean(node, flags) {
|
|
4297
4322
|
flags.boolean = true;
|
|
4298
4323
|
return `__requireBoolean(${emitExpr(node, flags)})`;
|
|
4299
4324
|
}
|
|
4300
|
-
function emitString(parts) {
|
|
4325
|
+
function emitString(parts, flags) {
|
|
4301
4326
|
if (parts.length === 1) return JSON.stringify(parts[0].text);
|
|
4302
|
-
return `\`${parts.map((item) => `${item.text.replaceAll("`", "\\`")}${item.value === null ? "" : `\${${item.value}}`}`).join("")}\``;
|
|
4327
|
+
return `\`${parts.map((item) => `${JSON.stringify(item.text).slice(1, -1).replaceAll("`", "\\`").replaceAll("${", "\\${")}${item.value === null ? "" : `\${${emitExpr(item.value, flags)}}`}`).join("")}\``;
|
|
4303
4328
|
}
|
|
4304
4329
|
function spaces(line) {
|
|
4305
4330
|
return line.length - line.trimStart().length;
|
package/src/runtime.mjs
CHANGED
|
@@ -15,6 +15,7 @@ const ROUNDING_MODES = Object.freeze({
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
export function runtimeSource(profile = {}) {
|
|
18
|
+
const browser = profile.target === "browser";
|
|
18
19
|
const scale = profile.decimal_division_scale ?? 6;
|
|
19
20
|
const rounding = profile.decimal_division_rounding ?? Decimal.Rounding.HalfEven;
|
|
20
21
|
if (!Number.isInteger(scale) || scale < 0)
|
|
@@ -22,9 +23,9 @@ export function runtimeSource(profile = {}) {
|
|
|
22
23
|
if (!(rounding in ROUNDING_MODES))
|
|
23
24
|
throw new Error(`unknown decimal_division_rounding ${rounding}`);
|
|
24
25
|
return `import BigNumber from "./bignumber.mjs";
|
|
25
|
-
import { createRequire } from "node:module";
|
|
26
|
+
${browser ? 'const DOMParser = globalThis.DOMParser;' : `import { createRequire } from "node:module";
|
|
26
27
|
const require = createRequire(import.meta.url);
|
|
27
|
-
const { DOMParser } = require("./xmldom/index.js")
|
|
28
|
+
const { DOMParser } = require("./xmldom/index.js");`}
|
|
28
29
|
|
|
29
30
|
const Decimal = BigNumber.clone({
|
|
30
31
|
DECIMAL_PLACES: ${scale},
|
|
@@ -207,6 +208,8 @@ export function xml2obj(xml) {
|
|
|
207
208
|
throw new Error(\`XmlParseError: \${error.message}\`);
|
|
208
209
|
}
|
|
209
210
|
if (errors.length) throw new Error(\`XmlParseError: \${errors[0]}\`);
|
|
211
|
+
if (${browser} && document.getElementsByTagName("parsererror").length)
|
|
212
|
+
throw new Error("XmlParseError: invalid XML document");
|
|
210
213
|
if (!document.documentElement)
|
|
211
214
|
throw new Error("XmlParseError: XML must contain one root tag");
|
|
212
215
|
return new Tag(document.documentElement);
|
|
@@ -270,10 +273,10 @@ export function println(value) {
|
|
|
270
273
|
}
|
|
271
274
|
|
|
272
275
|
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); },
|
|
276
|
+
debug(value) { ${browser ? 'console.debug(String(value)); return null;' : 'return writeLog(process.stdout, "DEBUG", value);'} },
|
|
277
|
+
info(value) { ${browser ? 'console.info(String(value)); return null;' : 'return writeLog(process.stdout, "INFO", value);'} },
|
|
278
|
+
warn(value) { ${browser ? 'console.warn(String(value)); return null;' : 'return writeLog(process.stderr, "WARN", value);'} },
|
|
279
|
+
err(value) { ${browser ? 'console.error(String(value)); return null;' : 'return writeLog(process.stderr, "ERROR", value);'} },
|
|
277
280
|
});
|
|
278
281
|
|
|
279
282
|
function writeLog(stream, level, value) {
|
|
@@ -527,6 +530,19 @@ export function requireNumber(value) {
|
|
|
527
530
|
return finite(value);
|
|
528
531
|
}
|
|
529
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
|
+
|
|
530
546
|
export function call(value, ...arguments_) {
|
|
531
547
|
if (typeof value !== "function") throw new Error("NotCallable");
|
|
532
548
|
return normalize(value(...arguments_));
|