@o3co/ts.hocon 1.2.0 → 1.3.0

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/zod.cjs CHANGED
@@ -39,6 +39,27 @@ module.exports = __toCommonJS(zod_exports);
39
39
 
40
40
  // src/coerce.ts
41
41
  var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
42
+ function isHoconWs(cp) {
43
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
44
+ if (cp >= 28 && cp <= 31) return true;
45
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
46
+ if (cp === 5760) return true;
47
+ if (cp >= 8192 && cp <= 8202) return true;
48
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
49
+ if (cp === 12288) return true;
50
+ return false;
51
+ }
52
+ function trimHoconWs(s) {
53
+ let start = 0;
54
+ let end = s.length;
55
+ while (start < end && isHoconWs(s.charCodeAt(start))) {
56
+ start++;
57
+ }
58
+ while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
59
+ end--;
60
+ }
61
+ return s.slice(start, end);
62
+ }
42
63
  var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
43
64
  var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
44
65
  function coerceBoolean(value) {
@@ -84,22 +105,29 @@ var OUTPUT_DURATION_UNITS = {
84
105
  d: 864e5
85
106
  };
86
107
  function parseDuration(value, outputUnit = "ms") {
87
- const trimmed = value.trim();
108
+ const trimmed = trimHoconWs(value);
88
109
  let i = 0;
89
110
  while (i < trimmed.length) {
90
111
  const ch = trimmed[i];
91
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
112
+ if (i === 0 && (ch === "-" || ch === "+")) {
113
+ i++;
114
+ continue;
115
+ }
116
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
92
117
  i++;
93
118
  }
94
119
  if (i === 0) return NaN;
95
120
  const num = Number(trimmed.slice(0, i));
96
121
  if (Number.isNaN(num)) return NaN;
97
- const unit = trimmed.slice(i).trim().toLowerCase();
122
+ const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
123
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
124
+ if (divisor === void 0) return NaN;
125
+ if (unit === "") {
126
+ return num / divisor;
127
+ }
98
128
  const mult = DURATION_UNITS[unit];
99
129
  if (mult === void 0) return NaN;
100
130
  const ms = num * mult;
101
- const divisor = OUTPUT_DURATION_UNITS[outputUnit];
102
- if (divisor === void 0) return NaN;
103
131
  return ms / divisor;
104
132
  }
105
133
  var BYTE_UNITS = {
@@ -130,6 +158,22 @@ var BYTE_UNITS = {
130
158
  TiB: 1099511627776,
131
159
  tebibyte: 1099511627776,
132
160
  tebibytes: 1099511627776,
161
+ // S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
162
+ // Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
163
+ // Both upper- and lowercase accepted per Lightbend behaviour.
164
+ // Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
165
+ K: 1024,
166
+ k: 1024,
167
+ M: 1024 ** 2,
168
+ m: 1024 ** 2,
169
+ G: 1024 ** 3,
170
+ g: 1024 ** 3,
171
+ T: 1024 ** 4,
172
+ t: 1024 ** 4,
173
+ P: 1024 ** 5,
174
+ p: 1024 ** 5,
175
+ E: 1024 ** 6,
176
+ e: 1024 ** 6,
133
177
  // lowercase short-form aliases
134
178
  b: 1,
135
179
  kb: 1e3,
@@ -153,25 +197,39 @@ var OUTPUT_BYTE_UNITS = {
153
197
  TiB: 1099511627776
154
198
  };
155
199
  function parseBytes(value, outputUnit = "B") {
156
- const trimmed = value.trim();
200
+ const trimmed = trimHoconWs(value);
157
201
  let i = 0;
158
202
  while (i < trimmed.length) {
159
203
  const ch = trimmed[i];
160
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
204
+ if (i === 0 && (ch === "-" || ch === "+")) {
205
+ i++;
206
+ continue;
207
+ }
208
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
161
209
  i++;
162
210
  }
163
211
  if (i === 0) return NaN;
164
212
  const num = Number(trimmed.slice(0, i));
165
213
  if (Number.isNaN(num)) return NaN;
166
- const unit = trimmed.slice(i).trim();
214
+ const unit = trimHoconWs(trimmed.slice(i));
215
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
216
+ if (divisor === void 0) return NaN;
217
+ if (unit === "") {
218
+ const bytes2 = Math.trunc(num);
219
+ if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
220
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
221
+ }
222
+ return bytes2 / divisor;
223
+ }
167
224
  let mult = BYTE_UNITS[unit];
168
225
  if (mult === void 0) {
169
226
  mult = BYTE_UNITS[unit.toLowerCase()];
170
227
  }
171
228
  if (mult === void 0) return NaN;
172
229
  const bytes = num * mult;
173
- const divisor = OUTPUT_BYTE_UNITS[outputUnit];
174
- if (divisor === void 0) return NaN;
230
+ if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
231
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
232
+ }
175
233
  const result = bytes / divisor;
176
234
  return outputUnit === "B" ? Math.round(result) : result;
177
235
  }
@@ -331,14 +389,6 @@ var Lexer = class {
331
389
  continue;
332
390
  }
333
391
  if (isUnquotedStart(ch)) {
334
- if (ch === "-" && !isDecimalDigit(this.peek(1))) {
335
- const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
336
- throw new ParseError(
337
- `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
338
- sl,
339
- sc
340
- );
341
- }
342
392
  let value = "";
343
393
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
344
394
  value += this.advance();
@@ -442,6 +492,10 @@ var Lexer = class {
442
492
  /**
443
493
  * Parse the body of a `${...}` substitution (called after `${` has been consumed).
444
494
  * Implements Appendix A state machine from the cross-language spec.
495
+ *
496
+ * S13c extension: if the segment-collection loop encounters `[`, it delegates to
497
+ * `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
498
+ * ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
445
499
  */
446
500
  parseSubstBody(startLine, startCol) {
447
501
  let optional = false;
@@ -456,6 +510,7 @@ var Lexer = class {
456
510
  let pendingWs = "";
457
511
  const segments = [];
458
512
  let lastDot = null;
513
+ let listSuffix = false;
459
514
  while (true) {
460
515
  if (this.pos >= this.input.length) {
461
516
  throw new ParseError("unterminated substitution", startLine, startCol);
@@ -465,6 +520,23 @@ var Lexer = class {
465
520
  this.advance();
466
521
  pendingWs = "";
467
522
  break;
523
+ } else if (ch === "[") {
524
+ if (!curStarted) {
525
+ throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
526
+ }
527
+ for (const w of pendingWs) {
528
+ if (w !== " " && w !== " ") {
529
+ throw new ParseError(
530
+ `only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
531
+ startLine,
532
+ this.col
533
+ );
534
+ }
535
+ }
536
+ segments.push({ text: curText, line: curLine, col: curCol });
537
+ pendingWs = "";
538
+ listSuffix = this.parseListSuffix(startLine);
539
+ break;
468
540
  } else if (ch === '"') {
469
541
  const qLine = startLine;
470
542
  const qCol = this.col;
@@ -524,15 +596,47 @@ var Lexer = class {
524
596
  throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
525
597
  }
526
598
  }
527
- if (curStarted) {
528
- segments.push({ text: curText, line: curLine, col: curCol });
529
- } else if (segments.length === 0) {
530
- throw new ParseError("empty substitution path", startLine, startCol);
531
- } else {
532
- const [errLine, errCol] = lastDot ?? [startLine, startCol];
533
- throw new ParseError("empty segment in path", errLine, errCol);
599
+ if (!listSuffix) {
600
+ if (curStarted) {
601
+ segments.push({ text: curText, line: curLine, col: curCol });
602
+ } else if (segments.length === 0) {
603
+ throw new ParseError("empty substitution path", startLine, startCol);
604
+ } else {
605
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
606
+ throw new ParseError("empty segment in path", errLine, errCol);
607
+ }
534
608
  }
535
- return { segments, optional };
609
+ return { segments, optional, listSuffix };
610
+ }
611
+ /**
612
+ * Consume the literal `[]` suffix inside a substitution body.
613
+ * Called after the `[` has been detected (but not yet consumed).
614
+ * Requires the next character to be `[`, then exactly `]`, then `}`.
615
+ * Whitespace inside `[]` is not permitted (per spec Decision §1).
616
+ * Returns true (always — only called when listSuffix path is taken).
617
+ */
618
+ parseListSuffix(startLine) {
619
+ this.advance();
620
+ const afterBracket = this.peek();
621
+ if (afterBracket !== "]") {
622
+ const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
623
+ throw new ParseError(
624
+ `expected ']' after '[' in substitution list suffix (got ${desc})`,
625
+ startLine,
626
+ this.col
627
+ );
628
+ }
629
+ this.advance();
630
+ if (this.peek() !== "}") {
631
+ const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
632
+ throw new ParseError(
633
+ `expected '}' after '[]' in substitution list suffix (got ${desc})`,
634
+ startLine,
635
+ this.col
636
+ );
637
+ }
638
+ this.advance();
639
+ return true;
536
640
  }
537
641
  };
538
642
  function tokenize(input) {
@@ -620,6 +724,14 @@ var Parser = class {
620
724
  const t = this.peek();
621
725
  if (t.kind === "eof" || t.kind === "rbrace") break;
622
726
  if (t.kind === "unquoted" && t.value === "include") {
727
+ const next = this.peek(1);
728
+ if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
729
+ throw new ParseError(
730
+ `'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
731
+ t.line,
732
+ t.col
733
+ );
734
+ }
623
735
  this.advance();
624
736
  fields.push(this.parseInclude());
625
737
  this.skip("newline");
@@ -628,7 +740,14 @@ var Parser = class {
628
740
  continue;
629
741
  }
630
742
  const keyPos = this.currentPos();
631
- const key = this.parseKey();
743
+ const { segments: key, firstWasQuoted } = this.parseKey();
744
+ if (key[0] === "include" && !firstWasQuoted) {
745
+ throw new ParseError(
746
+ `'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
747
+ keyPos.line,
748
+ keyPos.col
749
+ );
750
+ }
632
751
  this.skip("newline");
633
752
  let append = false;
634
753
  const sep = this.peek();
@@ -658,10 +777,12 @@ var Parser = class {
658
777
  parseKey() {
659
778
  const segments = [];
660
779
  let trailingDot = false;
780
+ let firstWasQuoted = false;
661
781
  while (true) {
662
782
  const t = this.peek();
663
783
  if (t.kind === "string") {
664
784
  this.advance();
785
+ if (segments.length === 0) firstWasQuoted = true;
665
786
  segments.push(t.value);
666
787
  trailingDot = false;
667
788
  } else if (t.kind === "unquoted") {
@@ -698,7 +819,7 @@ var Parser = class {
698
819
  }
699
820
  break;
700
821
  }
701
- return segments;
822
+ return { segments, firstWasQuoted };
702
823
  }
703
824
  parseInclude() {
704
825
  const p = this.currentPos();
@@ -777,7 +898,7 @@ var Parser = class {
777
898
  throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
778
899
  }
779
900
  const payload = t.subst;
780
- node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
901
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
781
902
  } else if (t.kind === "string" || t.kind === "triple_string") {
782
903
  this.advance();
783
904
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -828,6 +949,20 @@ function parseTokens(tokens) {
828
949
  return new Parser(tokens).parse();
829
950
  }
830
951
 
952
+ // src/internal/parser/empty-check.ts
953
+ function hasContentTokens(tokens) {
954
+ return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
955
+ }
956
+ function assertNonEmptyDocument(tokens, sourceDescriptor) {
957
+ if (!hasContentTokens(tokens)) {
958
+ throw new ParseError(
959
+ `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
960
+ 1,
961
+ 1
962
+ );
963
+ }
964
+ }
965
+
831
966
  // src/internal/resolver/types.ts
832
967
  var separatorValues = /* @__PURE__ */ new WeakSet();
833
968
  function isSubst(v) {
@@ -929,6 +1064,7 @@ var nodePath = __toESM(require("path"), 1);
929
1064
  // src/internal/properties/properties.ts
930
1065
  function parseProperties(input) {
931
1066
  const root = /* @__PURE__ */ Object.create(null);
1067
+ const pairs = [];
932
1068
  for (const line of input.split("\n")) {
933
1069
  const trimmed = line.trim();
934
1070
  if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
@@ -937,6 +1073,10 @@ function parseProperties(input) {
937
1073
  const key = trimmed.slice(0, sepIdx).trim();
938
1074
  const value = trimmed.slice(sepIdx + 1).trim();
939
1075
  if (key === "") continue;
1076
+ pairs.push([key, value]);
1077
+ }
1078
+ pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1079
+ for (const [key, value] of pairs) {
940
1080
  setNested(root, key.split("."), value);
941
1081
  }
942
1082
  return root;
@@ -960,6 +1100,7 @@ function setNested(obj, segments, value) {
960
1100
  }
961
1101
  const last = segments[segments.length - 1];
962
1102
  if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1103
+ if (typeof current[last] === "object" && current[last] !== null) return;
963
1104
  current[last] = value;
964
1105
  }
965
1106
  function propertiesToHoconValue(input) {
@@ -1073,7 +1214,9 @@ var IncludeLoader = class {
1073
1214
  if (candidate.endsWith(".properties")) {
1074
1215
  return hoconValueToResObj(propertiesToHoconValue(content));
1075
1216
  }
1076
- const ast = parseTokens(tokenize(content));
1217
+ const tokens = tokenize(content);
1218
+ assertNonEmptyDocument(tokens, candidate);
1219
+ const ast = parseTokens(tokens);
1077
1220
  return this.onBuildResObj(ast, {
1078
1221
  env,
1079
1222
  baseDir: nodePath.dirname(candidate),
@@ -1097,7 +1240,9 @@ var IncludeLoader = class {
1097
1240
  if (candidate.endsWith(".properties")) {
1098
1241
  return hoconValueToResObj(propertiesToHoconValue(content));
1099
1242
  }
1100
- const ast = parseTokens(tokenize(content));
1243
+ const tokens = tokenize(content);
1244
+ assertNonEmptyDocument(tokens, candidate);
1245
+ const ast = parseTokens(tokens);
1101
1246
  return this.onBuildResObjAsync(ast, {
1102
1247
  env,
1103
1248
  baseDir: nodePath.dirname(candidate),
@@ -1234,9 +1379,9 @@ var StructureBuilder = class _StructureBuilder {
1234
1379
  return inner;
1235
1380
  }
1236
1381
  case "subst":
1237
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1382
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1238
1383
  case "concat":
1239
- return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1384
+ return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1240
1385
  case "include":
1241
1386
  return { kind: "scalar", raw: "null", valueType: "null" };
1242
1387
  }
@@ -1258,13 +1403,13 @@ var StructureBuilder = class _StructureBuilder {
1258
1403
  case "object":
1259
1404
  return await this.buildAsync(ast, pathPrefix);
1260
1405
  case "subst":
1261
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1406
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1262
1407
  case "concat": {
1263
1408
  const nodes = [];
1264
1409
  for (const n of ast.nodes) {
1265
1410
  nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
1266
1411
  }
1267
- return { _kind: "concat-placeholder", nodes };
1412
+ return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
1268
1413
  }
1269
1414
  case "include":
1270
1415
  return { kind: "scalar", raw: "null", valueType: "null" };
@@ -1334,6 +1479,19 @@ var SubstitutionResolver = class {
1334
1479
  this.opts = opts;
1335
1480
  this.resolving = /* @__PURE__ */ new Set();
1336
1481
  this.cache = /* @__PURE__ */ new Map();
1482
+ // Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
1483
+ // Used by isSelfRef detection: fires only when found === a concat currently
1484
+ // mid-iteration (i.e. we are inside found's own resolution), preventing the
1485
+ // "foofoo" double-count without misfiring when an external field looks up the
1486
+ // same concat via a substitution (multi-reviewer convergence fix).
1487
+ //
1488
+ // Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
1489
+ // preservation for self-ref detection. Round-2 multi-agent-review surfaced a
1490
+ // false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
1491
+ // criterion was tightened to node-membership-in-iterating-set — strictly
1492
+ // narrower than path-equality. Spec amendment deferred to a follow-up
1493
+ // xx.hocon PR (see Phase 6 #3f close-out notes).
1494
+ this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1337
1495
  }
1338
1496
  resolve() {
1339
1497
  return this.resolveResObj(this.root);
@@ -1372,7 +1530,14 @@ var SubstitutionResolver = class {
1372
1530
  }
1373
1531
  resolveVal(v, scope) {
1374
1532
  if (isSubst(v)) return this.resolveSubst(v, scope);
1375
- if (isConcat(v)) return this.resolveConcat(v.nodes, scope);
1533
+ if (isConcat(v)) {
1534
+ this.resolvingConcats.add(v);
1535
+ try {
1536
+ return this.resolveConcat(v.nodes, scope, v.line, v.col);
1537
+ } finally {
1538
+ this.resolvingConcats.delete(v);
1539
+ }
1540
+ }
1376
1541
  if (isAppend(v)) return this.resolveAppend(v, scope);
1377
1542
  if (isResObj(v)) return this.resolveResObj(v);
1378
1543
  const hv = v;
@@ -1386,15 +1551,12 @@ var SubstitutionResolver = class {
1386
1551
  }
1387
1552
  return hv;
1388
1553
  }
1389
- segmentsEqual(a, b) {
1390
- return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1391
- }
1392
1554
  resolveSubst(s, scope) {
1393
- const key = segmentsToKey(s.segments);
1555
+ const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1394
1556
  if (this.cache.has(key)) return this.cache.get(key);
1395
1557
  if (this.resolving.has(key)) {
1396
1558
  let prior;
1397
- if (s.prefixLen > 0) {
1559
+ if (s.segments.length > 1) {
1398
1560
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1399
1561
  const parentScope = lookupResObj(
1400
1562
  this.root,
@@ -1426,29 +1588,31 @@ var SubstitutionResolver = class {
1426
1588
  try {
1427
1589
  const found = lookupPath(this.root, s.segments);
1428
1590
  if (found !== void 0) {
1429
- if (isSubst(found) || isConcat(found)) {
1430
- const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1431
- (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1432
- );
1433
- if (isSelfRef) {
1434
- let prior;
1435
- if (s.prefixLen > 0) {
1436
- const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1437
- const parentScope = lookupResObj(
1438
- this.root,
1439
- s.segments.slice(0, s.segments.length - 1)
1440
- );
1441
- prior = parentScope?.priorValues.get(leafSeg);
1442
- } else {
1443
- const rootSeg = s.segments[0]?.text ?? "";
1444
- prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1445
- }
1446
- if (prior !== void 0) {
1447
- const result2 = this.resolveVal(prior, scope);
1448
- if (result2 !== void 0) this.cache.set(key, result2);
1449
- return result2;
1450
- }
1591
+ if (isConcat(found) && this.resolvingConcats.has(found)) {
1592
+ let prior;
1593
+ if (s.segments.length > 1) {
1594
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1595
+ const parentScope = lookupResObj(
1596
+ this.root,
1597
+ s.segments.slice(0, s.segments.length - 1)
1598
+ );
1599
+ prior = parentScope?.priorValues.get(leafSeg);
1600
+ } else {
1601
+ const rootSeg = s.segments[0]?.text ?? "";
1602
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1451
1603
  }
1604
+ if (prior !== void 0) {
1605
+ const result2 = this.resolveVal(prior, scope);
1606
+ if (result2 !== void 0) this.cache.set(key, result2);
1607
+ return result2;
1608
+ }
1609
+ if (s.optional) return void 0;
1610
+ throw new ResolveError(
1611
+ `could not resolve substitution: \${${key}}`,
1612
+ key,
1613
+ s.line,
1614
+ s.col
1615
+ );
1452
1616
  }
1453
1617
  let result = this.resolveVal(found, scope);
1454
1618
  const effectiveLen = s.segments.length - s.prefixLen;
@@ -1477,6 +1641,21 @@ var SubstitutionResolver = class {
1477
1641
  if (result !== void 0) this.cache.set(key, result);
1478
1642
  return result;
1479
1643
  }
1644
+ if (s.listSuffix) {
1645
+ const result = this.resolveEnvList(s);
1646
+ if (result !== void 0) {
1647
+ this.cache.set(key, result);
1648
+ return result;
1649
+ }
1650
+ if (s.optional) return void 0;
1651
+ const envBase = s.segments.map((seg) => seg.text).join(".");
1652
+ throw new ResolveError(
1653
+ `could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1654
+ key,
1655
+ s.line,
1656
+ s.col
1657
+ );
1658
+ }
1480
1659
  const envKey = s.segments.map((seg) => seg.text).join(".");
1481
1660
  const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1482
1661
  if (envVal !== void 0) {
@@ -1495,7 +1674,42 @@ var SubstitutionResolver = class {
1495
1674
  this.resolving.delete(key);
1496
1675
  }
1497
1676
  }
1498
- resolveConcat(nodes, scope) {
1677
+ /**
1678
+ * S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
1679
+ *
1680
+ * Candidate bases follow the same fully-qualified→bare order used by the scalar
1681
+ * env fallback. The first base whose _0 key is present wins entirely — no
1682
+ * cross-base merging (spec §Relativized fallback precedence rules).
1683
+ *
1684
+ * Returns the HoconValue Array if any candidate base has _0, or undefined if
1685
+ * none do (caller applies optional/required handling).
1686
+ */
1687
+ resolveEnvList(s) {
1688
+ const bases = this.candidateBases(s);
1689
+ for (const base of bases) {
1690
+ const key0 = base + "_0";
1691
+ if (!(key0 in this.opts.env)) continue;
1692
+ const items = [];
1693
+ for (let i = 0; ; i++) {
1694
+ const k = base + "_" + i;
1695
+ if (!(k in this.opts.env)) break;
1696
+ items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
1697
+ }
1698
+ return { kind: "array", items };
1699
+ }
1700
+ return void 0;
1701
+ }
1702
+ /**
1703
+ * Return the candidate base names for env-var-list lookup, in precedence order.
1704
+ * Mirrors the fully-qualified→bare fallback used by the scalar env path.
1705
+ */
1706
+ candidateBases(s) {
1707
+ const full = s.segments.map((seg) => seg.text).join(".");
1708
+ if (s.prefixLen === 0) return [full];
1709
+ const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
1710
+ return [full, bare];
1711
+ }
1712
+ resolveConcat(nodes, scope, line = 0, col = 0) {
1499
1713
  const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1500
1714
  if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1501
1715
  if (resolved.length === 1) return resolved[0];
@@ -1513,27 +1727,45 @@ var SubstitutionResolver = class {
1513
1727
  if (converted !== null) {
1514
1728
  return { kind: "array", items: [...left.items, ...converted] };
1515
1729
  }
1516
- return { kind: "array", items: [...left.items, right] };
1730
+ throw new ResolveError(
1731
+ "cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
1732
+ "",
1733
+ line,
1734
+ col
1735
+ );
1517
1736
  }
1518
1737
  if (left.kind === "object" && right.kind === "array") {
1519
1738
  const converted = numericObjectToArray(left);
1520
1739
  if (converted !== null) {
1521
1740
  return { kind: "array", items: [...converted, ...right.items] };
1522
1741
  }
1523
- return { kind: "array", items: [left, ...right.items] };
1742
+ throw new ResolveError(
1743
+ "cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
1744
+ "",
1745
+ line,
1746
+ col
1747
+ );
1524
1748
  }
1525
1749
  if (left.kind === "array" && right.kind === "array") {
1526
1750
  return { kind: "array", items: [...left.items, ...right.items] };
1527
1751
  }
1528
- if (left.kind === "array") {
1529
- return { kind: "array", items: [...left.items, right] };
1752
+ if (left.kind === "array" || right.kind === "array") {
1753
+ throw new ResolveError(
1754
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
1755
+ "",
1756
+ line,
1757
+ col
1758
+ );
1530
1759
  }
1531
- if (right.kind === "array") {
1532
- return { kind: "array", items: [left, ...right.items] };
1760
+ if (left.kind === "object" || right.kind === "object") {
1761
+ throw new ResolveError(
1762
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
1763
+ "",
1764
+ line,
1765
+ col
1766
+ );
1533
1767
  }
1534
- const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1535
- const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1536
- return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
1768
+ return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
1537
1769
  };
1538
1770
  const [head, ...tail] = nonSep;
1539
1771
  const folded = tail.reduce(joinPair, head);
@@ -1600,6 +1832,7 @@ var Config = class _Config {
1600
1832
  }
1601
1833
  const result = parseBytes(v.raw, unit);
1602
1834
  if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
1835
+ if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
1603
1836
  return result;
1604
1837
  }
1605
1838
  getConfig(path2) {
@@ -1739,6 +1972,7 @@ function defaultReadFileSync(filePath) {
1739
1972
  }
1740
1973
  function buildResolveContext(input, opts) {
1741
1974
  const tokens = tokenize(input);
1975
+ assertNonEmptyDocument(tokens, "input");
1742
1976
  const ast = parseTokens(tokens);
1743
1977
  const resolveOpts = {
1744
1978
  env: getEnv(opts),