arc-lang 0.6.1 → 0.6.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/dist/interpreter.js +256 -52
- package/dist/parser.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/interpreter.js
CHANGED
|
@@ -202,12 +202,12 @@ function makePrelude(env) {
|
|
|
202
202
|
reduce: (list, fn, init) => {
|
|
203
203
|
if (!Array.isArray(list))
|
|
204
204
|
throw new Error("reduce expects a list");
|
|
205
|
-
let acc = init
|
|
205
|
+
let acc = init !== undefined ? init : list[0];
|
|
206
206
|
const start = init !== undefined ? 0 : 1;
|
|
207
207
|
for (let i = start; i < list.length; i++) {
|
|
208
208
|
acc = callFn(fn, [acc, list[i]]);
|
|
209
209
|
}
|
|
210
|
-
return acc;
|
|
210
|
+
return acc === undefined ? null : acc;
|
|
211
211
|
},
|
|
212
212
|
fold: (list, init, fn) => {
|
|
213
213
|
if (!Array.isArray(list))
|
|
@@ -249,7 +249,7 @@ function makePrelude(env) {
|
|
|
249
249
|
return 0;
|
|
250
250
|
return list.reduce((a, b) => a + (typeof b === "number" ? b : 0), 0);
|
|
251
251
|
},
|
|
252
|
-
flat: (list) => Array.isArray(list) ? list.flat() : list,
|
|
252
|
+
flat: (list) => Array.isArray(list) ? list.flat(Infinity) : list,
|
|
253
253
|
zip: (a, b) => {
|
|
254
254
|
if (!Array.isArray(a) || !Array.isArray(b))
|
|
255
255
|
return [];
|
|
@@ -262,7 +262,7 @@ function makePrelude(env) {
|
|
|
262
262
|
return list.map((v, i) => [i, v]);
|
|
263
263
|
},
|
|
264
264
|
trim: (s) => typeof s === "string" ? s.trim() : s,
|
|
265
|
-
split: (s, sep) => typeof s === "string" ? s.split(sep) : [],
|
|
265
|
+
split: (s, sep) => typeof s === "string" ? (sep === "" ? [...s] : s.split(sep)) : [],
|
|
266
266
|
join: (list, sep) => Array.isArray(list) ? list.map(toStr).join(sep) : "",
|
|
267
267
|
upper: (s) => typeof s === "string" ? s.toUpperCase() : s,
|
|
268
268
|
lower: (s) => typeof s === "string" ? s.toLowerCase() : s,
|
|
@@ -329,7 +329,7 @@ function makePrelude(env) {
|
|
|
329
329
|
return [...a, ...b];
|
|
330
330
|
return toStr(a) + toStr(b);
|
|
331
331
|
},
|
|
332
|
-
chars: (s) => typeof s === "string" ? s
|
|
332
|
+
chars: (s) => typeof s === "string" ? [...s] : [],
|
|
333
333
|
repeat: (s, n) => typeof s === "string" ? s.repeat(n) : s,
|
|
334
334
|
slice: (v, start, end) => {
|
|
335
335
|
const endVal = (end === null || end === undefined) ? undefined : end;
|
|
@@ -360,10 +360,13 @@ function makePrelude(env) {
|
|
|
360
360
|
return nodeCrypto.createHash(algorithm).update(data == null ? "" : data).digest("hex");
|
|
361
361
|
},
|
|
362
362
|
crypto_hmac: (algorithm, key, data) => {
|
|
363
|
-
return nodeCrypto.createHmac(algorithm, key).update(data).digest("hex");
|
|
363
|
+
return nodeCrypto.createHmac(algorithm, key == null ? "" : key).update(data == null ? "" : data).digest("hex");
|
|
364
364
|
},
|
|
365
365
|
crypto_random_bytes: (n) => {
|
|
366
|
-
const
|
|
366
|
+
const count = n;
|
|
367
|
+
if (count <= 0)
|
|
368
|
+
return [];
|
|
369
|
+
const buf = nodeCrypto.randomBytes(count);
|
|
367
370
|
return Array.from(buf);
|
|
368
371
|
},
|
|
369
372
|
crypto_random_int: (min, max) => {
|
|
@@ -373,7 +376,7 @@ function makePrelude(env) {
|
|
|
373
376
|
},
|
|
374
377
|
crypto_uuid: () => nodeCrypto.randomUUID(),
|
|
375
378
|
crypto_encode_base64: (s) => Buffer.from(s == null ? "" : s).toString("base64"),
|
|
376
|
-
crypto_decode_base64: (s) => Buffer.from(s, "base64").toString("utf-8"),
|
|
379
|
+
crypto_decode_base64: (s) => s == null ? "" : Buffer.from(s, "base64").toString("utf-8"),
|
|
377
380
|
// --- net natives ---
|
|
378
381
|
net_url_parse: (url) => {
|
|
379
382
|
try {
|
|
@@ -404,7 +407,20 @@ function makePrelude(env) {
|
|
|
404
407
|
const str = s.startsWith("?") ? s.slice(1) : s;
|
|
405
408
|
const params = new URLSearchParams(str);
|
|
406
409
|
const m = new Map();
|
|
407
|
-
params.forEach((v, k) =>
|
|
410
|
+
params.forEach((v, k) => {
|
|
411
|
+
const existing = m.get(k);
|
|
412
|
+
if (existing !== undefined) {
|
|
413
|
+
if (Array.isArray(existing)) {
|
|
414
|
+
existing.push(v);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
m.set(k, [existing, v]);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
m.set(k, v);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
408
424
|
return { __map: true, entries: m };
|
|
409
425
|
},
|
|
410
426
|
net_query_stringify: (map) => {
|
|
@@ -423,7 +439,7 @@ function makePrelude(env) {
|
|
|
423
439
|
// IPv4
|
|
424
440
|
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(str);
|
|
425
441
|
if (v4)
|
|
426
|
-
return v4.slice(1).every(n => parseInt(n) <= 255);
|
|
442
|
+
return v4.slice(1).every(n => parseInt(n) <= 255 && (n === "0" || !n.startsWith("0")));
|
|
427
443
|
// IPv6 (simplified check)
|
|
428
444
|
if (str.includes(":")) {
|
|
429
445
|
try {
|
|
@@ -469,41 +485,41 @@ function makePrelude(env) {
|
|
|
469
485
|
const errMap = new Map();
|
|
470
486
|
errMap.set("ok", false);
|
|
471
487
|
errMap.set("error", "argument is not a function");
|
|
472
|
-
return { __map: true, entries: errMap };
|
|
488
|
+
return { __map: true, __result: true, entries: errMap };
|
|
473
489
|
}
|
|
474
490
|
const result = typeof fn === "function" ? fn() : callFn(fn, []);
|
|
475
491
|
const okMap = new Map();
|
|
476
492
|
okMap.set("ok", true);
|
|
477
493
|
okMap.set("value", result);
|
|
478
|
-
return { __map: true, entries: okMap };
|
|
494
|
+
return { __map: true, __result: true, entries: okMap };
|
|
479
495
|
}
|
|
480
496
|
catch (e) {
|
|
481
497
|
const errMap = new Map();
|
|
482
498
|
errMap.set("ok", false);
|
|
483
499
|
errMap.set("error", e.message ?? toStr(e));
|
|
484
|
-
return { __map: true, entries: errMap };
|
|
500
|
+
return { __map: true, __result: true, entries: errMap };
|
|
485
501
|
}
|
|
486
502
|
},
|
|
487
503
|
Ok: (v) => {
|
|
488
504
|
const m = new Map();
|
|
489
505
|
m.set("ok", true);
|
|
490
506
|
m.set("value", v);
|
|
491
|
-
return { __map: true, entries: m };
|
|
507
|
+
return { __map: true, __result: true, entries: m };
|
|
492
508
|
},
|
|
493
509
|
Err: (e) => {
|
|
494
510
|
const m = new Map();
|
|
495
511
|
m.set("ok", false);
|
|
496
512
|
m.set("error", e);
|
|
497
|
-
return { __map: true, entries: m };
|
|
513
|
+
return { __map: true, __result: true, entries: m };
|
|
498
514
|
},
|
|
499
515
|
is_ok: (v) => {
|
|
500
|
-
if (v && typeof v === "object" && "__map" in v) {
|
|
516
|
+
if (v && typeof v === "object" && "__map" in v && "__result" in v) {
|
|
501
517
|
return v.entries.get("ok") === true;
|
|
502
518
|
}
|
|
503
519
|
return false;
|
|
504
520
|
},
|
|
505
521
|
is_err: (v) => {
|
|
506
|
-
if (v && typeof v === "object" && "__map" in v) {
|
|
522
|
+
if (v && typeof v === "object" && "__map" in v && "__result" in v) {
|
|
507
523
|
return v.entries.get("ok") === false;
|
|
508
524
|
}
|
|
509
525
|
return false;
|
|
@@ -534,7 +550,7 @@ function makePrelude(env) {
|
|
|
534
550
|
const newMap = new Map();
|
|
535
551
|
newMap.set("ok", true);
|
|
536
552
|
newMap.set("value", result);
|
|
537
|
-
return { __map: true, entries: newMap };
|
|
553
|
+
return { __map: true, __result: true, entries: newMap };
|
|
538
554
|
}
|
|
539
555
|
return v; // pass Err through
|
|
540
556
|
}
|
|
@@ -566,7 +582,12 @@ function makePrelude(env) {
|
|
|
566
582
|
},
|
|
567
583
|
regex_try_new: (pattern) => {
|
|
568
584
|
try {
|
|
569
|
-
|
|
585
|
+
const p = pattern;
|
|
586
|
+
// ReDoS protection: same check as regex_new
|
|
587
|
+
if (/(\+|\*|\{)\s*(\+|\*|\{)/.test(p) || /\([^)]*(\+|\*)\)[+*]/.test(p)) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
new RegExp(p);
|
|
570
591
|
const m = new Map();
|
|
571
592
|
m.set("pattern", pattern);
|
|
572
593
|
m.set("__regex", true);
|
|
@@ -721,8 +742,14 @@ function makePrelude(env) {
|
|
|
721
742
|
si++;
|
|
722
743
|
}
|
|
723
744
|
}
|
|
724
|
-
const
|
|
725
|
-
|
|
745
|
+
const d = new Date(year, month - 1, day, hour, min, sec);
|
|
746
|
+
const result = d.getTime();
|
|
747
|
+
if (isNaN(result))
|
|
748
|
+
return null;
|
|
749
|
+
// Validate that JS didn't roll over an invalid date (e.g. Feb 30 → Mar 2)
|
|
750
|
+
if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day)
|
|
751
|
+
return null;
|
|
752
|
+
return result;
|
|
726
753
|
},
|
|
727
754
|
__builtin_date_format: (ts, fmt) => {
|
|
728
755
|
const d = new Date(ts);
|
|
@@ -881,11 +908,14 @@ function makePrelude(env) {
|
|
|
881
908
|
if (dangerous.test(cmd)) {
|
|
882
909
|
throw new Error(`Potentially unsafe command (injection risk): ${cmd}`);
|
|
883
910
|
}
|
|
884
|
-
return execSync(cmd, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
911
|
+
return execSync(cmd, { encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
885
912
|
}
|
|
886
913
|
catch (e) {
|
|
887
914
|
if (e.message?.includes("injection risk"))
|
|
888
915
|
throw e;
|
|
916
|
+
// Return stderr if available
|
|
917
|
+
if (e.stderr)
|
|
918
|
+
return e.stderr.toString().trim();
|
|
889
919
|
return null;
|
|
890
920
|
}
|
|
891
921
|
}
|
|
@@ -905,15 +935,16 @@ function makePrelude(env) {
|
|
|
905
935
|
// --- prompt natives ---
|
|
906
936
|
case "prompt.token_count": {
|
|
907
937
|
const text = String(args[0] ?? "");
|
|
908
|
-
return Math.ceil(text.length / 4);
|
|
938
|
+
return Math.ceil([...text].length / 4);
|
|
909
939
|
}
|
|
910
940
|
case "prompt.token_truncate": {
|
|
911
941
|
const text = String(args[0] ?? "");
|
|
912
942
|
const maxTokens = args[1];
|
|
943
|
+
const codepoints = [...text];
|
|
913
944
|
const maxChars = maxTokens * 4;
|
|
914
|
-
if (
|
|
945
|
+
if (codepoints.length <= maxChars)
|
|
915
946
|
return text;
|
|
916
|
-
return
|
|
947
|
+
return codepoints.slice(0, maxChars).join("");
|
|
917
948
|
}
|
|
918
949
|
case "prompt.chunk": {
|
|
919
950
|
const text = String(args[0] ?? "");
|
|
@@ -921,9 +952,10 @@ function makePrelude(env) {
|
|
|
921
952
|
if (maxTokens <= 0)
|
|
922
953
|
return [];
|
|
923
954
|
const chunkSize = maxTokens * 4;
|
|
955
|
+
const codepoints = [...text];
|
|
924
956
|
const chunks = [];
|
|
925
|
-
for (let i = 0; i <
|
|
926
|
-
chunks.push(
|
|
957
|
+
for (let i = 0; i < codepoints.length; i += chunkSize) {
|
|
958
|
+
chunks.push(codepoints.slice(i, i + chunkSize).join(""));
|
|
927
959
|
}
|
|
928
960
|
return chunks.length > 0 ? chunks : [""];
|
|
929
961
|
}
|
|
@@ -999,7 +1031,12 @@ function makePrelude(env) {
|
|
|
999
1031
|
}
|
|
1000
1032
|
case "store.entries": {
|
|
1001
1033
|
const s = args[0];
|
|
1002
|
-
return Object.entries(s.data).map(([k, v]) =>
|
|
1034
|
+
return Object.entries(s.data).map(([k, v]) => {
|
|
1035
|
+
const m = new Map();
|
|
1036
|
+
m.set("key", k);
|
|
1037
|
+
m.set("value", v);
|
|
1038
|
+
return { __map: true, entries: m };
|
|
1039
|
+
});
|
|
1003
1040
|
}
|
|
1004
1041
|
case "store.clear": {
|
|
1005
1042
|
const s = args[0];
|
|
@@ -1044,6 +1081,10 @@ function makePrelude(env) {
|
|
|
1044
1081
|
case "math.cbrt": return Math.cbrt(args[0]);
|
|
1045
1082
|
case "math.pow": return Math.pow(args[0], args[1]);
|
|
1046
1083
|
case "math.ceil": return Math.ceil(args[0]);
|
|
1084
|
+
case "json.from_codepoint": {
|
|
1085
|
+
const hex = args[0];
|
|
1086
|
+
return String.fromCodePoint(parseInt(hex, 16));
|
|
1087
|
+
}
|
|
1047
1088
|
case "csv.parse": {
|
|
1048
1089
|
const text = args[0].trim();
|
|
1049
1090
|
if (text === "")
|
|
@@ -1079,7 +1120,7 @@ function makePrelude(env) {
|
|
|
1079
1120
|
i++;
|
|
1080
1121
|
}
|
|
1081
1122
|
}
|
|
1082
|
-
row.push(value
|
|
1123
|
+
row.push(value);
|
|
1083
1124
|
if (i < text.length && text[i] === ',') {
|
|
1084
1125
|
i++;
|
|
1085
1126
|
}
|
|
@@ -1203,10 +1244,37 @@ function makePrelude(env) {
|
|
|
1203
1244
|
}
|
|
1204
1245
|
else {
|
|
1205
1246
|
children = parseHTML(html.slice(i));
|
|
1247
|
+
// Bug #13: use depth counter to find correct closing tag for nested same-name elements
|
|
1206
1248
|
const closeTag = `</${tag}>`;
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1249
|
+
const openTag = `<${tag}`;
|
|
1250
|
+
let searchPos = i;
|
|
1251
|
+
let depth = 1;
|
|
1252
|
+
while (searchPos < html.length && depth > 0) {
|
|
1253
|
+
const nextOpen = html.toLowerCase().indexOf(openTag, searchPos);
|
|
1254
|
+
const nextClose = html.toLowerCase().indexOf(closeTag, searchPos);
|
|
1255
|
+
if (nextClose === -1) {
|
|
1256
|
+
searchPos = html.length;
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
1260
|
+
// Check it's a real open tag (followed by space, >, or /)
|
|
1261
|
+
const afterOpen = html[nextOpen + openTag.length];
|
|
1262
|
+
if (afterOpen === ' ' || afterOpen === '>' || afterOpen === '/' || afterOpen === undefined) {
|
|
1263
|
+
depth++;
|
|
1264
|
+
}
|
|
1265
|
+
searchPos = nextOpen + openTag.length;
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
depth--;
|
|
1269
|
+
if (depth === 0) {
|
|
1270
|
+
searchPos = nextClose;
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
searchPos = nextClose + closeTag.length;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (depth === 0) {
|
|
1277
|
+
i = searchPos + closeTag.length;
|
|
1210
1278
|
}
|
|
1211
1279
|
else {
|
|
1212
1280
|
i = html.length;
|
|
@@ -1382,13 +1450,40 @@ function makePrelude(env) {
|
|
|
1382
1450
|
// --- YAML natives ---
|
|
1383
1451
|
case "yaml.parse": {
|
|
1384
1452
|
const src = args[0];
|
|
1453
|
+
function flowSplit(inner) {
|
|
1454
|
+
const parts = [];
|
|
1455
|
+
let depth = 0, start = 0, inStr = false, strCh = "";
|
|
1456
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1457
|
+
const ch = inner[i];
|
|
1458
|
+
if (inStr) {
|
|
1459
|
+
if (ch === strCh && inner[i - 1] !== "\\")
|
|
1460
|
+
inStr = false;
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (ch === '"' || ch === "'") {
|
|
1464
|
+
inStr = true;
|
|
1465
|
+
strCh = ch;
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (ch === "{" || ch === "[")
|
|
1469
|
+
depth++;
|
|
1470
|
+
else if (ch === "}" || ch === "]")
|
|
1471
|
+
depth--;
|
|
1472
|
+
else if (ch === "," && depth === 0) {
|
|
1473
|
+
parts.push(inner.slice(start, i));
|
|
1474
|
+
start = i + 1;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (inner.slice(start).trim() !== "")
|
|
1478
|
+
parts.push(inner.slice(start));
|
|
1479
|
+
return parts;
|
|
1480
|
+
}
|
|
1385
1481
|
function yamlParseFlowMapping(s) {
|
|
1386
1482
|
const inner = s.slice(1, -1).trim();
|
|
1387
1483
|
const m = new Map();
|
|
1388
1484
|
if (inner === "")
|
|
1389
1485
|
return m;
|
|
1390
|
-
|
|
1391
|
-
const parts = inner.split(",");
|
|
1486
|
+
const parts = flowSplit(inner);
|
|
1392
1487
|
for (const part of parts) {
|
|
1393
1488
|
const colon = part.indexOf(":");
|
|
1394
1489
|
if (colon > 0) {
|
|
@@ -1401,10 +1496,16 @@ function makePrelude(env) {
|
|
|
1401
1496
|
const inner = s.slice(1, -1).trim();
|
|
1402
1497
|
if (inner === "")
|
|
1403
1498
|
return [];
|
|
1404
|
-
return inner
|
|
1499
|
+
return flowSplit(inner).map(x => yamlParseValue(x.trim()));
|
|
1405
1500
|
}
|
|
1406
1501
|
function yamlParseValue(s) {
|
|
1407
1502
|
s = s.trim();
|
|
1503
|
+
// Strip inline comments from unquoted values (bug #1)
|
|
1504
|
+
if (s !== "" && !s.startsWith('"') && !s.startsWith("'") && !s.startsWith("{") && !s.startsWith("[")) {
|
|
1505
|
+
const hashIdx = s.indexOf(" #");
|
|
1506
|
+
if (hashIdx >= 0)
|
|
1507
|
+
s = s.slice(0, hashIdx).trim();
|
|
1508
|
+
}
|
|
1408
1509
|
if (s === "" || s === "~" || s === "null")
|
|
1409
1510
|
return null;
|
|
1410
1511
|
if (s === "true" || s === "True" || s === "TRUE" || s === "yes" || s === "Yes" || s === "YES" || s === "on" || s === "On" || s === "ON")
|
|
@@ -1592,7 +1693,7 @@ function makePrelude(env) {
|
|
|
1592
1693
|
return result;
|
|
1593
1694
|
}
|
|
1594
1695
|
}
|
|
1595
|
-
const rawLines = src.split("\n").filter(l => !l.trimStart().startsWith("#"));
|
|
1696
|
+
const rawLines = src.split("\n").filter(l => !l.trimStart().startsWith("#") && l.trim() !== "---" && l.trim() !== "...");
|
|
1596
1697
|
return toArcValue(yamlParse(rawLines, 0));
|
|
1597
1698
|
}
|
|
1598
1699
|
case "yaml.stringify": {
|
|
@@ -1647,12 +1748,16 @@ function makePrelude(env) {
|
|
|
1647
1748
|
let current = root;
|
|
1648
1749
|
const lines = src.split("\n");
|
|
1649
1750
|
function tomlStripComment(s) {
|
|
1650
|
-
// Strip inline comments (not inside strings)
|
|
1751
|
+
// Strip inline comments (not inside strings), handling escapes properly
|
|
1651
1752
|
let inStr = false;
|
|
1652
1753
|
let strCh = "";
|
|
1653
1754
|
for (let i = 0; i < s.length; i++) {
|
|
1654
1755
|
if (inStr) {
|
|
1655
|
-
if (s[i] ===
|
|
1756
|
+
if (s[i] === "\\") {
|
|
1757
|
+
i++;
|
|
1758
|
+
continue;
|
|
1759
|
+
} // skip escaped char (handles \\ and \")
|
|
1760
|
+
if (s[i] === strCh)
|
|
1656
1761
|
inStr = false;
|
|
1657
1762
|
continue;
|
|
1658
1763
|
}
|
|
@@ -1666,12 +1771,51 @@ function makePrelude(env) {
|
|
|
1666
1771
|
}
|
|
1667
1772
|
return s;
|
|
1668
1773
|
}
|
|
1774
|
+
function tomlExtractString(s) {
|
|
1775
|
+
// Walk char by char to find closing quote, handling \" escapes
|
|
1776
|
+
let result = "";
|
|
1777
|
+
for (let i = 1; i < s.length; i++) {
|
|
1778
|
+
if (s[i] === "\\") {
|
|
1779
|
+
if (i + 1 < s.length) {
|
|
1780
|
+
const next = s[i + 1];
|
|
1781
|
+
if (next === '"') {
|
|
1782
|
+
result += '"';
|
|
1783
|
+
i++;
|
|
1784
|
+
}
|
|
1785
|
+
else if (next === '\\') {
|
|
1786
|
+
result += '\\';
|
|
1787
|
+
i++;
|
|
1788
|
+
}
|
|
1789
|
+
else if (next === 'n') {
|
|
1790
|
+
result += '\n';
|
|
1791
|
+
i++;
|
|
1792
|
+
}
|
|
1793
|
+
else if (next === 't') {
|
|
1794
|
+
result += '\t';
|
|
1795
|
+
i++;
|
|
1796
|
+
}
|
|
1797
|
+
else {
|
|
1798
|
+
result += s[i];
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
else if (s[i] === '"') {
|
|
1803
|
+
return result;
|
|
1804
|
+
}
|
|
1805
|
+
else {
|
|
1806
|
+
result += s[i];
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return result;
|
|
1810
|
+
}
|
|
1669
1811
|
function tomlParseValue(s) {
|
|
1670
1812
|
s = tomlStripComment(s).trim();
|
|
1671
1813
|
if (s === "true")
|
|
1672
1814
|
return true;
|
|
1673
1815
|
if (s === "false")
|
|
1674
1816
|
return false;
|
|
1817
|
+
if (s === "True" || s === "False")
|
|
1818
|
+
return s; // bug #6: reject capitalized booleans
|
|
1675
1819
|
if (s === "inf" || s === "+inf")
|
|
1676
1820
|
return Infinity;
|
|
1677
1821
|
if (s === "-inf")
|
|
@@ -1690,14 +1834,28 @@ function makePrelude(env) {
|
|
|
1690
1834
|
if (end !== -1)
|
|
1691
1835
|
return s.slice(3, end).replace(/^\n/, "");
|
|
1692
1836
|
}
|
|
1693
|
-
if (s.startsWith('"')
|
|
1694
|
-
return s
|
|
1837
|
+
if (s.startsWith('"'))
|
|
1838
|
+
return tomlExtractString(s); // bug #3: handle escaped quotes
|
|
1695
1839
|
if (s.startsWith("'") && s.endsWith("'"))
|
|
1696
1840
|
return s.slice(1, -1);
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1841
|
+
// bug #7: ISO 8601 datetime
|
|
1842
|
+
if (/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?)?/.test(s)) {
|
|
1843
|
+
const t = new Date(s).getTime();
|
|
1844
|
+
if (!isNaN(t))
|
|
1845
|
+
return t;
|
|
1846
|
+
}
|
|
1847
|
+
// bug #8: hex/octal/binary integers
|
|
1848
|
+
if (/^0x[0-9a-fA-F_]+$/.test(s))
|
|
1849
|
+
return parseInt(s.replace(/_/g, ""), 16);
|
|
1850
|
+
if (/^0o[0-7_]+$/.test(s))
|
|
1851
|
+
return parseInt(s.replace(/_/g, "").replace("0o", ""), 8);
|
|
1852
|
+
if (/^0b[01_]+$/.test(s))
|
|
1853
|
+
return parseInt(s.replace(/_/g, "").replace("0b", ""), 2);
|
|
1854
|
+
// bug #8: integers with underscores
|
|
1855
|
+
if (/^[+-]?\d[\d_]*$/.test(s))
|
|
1856
|
+
return parseInt(s.replace(/_/g, ""), 10);
|
|
1857
|
+
if (/^[+-]?\d[\d_]*\.[\d_]+$/.test(s))
|
|
1858
|
+
return parseFloat(s.replace(/_/g, ""));
|
|
1701
1859
|
if (s.startsWith("["))
|
|
1702
1860
|
return tomlParseArray(s);
|
|
1703
1861
|
if (s.startsWith("{"))
|
|
@@ -1787,6 +1945,7 @@ function makePrelude(env) {
|
|
|
1787
1945
|
}
|
|
1788
1946
|
return cur;
|
|
1789
1947
|
}
|
|
1948
|
+
const seenSections = new Set(); // bug #9
|
|
1790
1949
|
for (let li = 0; li < lines.length; li++) {
|
|
1791
1950
|
const trimmed = lines[li].trim();
|
|
1792
1951
|
if (trimmed === "" || trimmed.startsWith("#"))
|
|
@@ -1806,7 +1965,11 @@ function makePrelude(env) {
|
|
|
1806
1965
|
}
|
|
1807
1966
|
const secMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
1808
1967
|
if (secMatch) {
|
|
1809
|
-
const
|
|
1968
|
+
const secKey = secMatch[1].trim();
|
|
1969
|
+
if (seenSections.has(secKey))
|
|
1970
|
+
throw new Error(`Duplicate TOML section: [${secKey}]`);
|
|
1971
|
+
seenSections.add(secKey);
|
|
1972
|
+
const keys = secKey.split(".").map((k) => k.trim());
|
|
1810
1973
|
current = ensurePath(root, keys);
|
|
1811
1974
|
continue;
|
|
1812
1975
|
}
|
|
@@ -1836,6 +1999,31 @@ function makePrelude(env) {
|
|
|
1836
1999
|
valStr += "\n" + lines[li];
|
|
1837
2000
|
}
|
|
1838
2001
|
}
|
|
2002
|
+
// Bug #5: Handle multiline arrays
|
|
2003
|
+
if (valStr.startsWith("[") && !valStr.startsWith("[[")) {
|
|
2004
|
+
let depth = 0;
|
|
2005
|
+
for (const ch of valStr) {
|
|
2006
|
+
if (ch === "[")
|
|
2007
|
+
depth++;
|
|
2008
|
+
else if (ch === "]")
|
|
2009
|
+
depth--;
|
|
2010
|
+
}
|
|
2011
|
+
while (depth > 0 && li + 1 < lines.length) {
|
|
2012
|
+
li++;
|
|
2013
|
+
const cont = lines[li].trim();
|
|
2014
|
+
if (cont === "" || cont.startsWith("#")) {
|
|
2015
|
+
valStr += " ";
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
valStr += " " + cont;
|
|
2019
|
+
for (const ch of cont) {
|
|
2020
|
+
if (ch === "[")
|
|
2021
|
+
depth++;
|
|
2022
|
+
else if (ch === "]")
|
|
2023
|
+
depth--;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
1839
2027
|
const val = tomlParseValue(valStr);
|
|
1840
2028
|
// Expand dotted keys: a.b.c = 1 → nested maps
|
|
1841
2029
|
if (key.includes(".")) {
|
|
@@ -1965,6 +2153,16 @@ function makePrelude(env) {
|
|
|
1965
2153
|
const level = args[0];
|
|
1966
2154
|
const msg = args[1];
|
|
1967
2155
|
const fields = args[2];
|
|
2156
|
+
// Bug #12: validate level
|
|
2157
|
+
const validLogLevels = ["debug", "info", "warn", "error", "fatal"];
|
|
2158
|
+
if (!validLogLevels.includes(level)) {
|
|
2159
|
+
throw new ArcRuntimeError(`Invalid log level '${level}'. Must be one of: ${validLogLevels.join(", ")}`, { code: ErrorCode.UNDEFINED_VARIABLE });
|
|
2160
|
+
}
|
|
2161
|
+
// Bug #11: check level threshold
|
|
2162
|
+
const li = validLogLevels.indexOf(level);
|
|
2163
|
+
const mi = validLogLevels.indexOf(globalThis.__arc_log_level ?? "debug");
|
|
2164
|
+
if (li < mi)
|
|
2165
|
+
return null;
|
|
1968
2166
|
const obj = {
|
|
1969
2167
|
timestamp: new Date().toISOString(),
|
|
1970
2168
|
level: level,
|
|
@@ -2105,7 +2303,7 @@ function makePrelude(env) {
|
|
|
2105
2303
|
};
|
|
2106
2304
|
function callFn(fn, args) {
|
|
2107
2305
|
if (fn && typeof fn === "object" && "__fn" in fn) {
|
|
2108
|
-
if (++callDepth >
|
|
2306
|
+
if (++callDepth > 2000) {
|
|
2109
2307
|
callDepth = 0;
|
|
2110
2308
|
throw new ArcRuntimeError("Maximum call stack depth exceeded");
|
|
2111
2309
|
}
|
|
@@ -2325,7 +2523,7 @@ function evalExpr(expr, env) {
|
|
|
2325
2523
|
}
|
|
2326
2524
|
else if (callee && typeof callee === "object" && "__fn" in callee) {
|
|
2327
2525
|
let fn = callee;
|
|
2328
|
-
if (++callDepth >
|
|
2526
|
+
if (++callDepth > 2000) {
|
|
2329
2527
|
callDepth = 0;
|
|
2330
2528
|
throw new ArcRuntimeError("Maximum call stack depth exceeded");
|
|
2331
2529
|
}
|
|
@@ -2334,7 +2532,7 @@ function evalExpr(expr, env) {
|
|
|
2334
2532
|
let tcoIterations = 0;
|
|
2335
2533
|
try {
|
|
2336
2534
|
tailLoop: while (true) {
|
|
2337
|
-
if (++tcoIterations >
|
|
2535
|
+
if (++tcoIterations > 2000) {
|
|
2338
2536
|
callDepth--;
|
|
2339
2537
|
throw new ArcRuntimeError("Maximum call stack depth exceeded");
|
|
2340
2538
|
}
|
|
@@ -2411,10 +2609,16 @@ function evalExpr(expr, env) {
|
|
|
2411
2609
|
case "IndexExpr": {
|
|
2412
2610
|
const obj = evalExpr(expr.object, env);
|
|
2413
2611
|
const idx = evalExpr(expr.index, env);
|
|
2414
|
-
if (typeof obj === "string" && typeof idx === "number")
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2612
|
+
if (typeof obj === "string" && typeof idx === "number") {
|
|
2613
|
+
if (idx !== Math.floor(idx))
|
|
2614
|
+
throw new ArcRuntimeError("String index must be an integer");
|
|
2615
|
+
let i = idx < 0 ? obj.length + idx : idx;
|
|
2616
|
+
return i >= 0 && i < obj.length ? obj.charAt(i) : null;
|
|
2617
|
+
}
|
|
2618
|
+
if (Array.isArray(obj) && typeof idx === "number") {
|
|
2619
|
+
let i = idx < 0 ? obj.length + idx : idx;
|
|
2620
|
+
return obj[i] ?? null;
|
|
2621
|
+
}
|
|
2418
2622
|
if (obj && typeof obj === "object" && "__map" in obj) {
|
|
2419
2623
|
const key = typeof idx === "string" ? idx : toStr(idx);
|
|
2420
2624
|
return obj.entries.get(key) ?? null;
|
package/dist/parser.js
CHANGED
|
@@ -169,6 +169,8 @@ export class Parser {
|
|
|
169
169
|
if (this.at(TokenType.DotDotDot)) {
|
|
170
170
|
this.advance();
|
|
171
171
|
const pname = this.expect(TokenType.Ident).value;
|
|
172
|
+
if (params.includes(pname))
|
|
173
|
+
throw new ParseError(`Duplicate parameter name '${pname}'`, this.loc());
|
|
172
174
|
params.push(pname);
|
|
173
175
|
richParams.push({ name: pname, rest: true });
|
|
174
176
|
hasRichParams = true;
|
|
@@ -176,6 +178,8 @@ export class Parser {
|
|
|
176
178
|
break;
|
|
177
179
|
}
|
|
178
180
|
const pname = this.expect(TokenType.Ident).value;
|
|
181
|
+
if (params.includes(pname))
|
|
182
|
+
throw new ParseError(`Duplicate parameter name '${pname}'`, this.loc());
|
|
179
183
|
params.push(pname);
|
|
180
184
|
if (this.at(TokenType.Assign)) {
|
|
181
185
|
this.advance();
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED