arc-lang 0.6.1 → 0.6.2

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