arc-lang 0.6.0 → 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 +617 -91
- package/dist/lexer.js +5 -0
- 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
|
@@ -170,6 +170,7 @@ function jsToArc(v) {
|
|
|
170
170
|
}
|
|
171
171
|
return String(v);
|
|
172
172
|
}
|
|
173
|
+
let callDepth = 0;
|
|
173
174
|
function resolveAsync(v) {
|
|
174
175
|
if (v && typeof v === "object" && "__async" in v) {
|
|
175
176
|
return v.thunk();
|
|
@@ -201,12 +202,12 @@ function makePrelude(env) {
|
|
|
201
202
|
reduce: (list, fn, init) => {
|
|
202
203
|
if (!Array.isArray(list))
|
|
203
204
|
throw new Error("reduce expects a list");
|
|
204
|
-
let acc = init
|
|
205
|
+
let acc = init !== undefined ? init : list[0];
|
|
205
206
|
const start = init !== undefined ? 0 : 1;
|
|
206
207
|
for (let i = start; i < list.length; i++) {
|
|
207
208
|
acc = callFn(fn, [acc, list[i]]);
|
|
208
209
|
}
|
|
209
|
-
return acc;
|
|
210
|
+
return acc === undefined ? null : acc;
|
|
210
211
|
},
|
|
211
212
|
fold: (list, init, fn) => {
|
|
212
213
|
if (!Array.isArray(list))
|
|
@@ -248,7 +249,7 @@ function makePrelude(env) {
|
|
|
248
249
|
return 0;
|
|
249
250
|
return list.reduce((a, b) => a + (typeof b === "number" ? b : 0), 0);
|
|
250
251
|
},
|
|
251
|
-
flat: (list) => Array.isArray(list) ? list.flat() : list,
|
|
252
|
+
flat: (list) => Array.isArray(list) ? list.flat(Infinity) : list,
|
|
252
253
|
zip: (a, b) => {
|
|
253
254
|
if (!Array.isArray(a) || !Array.isArray(b))
|
|
254
255
|
return [];
|
|
@@ -261,7 +262,7 @@ function makePrelude(env) {
|
|
|
261
262
|
return list.map((v, i) => [i, v]);
|
|
262
263
|
},
|
|
263
264
|
trim: (s) => typeof s === "string" ? s.trim() : s,
|
|
264
|
-
split: (s, sep) => typeof s === "string" ? s.split(sep) : [],
|
|
265
|
+
split: (s, sep) => typeof s === "string" ? (sep === "" ? [...s] : s.split(sep)) : [],
|
|
265
266
|
join: (list, sep) => Array.isArray(list) ? list.map(toStr).join(sep) : "",
|
|
266
267
|
upper: (s) => typeof s === "string" ? s.toUpperCase() : s,
|
|
267
268
|
lower: (s) => typeof s === "string" ? s.toLowerCase() : s,
|
|
@@ -328,7 +329,7 @@ function makePrelude(env) {
|
|
|
328
329
|
return [...a, ...b];
|
|
329
330
|
return toStr(a) + toStr(b);
|
|
330
331
|
},
|
|
331
|
-
chars: (s) => typeof s === "string" ? s
|
|
332
|
+
chars: (s) => typeof s === "string" ? [...s] : [],
|
|
332
333
|
repeat: (s, n) => typeof s === "string" ? s.repeat(n) : s,
|
|
333
334
|
slice: (v, start, end) => {
|
|
334
335
|
const endVal = (end === null || end === undefined) ? undefined : end;
|
|
@@ -356,13 +357,16 @@ function makePrelude(env) {
|
|
|
356
357
|
time_ms: () => Date.now(),
|
|
357
358
|
// --- crypto natives ---
|
|
358
359
|
crypto_hash: (algorithm, data) => {
|
|
359
|
-
return nodeCrypto.createHash(algorithm).update(data).digest("hex");
|
|
360
|
+
return nodeCrypto.createHash(algorithm).update(data == null ? "" : data).digest("hex");
|
|
360
361
|
},
|
|
361
362
|
crypto_hmac: (algorithm, key, data) => {
|
|
362
|
-
return nodeCrypto.createHmac(algorithm, key).update(data).digest("hex");
|
|
363
|
+
return nodeCrypto.createHmac(algorithm, key == null ? "" : key).update(data == null ? "" : data).digest("hex");
|
|
363
364
|
},
|
|
364
365
|
crypto_random_bytes: (n) => {
|
|
365
|
-
const
|
|
366
|
+
const count = n;
|
|
367
|
+
if (count <= 0)
|
|
368
|
+
return [];
|
|
369
|
+
const buf = nodeCrypto.randomBytes(count);
|
|
366
370
|
return Array.from(buf);
|
|
367
371
|
},
|
|
368
372
|
crypto_random_int: (min, max) => {
|
|
@@ -371,8 +375,8 @@ function makePrelude(env) {
|
|
|
371
375
|
return nodeCrypto.randomInt(lo, hi + 1);
|
|
372
376
|
},
|
|
373
377
|
crypto_uuid: () => nodeCrypto.randomUUID(),
|
|
374
|
-
crypto_encode_base64: (s) => Buffer.from(s).toString("base64"),
|
|
375
|
-
crypto_decode_base64: (s) => Buffer.from(s, "base64").toString("utf-8"),
|
|
378
|
+
crypto_encode_base64: (s) => Buffer.from(s == null ? "" : s).toString("base64"),
|
|
379
|
+
crypto_decode_base64: (s) => s == null ? "" : Buffer.from(s, "base64").toString("utf-8"),
|
|
376
380
|
// --- net natives ---
|
|
377
381
|
net_url_parse: (url) => {
|
|
378
382
|
try {
|
|
@@ -391,12 +395,32 @@ function makePrelude(env) {
|
|
|
391
395
|
}
|
|
392
396
|
},
|
|
393
397
|
net_url_encode: (s) => encodeURIComponent(s),
|
|
394
|
-
net_url_decode: (s) =>
|
|
398
|
+
net_url_decode: (s) => { try {
|
|
399
|
+
return decodeURIComponent(s);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return null;
|
|
403
|
+
} },
|
|
395
404
|
net_query_parse: (s) => {
|
|
405
|
+
if (s == null)
|
|
406
|
+
return { __map: true, entries: new Map() };
|
|
396
407
|
const str = s.startsWith("?") ? s.slice(1) : s;
|
|
397
408
|
const params = new URLSearchParams(str);
|
|
398
409
|
const m = new Map();
|
|
399
|
-
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
|
+
});
|
|
400
424
|
return { __map: true, entries: m };
|
|
401
425
|
},
|
|
402
426
|
net_query_stringify: (map) => {
|
|
@@ -409,11 +433,13 @@ function makePrelude(env) {
|
|
|
409
433
|
return "";
|
|
410
434
|
},
|
|
411
435
|
net_ip_is_valid: (s) => {
|
|
436
|
+
if (s == null)
|
|
437
|
+
return false;
|
|
412
438
|
const str = s;
|
|
413
439
|
// IPv4
|
|
414
440
|
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(str);
|
|
415
441
|
if (v4)
|
|
416
|
-
return v4.slice(1).every(n => parseInt(n) <= 255);
|
|
442
|
+
return v4.slice(1).every(n => parseInt(n) <= 255 && (n === "0" || !n.startsWith("0")));
|
|
417
443
|
// IPv6 (simplified check)
|
|
418
444
|
if (str.includes(":")) {
|
|
419
445
|
try {
|
|
@@ -455,40 +481,45 @@ function makePrelude(env) {
|
|
|
455
481
|
},
|
|
456
482
|
error_try: (fn) => {
|
|
457
483
|
try {
|
|
458
|
-
|
|
459
|
-
|
|
484
|
+
if (typeof fn !== "function" && !(fn && typeof fn === "object" && "__fn" in fn)) {
|
|
485
|
+
const errMap = new Map();
|
|
486
|
+
errMap.set("ok", false);
|
|
487
|
+
errMap.set("error", "argument is not a function");
|
|
488
|
+
return { __map: true, __result: true, entries: errMap };
|
|
489
|
+
}
|
|
490
|
+
const result = typeof fn === "function" ? fn() : callFn(fn, []);
|
|
460
491
|
const okMap = new Map();
|
|
461
492
|
okMap.set("ok", true);
|
|
462
493
|
okMap.set("value", result);
|
|
463
|
-
return { __map: true, entries: okMap };
|
|
494
|
+
return { __map: true, __result: true, entries: okMap };
|
|
464
495
|
}
|
|
465
496
|
catch (e) {
|
|
466
497
|
const errMap = new Map();
|
|
467
498
|
errMap.set("ok", false);
|
|
468
499
|
errMap.set("error", e.message ?? toStr(e));
|
|
469
|
-
return { __map: true, entries: errMap };
|
|
500
|
+
return { __map: true, __result: true, entries: errMap };
|
|
470
501
|
}
|
|
471
502
|
},
|
|
472
503
|
Ok: (v) => {
|
|
473
504
|
const m = new Map();
|
|
474
505
|
m.set("ok", true);
|
|
475
506
|
m.set("value", v);
|
|
476
|
-
return { __map: true, entries: m };
|
|
507
|
+
return { __map: true, __result: true, entries: m };
|
|
477
508
|
},
|
|
478
509
|
Err: (e) => {
|
|
479
510
|
const m = new Map();
|
|
480
511
|
m.set("ok", false);
|
|
481
512
|
m.set("error", e);
|
|
482
|
-
return { __map: true, entries: m };
|
|
513
|
+
return { __map: true, __result: true, entries: m };
|
|
483
514
|
},
|
|
484
515
|
is_ok: (v) => {
|
|
485
|
-
if (v && typeof v === "object" && "__map" in v) {
|
|
516
|
+
if (v && typeof v === "object" && "__map" in v && "__result" in v) {
|
|
486
517
|
return v.entries.get("ok") === true;
|
|
487
518
|
}
|
|
488
519
|
return false;
|
|
489
520
|
},
|
|
490
521
|
is_err: (v) => {
|
|
491
|
-
if (v && typeof v === "object" && "__map" in v) {
|
|
522
|
+
if (v && typeof v === "object" && "__map" in v && "__result" in v) {
|
|
492
523
|
return v.entries.get("ok") === false;
|
|
493
524
|
}
|
|
494
525
|
return false;
|
|
@@ -519,7 +550,7 @@ function makePrelude(env) {
|
|
|
519
550
|
const newMap = new Map();
|
|
520
551
|
newMap.set("ok", true);
|
|
521
552
|
newMap.set("value", result);
|
|
522
|
-
return { __map: true, entries: newMap };
|
|
553
|
+
return { __map: true, __result: true, entries: newMap };
|
|
523
554
|
}
|
|
524
555
|
return v; // pass Err through
|
|
525
556
|
}
|
|
@@ -551,7 +582,12 @@ function makePrelude(env) {
|
|
|
551
582
|
},
|
|
552
583
|
regex_try_new: (pattern) => {
|
|
553
584
|
try {
|
|
554
|
-
|
|
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);
|
|
555
591
|
const m = new Map();
|
|
556
592
|
m.set("pattern", pattern);
|
|
557
593
|
m.set("__regex", true);
|
|
@@ -664,7 +700,8 @@ function makePrelude(env) {
|
|
|
664
700
|
const fmt = format;
|
|
665
701
|
// Support ISO format and custom format tokens
|
|
666
702
|
if (fmt === "ISO" || fmt === "iso") {
|
|
667
|
-
|
|
703
|
+
const t = new Date(s).getTime();
|
|
704
|
+
return isNaN(t) ? null : t;
|
|
668
705
|
}
|
|
669
706
|
// Parse using format tokens: YYYY, MM, DD, hh, mm, ss
|
|
670
707
|
let year = 2000, month = 1, day = 1, hour = 0, min = 0, sec = 0;
|
|
@@ -705,21 +742,28 @@ function makePrelude(env) {
|
|
|
705
742
|
si++;
|
|
706
743
|
}
|
|
707
744
|
}
|
|
708
|
-
|
|
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;
|
|
709
753
|
},
|
|
710
754
|
__builtin_date_format: (ts, fmt) => {
|
|
711
755
|
const d = new Date(ts);
|
|
712
756
|
let result = fmt;
|
|
713
|
-
result = result.
|
|
714
|
-
result = result.
|
|
715
|
-
result = result.
|
|
716
|
-
result = result.
|
|
717
|
-
result = result.
|
|
718
|
-
result = result.
|
|
757
|
+
result = result.replaceAll("YYYY", String(d.getFullYear()));
|
|
758
|
+
result = result.replaceAll("MM", String(d.getMonth() + 1).padStart(2, "0"));
|
|
759
|
+
result = result.replaceAll("DD", String(d.getDate()).padStart(2, "0"));
|
|
760
|
+
result = result.replaceAll("hh", String(d.getHours()).padStart(2, "0"));
|
|
761
|
+
result = result.replaceAll("mm", String(d.getMinutes()).padStart(2, "0"));
|
|
762
|
+
result = result.replaceAll("ss", String(d.getSeconds()).padStart(2, "0"));
|
|
719
763
|
return result;
|
|
720
764
|
},
|
|
721
765
|
__builtin_date_to_iso: (ts) => new Date(ts).toISOString(),
|
|
722
|
-
__builtin_date_from_iso: (s) => new Date(s).getTime(),
|
|
766
|
+
__builtin_date_from_iso: (s) => { const t = new Date(s).getTime(); return isNaN(t) ? null : t; },
|
|
723
767
|
// --- os natives ---
|
|
724
768
|
__native: (name, ...args) => {
|
|
725
769
|
const cmd = name;
|
|
@@ -737,6 +781,13 @@ function makePrelude(env) {
|
|
|
737
781
|
entries.set(k, toArcValue(val));
|
|
738
782
|
return { __map: true, entries };
|
|
739
783
|
}
|
|
784
|
+
if (typeof v === "object" && v !== null && !("__map" in v)) {
|
|
785
|
+
// Convert plain objects to MapValue to prevent re-evaluation as Arc code
|
|
786
|
+
const entries = new Map();
|
|
787
|
+
for (const [k, val] of Object.entries(v))
|
|
788
|
+
entries.set(k, toArcValue(val));
|
|
789
|
+
return { __map: true, entries };
|
|
790
|
+
}
|
|
740
791
|
return v;
|
|
741
792
|
}
|
|
742
793
|
// Convert Arc MapValue back to raw Maps for stringifying
|
|
@@ -774,7 +825,6 @@ function makePrelude(env) {
|
|
|
774
825
|
case "os.temp_dir": return nodeOs.tmpdir();
|
|
775
826
|
case "os.list_dir": {
|
|
776
827
|
try {
|
|
777
|
-
const fs = require("fs");
|
|
778
828
|
return nodeFs.readdirSync(args[0]);
|
|
779
829
|
}
|
|
780
830
|
catch {
|
|
@@ -783,7 +833,6 @@ function makePrelude(env) {
|
|
|
783
833
|
}
|
|
784
834
|
case "os.is_file": {
|
|
785
835
|
try {
|
|
786
|
-
const fs = require("fs");
|
|
787
836
|
return nodeFs.statSync(args[0]).isFile();
|
|
788
837
|
}
|
|
789
838
|
catch {
|
|
@@ -792,7 +841,6 @@ function makePrelude(env) {
|
|
|
792
841
|
}
|
|
793
842
|
case "os.is_dir": {
|
|
794
843
|
try {
|
|
795
|
-
const fs = require("fs");
|
|
796
844
|
return nodeFs.statSync(args[0]).isDirectory();
|
|
797
845
|
}
|
|
798
846
|
catch {
|
|
@@ -801,7 +849,6 @@ function makePrelude(env) {
|
|
|
801
849
|
}
|
|
802
850
|
case "os.mkdir": {
|
|
803
851
|
try {
|
|
804
|
-
const fs = require("fs");
|
|
805
852
|
nodeFs.mkdirSync(args[0], { recursive: true });
|
|
806
853
|
return true;
|
|
807
854
|
}
|
|
@@ -811,7 +858,6 @@ function makePrelude(env) {
|
|
|
811
858
|
}
|
|
812
859
|
case "os.rmdir": {
|
|
813
860
|
try {
|
|
814
|
-
const fs = require("fs");
|
|
815
861
|
nodeFs.rmdirSync(args[0]);
|
|
816
862
|
return true;
|
|
817
863
|
}
|
|
@@ -821,7 +867,6 @@ function makePrelude(env) {
|
|
|
821
867
|
}
|
|
822
868
|
case "os.remove": {
|
|
823
869
|
try {
|
|
824
|
-
const fs = require("fs");
|
|
825
870
|
nodeFs.unlinkSync(args[0]);
|
|
826
871
|
return true;
|
|
827
872
|
}
|
|
@@ -831,7 +876,6 @@ function makePrelude(env) {
|
|
|
831
876
|
}
|
|
832
877
|
case "os.rename": {
|
|
833
878
|
try {
|
|
834
|
-
const fs = require("fs");
|
|
835
879
|
nodeFs.renameSync(args[0], args[1]);
|
|
836
880
|
return true;
|
|
837
881
|
}
|
|
@@ -841,7 +885,6 @@ function makePrelude(env) {
|
|
|
841
885
|
}
|
|
842
886
|
case "os.copy": {
|
|
843
887
|
try {
|
|
844
|
-
const fs = require("fs");
|
|
845
888
|
nodeFs.copyFileSync(args[0], args[1]);
|
|
846
889
|
return true;
|
|
847
890
|
}
|
|
@@ -851,7 +894,6 @@ function makePrelude(env) {
|
|
|
851
894
|
}
|
|
852
895
|
case "os.file_size": {
|
|
853
896
|
try {
|
|
854
|
-
const fs = require("fs");
|
|
855
897
|
return nodeFs.statSync(args[0]).size;
|
|
856
898
|
}
|
|
857
899
|
catch {
|
|
@@ -861,40 +903,59 @@ function makePrelude(env) {
|
|
|
861
903
|
case "os.exec": {
|
|
862
904
|
try {
|
|
863
905
|
const cmd = args[0];
|
|
864
|
-
// Command injection protection:
|
|
865
|
-
const dangerous =
|
|
906
|
+
// Command injection protection: block backticks and $() subshells
|
|
907
|
+
const dangerous = /`|\$\(|>\s*>|<\s*<|\beval\b|\bsource\b/;
|
|
866
908
|
if (dangerous.test(cmd)) {
|
|
867
909
|
throw new Error(`Potentially unsafe command (injection risk): ${cmd}`);
|
|
868
910
|
}
|
|
869
|
-
|
|
870
|
-
return execSync(cmd, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
911
|
+
return execSync(cmd, { encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
871
912
|
}
|
|
872
913
|
catch (e) {
|
|
873
914
|
if (e.message?.includes("injection risk"))
|
|
874
915
|
throw e;
|
|
916
|
+
// Return stderr if available
|
|
917
|
+
if (e.stderr)
|
|
918
|
+
return e.stderr.toString().trim();
|
|
875
919
|
return null;
|
|
876
920
|
}
|
|
877
921
|
}
|
|
922
|
+
case "regex.escape": {
|
|
923
|
+
const s = String(args[0] ?? "");
|
|
924
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
925
|
+
}
|
|
926
|
+
case "time.now": return Date.now();
|
|
927
|
+
case "time.sleep": {
|
|
928
|
+
const ms = args[0];
|
|
929
|
+
if (ms > 0) {
|
|
930
|
+
const end = Date.now() + ms;
|
|
931
|
+
while (Date.now() < end) { /* busy wait */ }
|
|
932
|
+
}
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
878
935
|
// --- prompt natives ---
|
|
879
936
|
case "prompt.token_count": {
|
|
880
937
|
const text = String(args[0] ?? "");
|
|
881
|
-
return Math.ceil(text.length / 4);
|
|
938
|
+
return Math.ceil([...text].length / 4);
|
|
882
939
|
}
|
|
883
940
|
case "prompt.token_truncate": {
|
|
884
941
|
const text = String(args[0] ?? "");
|
|
885
942
|
const maxTokens = args[1];
|
|
943
|
+
const codepoints = [...text];
|
|
886
944
|
const maxChars = maxTokens * 4;
|
|
887
|
-
if (
|
|
945
|
+
if (codepoints.length <= maxChars)
|
|
888
946
|
return text;
|
|
889
|
-
return
|
|
947
|
+
return codepoints.slice(0, maxChars).join("");
|
|
890
948
|
}
|
|
891
949
|
case "prompt.chunk": {
|
|
892
950
|
const text = String(args[0] ?? "");
|
|
893
951
|
const maxTokens = args[1];
|
|
952
|
+
if (maxTokens <= 0)
|
|
953
|
+
return [];
|
|
894
954
|
const chunkSize = maxTokens * 4;
|
|
955
|
+
const codepoints = [...text];
|
|
895
956
|
const chunks = [];
|
|
896
|
-
for (let i = 0; i <
|
|
897
|
-
chunks.push(
|
|
957
|
+
for (let i = 0; i < codepoints.length; i += chunkSize) {
|
|
958
|
+
chunks.push(codepoints.slice(i, i + chunkSize).join(""));
|
|
898
959
|
}
|
|
899
960
|
return chunks.length > 0 ? chunks : [""];
|
|
900
961
|
}
|
|
@@ -970,7 +1031,12 @@ function makePrelude(env) {
|
|
|
970
1031
|
}
|
|
971
1032
|
case "store.entries": {
|
|
972
1033
|
const s = args[0];
|
|
973
|
-
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
|
+
});
|
|
974
1040
|
}
|
|
975
1041
|
case "store.clear": {
|
|
976
1042
|
const s = args[0];
|
|
@@ -1013,6 +1079,64 @@ function makePrelude(env) {
|
|
|
1013
1079
|
case "math.exp": return Math.exp(args[0]);
|
|
1014
1080
|
case "math.hypot": return Math.hypot(args[0], args[1]);
|
|
1015
1081
|
case "math.cbrt": return Math.cbrt(args[0]);
|
|
1082
|
+
case "math.pow": return Math.pow(args[0], args[1]);
|
|
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
|
+
}
|
|
1088
|
+
case "csv.parse": {
|
|
1089
|
+
const text = args[0].trim();
|
|
1090
|
+
if (text === "")
|
|
1091
|
+
return [];
|
|
1092
|
+
const rows = [];
|
|
1093
|
+
let i = 0;
|
|
1094
|
+
while (i < text.length) {
|
|
1095
|
+
const row = [];
|
|
1096
|
+
while (true) {
|
|
1097
|
+
let value = "";
|
|
1098
|
+
if (i < text.length && text[i] === '"') {
|
|
1099
|
+
i++; // skip opening quote
|
|
1100
|
+
while (i < text.length) {
|
|
1101
|
+
if (text[i] === '"') {
|
|
1102
|
+
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
1103
|
+
value += '"';
|
|
1104
|
+
i += 2;
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
i++; // skip closing quote
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
value += text[i];
|
|
1113
|
+
i++;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
else {
|
|
1118
|
+
while (i < text.length && text[i] !== ',' && text[i] !== '\n' && text[i] !== '\r') {
|
|
1119
|
+
value += text[i];
|
|
1120
|
+
i++;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
row.push(value);
|
|
1124
|
+
if (i < text.length && text[i] === ',') {
|
|
1125
|
+
i++;
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// skip line ending
|
|
1132
|
+
if (i < text.length && text[i] === '\r')
|
|
1133
|
+
i++;
|
|
1134
|
+
if (i < text.length && text[i] === '\n')
|
|
1135
|
+
i++;
|
|
1136
|
+
rows.push(row);
|
|
1137
|
+
}
|
|
1138
|
+
return rows;
|
|
1139
|
+
}
|
|
1016
1140
|
// --- html natives ---
|
|
1017
1141
|
case "html.parse": {
|
|
1018
1142
|
const src = args[0];
|
|
@@ -1023,7 +1147,9 @@ function makePrelude(env) {
|
|
|
1023
1147
|
return m;
|
|
1024
1148
|
}
|
|
1025
1149
|
function decodeEntities(s) {
|
|
1026
|
-
return s.replace(
|
|
1150
|
+
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
|
1151
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
|
|
1152
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'");
|
|
1027
1153
|
}
|
|
1028
1154
|
function parseHTML(html) {
|
|
1029
1155
|
const nodes = [];
|
|
@@ -1033,6 +1159,18 @@ function makePrelude(env) {
|
|
|
1033
1159
|
if (html[i + 1] === '/') {
|
|
1034
1160
|
break;
|
|
1035
1161
|
}
|
|
1162
|
+
// Skip comments
|
|
1163
|
+
if (html.slice(i, i + 4) === '<!--') {
|
|
1164
|
+
const endComment = html.indexOf('-->', i + 4);
|
|
1165
|
+
i = endComment !== -1 ? endComment + 3 : html.length;
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
// Skip DOCTYPE
|
|
1169
|
+
if (html.slice(i, i + 9).toLowerCase() === '<!doctype') {
|
|
1170
|
+
const endDoc = html.indexOf('>', i);
|
|
1171
|
+
i = endDoc !== -1 ? endDoc + 1 : html.length;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1036
1174
|
const tagMatch = html.slice(i).match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
|
|
1037
1175
|
if (!tagMatch) {
|
|
1038
1176
|
nodes.push(decodeEntities(html[i]));
|
|
@@ -1088,15 +1226,59 @@ function makePrelude(env) {
|
|
|
1088
1226
|
i++;
|
|
1089
1227
|
const voidTags = new Set(['br', 'hr', 'img', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr']);
|
|
1090
1228
|
let children = [];
|
|
1229
|
+
const rawTags = new Set(['script', 'style']);
|
|
1091
1230
|
if (!selfClosing && !voidTags.has(tag)) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1231
|
+
if (rawTags.has(tag)) {
|
|
1232
|
+
// Treat script/style content as raw text — don't parse inner content
|
|
1233
|
+
const closeTag = `</${tag}>`;
|
|
1234
|
+
const closeIdx = html.toLowerCase().indexOf(closeTag, i);
|
|
1235
|
+
if (closeIdx !== -1) {
|
|
1236
|
+
const rawText = html.slice(i, closeIdx);
|
|
1237
|
+
if (rawText.trim())
|
|
1238
|
+
children = [rawText];
|
|
1239
|
+
i = closeIdx + closeTag.length;
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
i = html.length;
|
|
1243
|
+
}
|
|
1097
1244
|
}
|
|
1098
1245
|
else {
|
|
1099
|
-
|
|
1246
|
+
children = parseHTML(html.slice(i));
|
|
1247
|
+
// Bug #13: use depth counter to find correct closing tag for nested same-name elements
|
|
1248
|
+
const closeTag = `</${tag}>`;
|
|
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;
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
i = html.length;
|
|
1281
|
+
}
|
|
1100
1282
|
}
|
|
1101
1283
|
}
|
|
1102
1284
|
nodes.push(mkMap([['tag', tag], ['attrs', mkMap(attrEntries)], ['children', children]]));
|
|
@@ -1231,7 +1413,12 @@ function makePrelude(env) {
|
|
|
1231
1413
|
case "env.get": return process.env[args[0]] ?? null;
|
|
1232
1414
|
case "env.get_or": return process.env[args[0]] ?? args[1];
|
|
1233
1415
|
case "env.set": {
|
|
1234
|
-
|
|
1416
|
+
if (args[1] == null) {
|
|
1417
|
+
delete process.env[args[0]];
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
process.env[args[0]] = String(args[1]);
|
|
1421
|
+
}
|
|
1235
1422
|
return null;
|
|
1236
1423
|
}
|
|
1237
1424
|
case "env.remove": {
|
|
@@ -1263,13 +1450,67 @@ function makePrelude(env) {
|
|
|
1263
1450
|
// --- YAML natives ---
|
|
1264
1451
|
case "yaml.parse": {
|
|
1265
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
|
+
}
|
|
1481
|
+
function yamlParseFlowMapping(s) {
|
|
1482
|
+
const inner = s.slice(1, -1).trim();
|
|
1483
|
+
const m = new Map();
|
|
1484
|
+
if (inner === "")
|
|
1485
|
+
return m;
|
|
1486
|
+
const parts = flowSplit(inner);
|
|
1487
|
+
for (const part of parts) {
|
|
1488
|
+
const colon = part.indexOf(":");
|
|
1489
|
+
if (colon > 0) {
|
|
1490
|
+
m.set(part.slice(0, colon).trim(), yamlParseValue(part.slice(colon + 1).trim()));
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return m;
|
|
1494
|
+
}
|
|
1495
|
+
function yamlParseFlowSequence(s) {
|
|
1496
|
+
const inner = s.slice(1, -1).trim();
|
|
1497
|
+
if (inner === "")
|
|
1498
|
+
return [];
|
|
1499
|
+
return flowSplit(inner).map(x => yamlParseValue(x.trim()));
|
|
1500
|
+
}
|
|
1266
1501
|
function yamlParseValue(s) {
|
|
1267
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
|
+
}
|
|
1268
1509
|
if (s === "" || s === "~" || s === "null")
|
|
1269
1510
|
return null;
|
|
1270
|
-
if (s === "true" || s === "True" || s === "TRUE")
|
|
1511
|
+
if (s === "true" || s === "True" || s === "TRUE" || s === "yes" || s === "Yes" || s === "YES" || s === "on" || s === "On" || s === "ON")
|
|
1271
1512
|
return true;
|
|
1272
|
-
if (s === "false" || s === "False" || s === "FALSE")
|
|
1513
|
+
if (s === "false" || s === "False" || s === "FALSE" || s === "no" || s === "No" || s === "NO" || s === "off" || s === "Off" || s === "OFF")
|
|
1273
1514
|
return false;
|
|
1274
1515
|
if (/^-?\d+$/.test(s))
|
|
1275
1516
|
return parseInt(s, 10);
|
|
@@ -1277,6 +1518,10 @@ function makePrelude(env) {
|
|
|
1277
1518
|
return parseFloat(s);
|
|
1278
1519
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'")))
|
|
1279
1520
|
return s.slice(1, -1);
|
|
1521
|
+
if (s.startsWith("{") && s.endsWith("}"))
|
|
1522
|
+
return yamlParseFlowMapping(s);
|
|
1523
|
+
if (s.startsWith("[") && s.endsWith("]"))
|
|
1524
|
+
return yamlParseFlowSequence(s);
|
|
1280
1525
|
return s;
|
|
1281
1526
|
}
|
|
1282
1527
|
function yamlParse(lines, baseIndent) {
|
|
@@ -1300,13 +1545,36 @@ function makePrelude(env) {
|
|
|
1300
1545
|
continue;
|
|
1301
1546
|
}
|
|
1302
1547
|
const lt = line.trimStart();
|
|
1303
|
-
if (!lt.startsWith("- ")) {
|
|
1548
|
+
if (!lt.startsWith("- ") && lt !== "-") {
|
|
1304
1549
|
i++;
|
|
1305
1550
|
continue;
|
|
1306
1551
|
}
|
|
1307
|
-
const itemVal = lt.slice(2);
|
|
1552
|
+
const itemVal = lt === "-" ? "" : lt.slice(2);
|
|
1308
1553
|
const lineIndent = line.length - line.trimStart().length;
|
|
1309
1554
|
const childIndent = lineIndent + 2;
|
|
1555
|
+
// Handle nested list: `- - val`
|
|
1556
|
+
if (itemVal.startsWith("- ") || itemVal === "-") {
|
|
1557
|
+
const nestedLines = [" ".repeat(childIndent) + itemVal];
|
|
1558
|
+
let j = i + 1;
|
|
1559
|
+
while (j < lines.length) {
|
|
1560
|
+
const cl = lines[j];
|
|
1561
|
+
if (cl.trim() === "") {
|
|
1562
|
+
nestedLines.push(cl);
|
|
1563
|
+
j++;
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const ci = cl.length - cl.trimStart().length;
|
|
1567
|
+
if (ci >= childIndent) {
|
|
1568
|
+
nestedLines.push(lines[j]);
|
|
1569
|
+
j++;
|
|
1570
|
+
}
|
|
1571
|
+
else
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
result.push(yamlParse(nestedLines, childIndent));
|
|
1575
|
+
i = j;
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1310
1578
|
const children = [];
|
|
1311
1579
|
let j = i + 1;
|
|
1312
1580
|
while (j < lines.length) {
|
|
@@ -1324,7 +1592,11 @@ function makePrelude(env) {
|
|
|
1324
1592
|
else
|
|
1325
1593
|
break;
|
|
1326
1594
|
}
|
|
1327
|
-
if (
|
|
1595
|
+
if (itemVal.trim() === "" && children.length === 0) {
|
|
1596
|
+
// Empty list item: `- ` or bare `-`
|
|
1597
|
+
result.push(null);
|
|
1598
|
+
}
|
|
1599
|
+
else if (children.length > 0 && children.some(c => c.trim() !== "" && c.trimStart().match(/^[^:]+:\s/))) {
|
|
1328
1600
|
if (itemVal.trim() !== "") {
|
|
1329
1601
|
const allLines = [" ".repeat(childIndent) + itemVal, ...children];
|
|
1330
1602
|
result.push(yamlParse(allLines, childIndent));
|
|
@@ -1403,10 +1675,14 @@ function makePrelude(env) {
|
|
|
1403
1675
|
else
|
|
1404
1676
|
break;
|
|
1405
1677
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1678
|
+
if (children.length === 0 || children.every(c => c.trim() === "")) {
|
|
1679
|
+
// No children → nil, not empty map
|
|
1680
|
+
result.set(key, null);
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1683
|
+
const childIndent = (children.find(c => c.trim() !== "")?.length ?? indent + 2) - (children.find(c => c.trim() !== "")?.trimStart().length ?? 0);
|
|
1684
|
+
result.set(key, yamlParse(children, childIndent));
|
|
1685
|
+
}
|
|
1410
1686
|
i = j;
|
|
1411
1687
|
}
|
|
1412
1688
|
else {
|
|
@@ -1417,7 +1693,7 @@ function makePrelude(env) {
|
|
|
1417
1693
|
return result;
|
|
1418
1694
|
}
|
|
1419
1695
|
}
|
|
1420
|
-
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() !== "...");
|
|
1421
1697
|
return toArcValue(yamlParse(rawLines, 0));
|
|
1422
1698
|
}
|
|
1423
1699
|
case "yaml.stringify": {
|
|
@@ -1471,20 +1747,115 @@ function makePrelude(env) {
|
|
|
1471
1747
|
const root = new Map();
|
|
1472
1748
|
let current = root;
|
|
1473
1749
|
const lines = src.split("\n");
|
|
1750
|
+
function tomlStripComment(s) {
|
|
1751
|
+
// Strip inline comments (not inside strings), handling escapes properly
|
|
1752
|
+
let inStr = false;
|
|
1753
|
+
let strCh = "";
|
|
1754
|
+
for (let i = 0; i < s.length; i++) {
|
|
1755
|
+
if (inStr) {
|
|
1756
|
+
if (s[i] === "\\") {
|
|
1757
|
+
i++;
|
|
1758
|
+
continue;
|
|
1759
|
+
} // skip escaped char (handles \\ and \")
|
|
1760
|
+
if (s[i] === strCh)
|
|
1761
|
+
inStr = false;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
if (s[i] === '"' || s[i] === "'") {
|
|
1765
|
+
inStr = true;
|
|
1766
|
+
strCh = s[i];
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
if (s[i] === "#")
|
|
1770
|
+
return s.slice(0, i).trim();
|
|
1771
|
+
}
|
|
1772
|
+
return s;
|
|
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
|
+
}
|
|
1474
1811
|
function tomlParseValue(s) {
|
|
1475
|
-
s = s.trim();
|
|
1812
|
+
s = tomlStripComment(s).trim();
|
|
1476
1813
|
if (s === "true")
|
|
1477
1814
|
return true;
|
|
1478
1815
|
if (s === "false")
|
|
1479
1816
|
return false;
|
|
1480
|
-
if (s
|
|
1481
|
-
return s
|
|
1817
|
+
if (s === "True" || s === "False")
|
|
1818
|
+
return s; // bug #6: reject capitalized booleans
|
|
1819
|
+
if (s === "inf" || s === "+inf")
|
|
1820
|
+
return Infinity;
|
|
1821
|
+
if (s === "-inf")
|
|
1822
|
+
return -Infinity;
|
|
1823
|
+
if (s === "nan" || s === "+nan" || s === "-nan")
|
|
1824
|
+
return NaN;
|
|
1825
|
+
// Multiline basic strings
|
|
1826
|
+
if (s.startsWith('"""')) {
|
|
1827
|
+
const end = s.indexOf('"""', 3);
|
|
1828
|
+
if (end !== -1)
|
|
1829
|
+
return s.slice(3, end).replace(/^\n/, "").replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\\\/g, "\\");
|
|
1830
|
+
}
|
|
1831
|
+
// Multiline literal strings
|
|
1832
|
+
if (s.startsWith("'''")) {
|
|
1833
|
+
const end = s.indexOf("'''", 3);
|
|
1834
|
+
if (end !== -1)
|
|
1835
|
+
return s.slice(3, end).replace(/^\n/, "");
|
|
1836
|
+
}
|
|
1837
|
+
if (s.startsWith('"'))
|
|
1838
|
+
return tomlExtractString(s); // bug #3: handle escaped quotes
|
|
1482
1839
|
if (s.startsWith("'") && s.endsWith("'"))
|
|
1483
1840
|
return s.slice(1, -1);
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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, ""));
|
|
1488
1859
|
if (s.startsWith("["))
|
|
1489
1860
|
return tomlParseArray(s);
|
|
1490
1861
|
if (s.startsWith("{"))
|
|
@@ -1574,8 +1945,9 @@ function makePrelude(env) {
|
|
|
1574
1945
|
}
|
|
1575
1946
|
return cur;
|
|
1576
1947
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1948
|
+
const seenSections = new Set(); // bug #9
|
|
1949
|
+
for (let li = 0; li < lines.length; li++) {
|
|
1950
|
+
const trimmed = lines[li].trim();
|
|
1579
1951
|
if (trimmed === "" || trimmed.startsWith("#"))
|
|
1580
1952
|
continue;
|
|
1581
1953
|
const arrMatch = trimmed.match(/^\[\[([^\]]+)\]\]$/);
|
|
@@ -1593,15 +1965,80 @@ function makePrelude(env) {
|
|
|
1593
1965
|
}
|
|
1594
1966
|
const secMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
1595
1967
|
if (secMatch) {
|
|
1596
|
-
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());
|
|
1597
1973
|
current = ensurePath(root, keys);
|
|
1598
1974
|
continue;
|
|
1599
1975
|
}
|
|
1600
1976
|
const eq = trimmed.indexOf("=");
|
|
1601
1977
|
if (eq > 0) {
|
|
1602
1978
|
const key = trimmed.slice(0, eq).trim();
|
|
1603
|
-
|
|
1604
|
-
|
|
1979
|
+
let valStr = trimmed.slice(eq + 1).trim();
|
|
1980
|
+
// Handle multiline basic strings
|
|
1981
|
+
if (valStr.startsWith('"""') && !valStr.slice(3).includes('"""')) {
|
|
1982
|
+
while (li + 1 < lines.length && !lines[li + 1].includes('"""')) {
|
|
1983
|
+
li++;
|
|
1984
|
+
valStr += "\n" + lines[li];
|
|
1985
|
+
}
|
|
1986
|
+
if (li + 1 < lines.length) {
|
|
1987
|
+
li++;
|
|
1988
|
+
valStr += "\n" + lines[li];
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
// Handle multiline literal strings
|
|
1992
|
+
if (valStr.startsWith("'''") && !valStr.slice(3).includes("'''")) {
|
|
1993
|
+
while (li + 1 < lines.length && !lines[li + 1].includes("'''")) {
|
|
1994
|
+
li++;
|
|
1995
|
+
valStr += "\n" + lines[li];
|
|
1996
|
+
}
|
|
1997
|
+
if (li + 1 < lines.length) {
|
|
1998
|
+
li++;
|
|
1999
|
+
valStr += "\n" + lines[li];
|
|
2000
|
+
}
|
|
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
|
+
}
|
|
2027
|
+
const val = tomlParseValue(valStr);
|
|
2028
|
+
// Expand dotted keys: a.b.c = 1 → nested maps
|
|
2029
|
+
if (key.includes(".")) {
|
|
2030
|
+
const parts = key.split(".").map(k => k.trim());
|
|
2031
|
+
let target = current;
|
|
2032
|
+
for (let ki = 0; ki < parts.length - 1; ki++) {
|
|
2033
|
+
if (!target.has(parts[ki]))
|
|
2034
|
+
target.set(parts[ki], new Map());
|
|
2035
|
+
target = target.get(parts[ki]);
|
|
2036
|
+
}
|
|
2037
|
+
target.set(parts[parts.length - 1], val);
|
|
2038
|
+
}
|
|
2039
|
+
else {
|
|
2040
|
+
current.set(key, val);
|
|
2041
|
+
}
|
|
1605
2042
|
}
|
|
1606
2043
|
}
|
|
1607
2044
|
return toArcValue(root);
|
|
@@ -1661,6 +2098,10 @@ function makePrelude(env) {
|
|
|
1661
2098
|
}
|
|
1662
2099
|
return lines.join("\n");
|
|
1663
2100
|
}
|
|
2101
|
+
if (val === null || val === undefined)
|
|
2102
|
+
return "";
|
|
2103
|
+
if (!(val instanceof Map))
|
|
2104
|
+
return "";
|
|
1664
2105
|
return tomlStringifySection(val, "");
|
|
1665
2106
|
}
|
|
1666
2107
|
// --- log natives ---
|
|
@@ -1697,7 +2138,12 @@ function makePrelude(env) {
|
|
|
1697
2138
|
return null;
|
|
1698
2139
|
}
|
|
1699
2140
|
case "log.set_level": {
|
|
1700
|
-
|
|
2141
|
+
const lvl = args[0];
|
|
2142
|
+
const validLevels = ["debug", "info", "warn", "error", "fatal"];
|
|
2143
|
+
if (!validLevels.includes(lvl)) {
|
|
2144
|
+
throw new ArcRuntimeError(`Invalid log level '${lvl}'. Must be one of: ${validLevels.join(", ")}`, { code: ErrorCode.UNDEFINED_VARIABLE });
|
|
2145
|
+
}
|
|
2146
|
+
globalThis.__arc_log_level = lvl;
|
|
1701
2147
|
return null;
|
|
1702
2148
|
}
|
|
1703
2149
|
case "log.with": {
|
|
@@ -1707,6 +2153,16 @@ function makePrelude(env) {
|
|
|
1707
2153
|
const level = args[0];
|
|
1708
2154
|
const msg = args[1];
|
|
1709
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;
|
|
1710
2166
|
const obj = {
|
|
1711
2167
|
timestamp: new Date().toISOString(),
|
|
1712
2168
|
level: level,
|
|
@@ -1726,6 +2182,8 @@ function makePrelude(env) {
|
|
|
1726
2182
|
embed_dot_product: (a, b) => {
|
|
1727
2183
|
if (!Array.isArray(a) || !Array.isArray(b))
|
|
1728
2184
|
return 0;
|
|
2185
|
+
if (a.length !== b.length)
|
|
2186
|
+
throw new Error(`Vector length mismatch: ${a.length} vs ${b.length}`);
|
|
1729
2187
|
let sum = 0;
|
|
1730
2188
|
for (let i = 0; i < a.length; i++)
|
|
1731
2189
|
sum += a[i] * b[i];
|
|
@@ -1742,6 +2200,8 @@ function makePrelude(env) {
|
|
|
1742
2200
|
embed_cosine_similarity: (a, b) => {
|
|
1743
2201
|
if (!Array.isArray(a) || !Array.isArray(b))
|
|
1744
2202
|
return 0;
|
|
2203
|
+
if (a.length !== b.length)
|
|
2204
|
+
throw new Error(`Vector length mismatch: ${a.length} vs ${b.length}`);
|
|
1745
2205
|
let dot = 0, magA = 0, magB = 0;
|
|
1746
2206
|
for (let i = 0; i < a.length; i++) {
|
|
1747
2207
|
const ai = a[i], bi = b[i];
|
|
@@ -1766,6 +2226,8 @@ function makePrelude(env) {
|
|
|
1766
2226
|
embed_euclidean_distance: (a, b) => {
|
|
1767
2227
|
if (!Array.isArray(a) || !Array.isArray(b))
|
|
1768
2228
|
return 0;
|
|
2229
|
+
if (a.length !== b.length)
|
|
2230
|
+
throw new Error(`Vector length mismatch: ${a.length} vs ${b.length}`);
|
|
1769
2231
|
let sum = 0;
|
|
1770
2232
|
for (let i = 0; i < a.length; i++) {
|
|
1771
2233
|
const d = a[i] - b[i];
|
|
@@ -1841,13 +2303,20 @@ function makePrelude(env) {
|
|
|
1841
2303
|
};
|
|
1842
2304
|
function callFn(fn, args) {
|
|
1843
2305
|
if (fn && typeof fn === "object" && "__fn" in fn) {
|
|
2306
|
+
if (++callDepth > 2000) {
|
|
2307
|
+
callDepth = 0;
|
|
2308
|
+
throw new ArcRuntimeError("Maximum call stack depth exceeded");
|
|
2309
|
+
}
|
|
1844
2310
|
const f = fn;
|
|
1845
2311
|
const fnEnv = new Env(f.closure);
|
|
1846
2312
|
bindParams(f, args, fnEnv, evalExpr);
|
|
1847
2313
|
try {
|
|
1848
|
-
|
|
2314
|
+
const result = evalExpr(f.body, fnEnv);
|
|
2315
|
+
callDepth--;
|
|
2316
|
+
return result;
|
|
1849
2317
|
}
|
|
1850
2318
|
catch (e) {
|
|
2319
|
+
callDepth--;
|
|
1851
2320
|
if (e instanceof ReturnSignal)
|
|
1852
2321
|
return e.value;
|
|
1853
2322
|
throw e;
|
|
@@ -1974,13 +2443,31 @@ function evalExpr(expr, env) {
|
|
|
1974
2443
|
switch (expr.op) {
|
|
1975
2444
|
case "+": {
|
|
1976
2445
|
if (typeof left === "string" || typeof right === "string") {
|
|
2446
|
+
if (left === null || right === null)
|
|
2447
|
+
throw new ArcRuntimeError(`TypeError: cannot add nil`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
1977
2448
|
return toStr(left) + toStr(right);
|
|
1978
2449
|
}
|
|
2450
|
+
if (left === null || right === null)
|
|
2451
|
+
throw new ArcRuntimeError(`TypeError: cannot add nil`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
2452
|
+
if (typeof left !== "number" || typeof right !== "number")
|
|
2453
|
+
throw new ArcRuntimeError(`TypeError: cannot add ${typeof left} and ${typeof right}`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
1979
2454
|
return left + right;
|
|
1980
2455
|
}
|
|
1981
|
-
case "-":
|
|
1982
|
-
|
|
2456
|
+
case "-": {
|
|
2457
|
+
if (typeof left !== "number" || typeof right !== "number")
|
|
2458
|
+
throw new ArcRuntimeError(`TypeError: cannot subtract non-numbers`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
2459
|
+
return left - right;
|
|
2460
|
+
}
|
|
2461
|
+
case "*": {
|
|
2462
|
+
if (typeof left === "string" && typeof right === "number" && Number.isInteger(right) && right >= 0)
|
|
2463
|
+
return left.repeat(right);
|
|
2464
|
+
if (typeof left !== "number" || typeof right !== "number")
|
|
2465
|
+
throw new ArcRuntimeError(`TypeError: cannot multiply non-numbers`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
2466
|
+
return left * right;
|
|
2467
|
+
}
|
|
1983
2468
|
case "/": {
|
|
2469
|
+
if (typeof left !== "number" || typeof right !== "number")
|
|
2470
|
+
throw new ArcRuntimeError(`TypeError: cannot divide non-numbers`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
1984
2471
|
if (right === 0)
|
|
1985
2472
|
throw new ArcRuntimeError(`Division by zero`, {
|
|
1986
2473
|
code: ErrorCode.DIVISION_BY_ZERO, loc: expr.loc,
|
|
@@ -1989,6 +2476,8 @@ function evalExpr(expr, env) {
|
|
|
1989
2476
|
return left / right;
|
|
1990
2477
|
}
|
|
1991
2478
|
case "%": {
|
|
2479
|
+
if (typeof left !== "number" || typeof right !== "number")
|
|
2480
|
+
throw new ArcRuntimeError(`TypeError: cannot modulo non-numbers`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
1992
2481
|
if (right === 0)
|
|
1993
2482
|
throw new ArcRuntimeError(`Modulo by zero`, {
|
|
1994
2483
|
code: ErrorCode.DIVISION_BY_ZERO, loc: expr.loc,
|
|
@@ -1996,7 +2485,11 @@ function evalExpr(expr, env) {
|
|
|
1996
2485
|
});
|
|
1997
2486
|
return left % right;
|
|
1998
2487
|
}
|
|
1999
|
-
case "**":
|
|
2488
|
+
case "**": {
|
|
2489
|
+
if (typeof left !== "number" || typeof right !== "number")
|
|
2490
|
+
throw new ArcRuntimeError(`TypeError: cannot exponentiate non-numbers`, { code: ErrorCode.INVALID_OPERATOR, loc: expr.loc });
|
|
2491
|
+
return Math.pow(left, right);
|
|
2492
|
+
}
|
|
2000
2493
|
case "==": return left === right;
|
|
2001
2494
|
case "!=": return left !== right;
|
|
2002
2495
|
case "<": return left < right;
|
|
@@ -2017,7 +2510,7 @@ function evalExpr(expr, env) {
|
|
|
2017
2510
|
const operand = evalExpr(expr.operand, env);
|
|
2018
2511
|
if (expr.op === "-")
|
|
2019
2512
|
return -operand;
|
|
2020
|
-
if (expr.op === "not")
|
|
2513
|
+
if (expr.op === "not" || expr.op === "!")
|
|
2021
2514
|
return !isTruthy(operand);
|
|
2022
2515
|
throw new Error(`Unknown unary op: ${expr.op}`);
|
|
2023
2516
|
}
|
|
@@ -2030,10 +2523,19 @@ function evalExpr(expr, env) {
|
|
|
2030
2523
|
}
|
|
2031
2524
|
else if (callee && typeof callee === "object" && "__fn" in callee) {
|
|
2032
2525
|
let fn = callee;
|
|
2526
|
+
if (++callDepth > 2000) {
|
|
2527
|
+
callDepth = 0;
|
|
2528
|
+
throw new ArcRuntimeError("Maximum call stack depth exceeded");
|
|
2529
|
+
}
|
|
2033
2530
|
// Tail call optimization loop: if the function body resolves to
|
|
2034
2531
|
// a tail call back to itself, reuse the frame instead of recursing
|
|
2532
|
+
let tcoIterations = 0;
|
|
2035
2533
|
try {
|
|
2036
2534
|
tailLoop: while (true) {
|
|
2535
|
+
if (++tcoIterations > 2000) {
|
|
2536
|
+
callDepth--;
|
|
2537
|
+
throw new ArcRuntimeError("Maximum call stack depth exceeded");
|
|
2538
|
+
}
|
|
2037
2539
|
const fnEnv = new Env(fn.closure);
|
|
2038
2540
|
bindParams(fn, args, fnEnv, evalExpr);
|
|
2039
2541
|
const bodyResult = evalExprTCO(fn.body, fnEnv, fn.name);
|
|
@@ -2046,8 +2548,10 @@ function evalExpr(expr, env) {
|
|
|
2046
2548
|
result = bodyResult;
|
|
2047
2549
|
break;
|
|
2048
2550
|
}
|
|
2551
|
+
callDepth--;
|
|
2049
2552
|
}
|
|
2050
2553
|
catch (e) {
|
|
2554
|
+
callDepth--;
|
|
2051
2555
|
if (e instanceof ReturnSignal) {
|
|
2052
2556
|
result = e.value;
|
|
2053
2557
|
}
|
|
@@ -2105,8 +2609,16 @@ function evalExpr(expr, env) {
|
|
|
2105
2609
|
case "IndexExpr": {
|
|
2106
2610
|
const obj = evalExpr(expr.object, env);
|
|
2107
2611
|
const idx = evalExpr(expr.index, env);
|
|
2108
|
-
if (
|
|
2109
|
-
|
|
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
|
+
}
|
|
2110
2622
|
if (obj && typeof obj === "object" && "__map" in obj) {
|
|
2111
2623
|
const key = typeof idx === "string" ? idx : toStr(idx);
|
|
2112
2624
|
return obj.entries.get(key) ?? null;
|
|
@@ -2464,7 +2976,14 @@ export function interpret(program, onUse) {
|
|
|
2464
2976
|
onUse(stmt, env);
|
|
2465
2977
|
}
|
|
2466
2978
|
else {
|
|
2467
|
-
|
|
2979
|
+
try {
|
|
2980
|
+
evalStmt(stmt, env);
|
|
2981
|
+
}
|
|
2982
|
+
catch (e) {
|
|
2983
|
+
if (e instanceof ReturnSignal)
|
|
2984
|
+
throw new ArcRuntimeError("ret used outside of function");
|
|
2985
|
+
throw e;
|
|
2986
|
+
}
|
|
2468
2987
|
}
|
|
2469
2988
|
}
|
|
2470
2989
|
}
|
|
@@ -2476,7 +2995,14 @@ export function interpretWithEnv(program, env, onUse) {
|
|
|
2476
2995
|
result = null;
|
|
2477
2996
|
}
|
|
2478
2997
|
else {
|
|
2479
|
-
|
|
2998
|
+
try {
|
|
2999
|
+
result = evalStmt(stmt, env);
|
|
3000
|
+
}
|
|
3001
|
+
catch (e) {
|
|
3002
|
+
if (e instanceof ReturnSignal)
|
|
3003
|
+
throw new ArcRuntimeError("ret used outside of function");
|
|
3004
|
+
throw e;
|
|
3005
|
+
}
|
|
2480
3006
|
}
|
|
2481
3007
|
}
|
|
2482
3008
|
return result;
|
package/dist/lexer.js
CHANGED
|
@@ -377,6 +377,11 @@ export function lex(source) {
|
|
|
377
377
|
tokens.push(tok(TokenType.Neq, "!=", sl, sc));
|
|
378
378
|
continue;
|
|
379
379
|
}
|
|
380
|
+
if (ch === "!") {
|
|
381
|
+
advance();
|
|
382
|
+
tokens.push(tok(TokenType.Not, "!", sl, sc));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
380
385
|
if (ch === "<" && peek(1) === "=") {
|
|
381
386
|
advance();
|
|
382
387
|
advance();
|
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