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.
@@ -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 ?? list[0];
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.split("") : [],
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 buf = nodeCrypto.randomBytes(n);
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) => decodeURIComponent(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) => m.set(k, v));
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
- const result = typeof fn === "function" ? fn() :
459
- (fn && typeof fn === "object" && "__fn" in fn) ? callFn(fn, []) : null;
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
- new RegExp(pattern);
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
- return new Date(s).getTime();
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
- return new Date(year, month - 1, day, hour, min, sec).getTime();
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.replace("YYYY", String(d.getFullYear()));
714
- result = result.replace("MM", String(d.getMonth() + 1).padStart(2, "0"));
715
- result = result.replace("DD", String(d.getDate()).padStart(2, "0"));
716
- result = result.replace("hh", String(d.getHours()).padStart(2, "0"));
717
- result = result.replace("mm", String(d.getMinutes()).padStart(2, "0"));
718
- result = result.replace("ss", String(d.getSeconds()).padStart(2, "0"));
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: reject commands with common shell injection patterns
865
- const dangerous = /[;&|`$]|\$\(|>\s*>|<\s*<|\beval\b|\bsource\b/;
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
- const cp = require("child_process");
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 (text.length <= maxChars)
945
+ if (codepoints.length <= maxChars)
888
946
  return text;
889
- return text.slice(0, maxChars);
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 < text.length; i += chunkSize) {
897
- chunks.push(text.slice(i, i + chunkSize));
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]) => ({ key: k, value: 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(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'");
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(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/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
- children = parseHTML(html.slice(i));
1093
- const closeTag = `</${tag}>`;
1094
- const closeIdx = html.toLowerCase().indexOf(closeTag, i);
1095
- if (closeIdx !== -1) {
1096
- i = closeIdx + closeTag.length;
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
- i = html.length;
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
- process.env[args[0]] = String(args[1]);
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 (children.length > 0 && children.some(c => c.trim() !== "" && c.trimStart().match(/^[^:]+:\s/))) {
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
- const childIndent = children.length > 0 ?
1407
- (children.find(c => c.trim() !== "")?.length ?? indent + 2) - (children.find(c => c.trim() !== "")?.trimStart().length ?? 0) :
1408
- indent + 2;
1409
- result.set(key, yamlParse(children, childIndent));
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.startsWith('"') && s.endsWith('"'))
1481
- return s.slice(1, -1).replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\\\/g, "\\");
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
- if (/^-?\d+$/.test(s))
1485
- return parseInt(s, 10);
1486
- if (/^-?\d+\.\d+$/.test(s))
1487
- return parseFloat(s);
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
- for (const line of lines) {
1578
- const trimmed = line.trim();
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 keys = secMatch[1].split(".").map((k) => k.trim());
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
- const val = tomlParseValue(trimmed.slice(eq + 1));
1604
- current.set(key, val);
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
- globalThis.__arc_log_level = args[0];
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
- return evalExpr(f.body, fnEnv);
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 "-": return left - right;
1982
- case "*": return left * right;
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 "**": return Math.pow(left, right);
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 (Array.isArray(obj) && typeof idx === "number")
2109
- return obj[idx] ?? null;
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
- evalStmt(stmt, env);
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
- result = evalStmt(stmt, env);
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
@@ -1,4 +1,4 @@
1
- export declare const ARC_VERSION = "0.6.0";
1
+ export declare const ARC_VERSION = "0.6.2";
2
2
  export declare const ARC_BUILD_DATE: string;
3
3
  export declare const ARC_PLATFORM: string;
4
4
  /** Print version info */
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Arc Version System
2
- export const ARC_VERSION = "0.6.0";
2
+ export const ARC_VERSION = "0.6.2";
3
3
  export const ARC_BUILD_DATE = new Date().toISOString().split("T")[0];
4
4
  export const ARC_PLATFORM = `${process.platform}-${process.arch}`;
5
5
  /** Print version info */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arc-lang",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Arc ⚡ — A programming language designed by AI agents, for AI agents. 27-63% fewer tokens than JavaScript.",
5
5
  "type": "module",
6
6
  "bin": {