@o3co/ts.hocon 1.1.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
  }
@@ -205,6 +263,20 @@ var ConfigError = class extends Error {
205
263
  };
206
264
 
207
265
  // src/internal/lexer/lexer.ts
266
+ function isHoconWhitespace(ch) {
267
+ const cp = ch.codePointAt(0) ?? -1;
268
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
269
+ if (cp >= 28 && cp <= 31) return true;
270
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
271
+ if (cp === 5760) return true;
272
+ if (cp >= 8192 && cp <= 8202) return true;
273
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
274
+ if (cp === 12288) return true;
275
+ return false;
276
+ }
277
+ function isHoconNewline(ch) {
278
+ return ch === "\n";
279
+ }
208
280
  var SINGLE_CHAR_TOKENS = {
209
281
  "{": "lbrace",
210
282
  "}": "rbrace",
@@ -227,18 +299,18 @@ var Lexer = class {
227
299
  while (this.pos < this.input.length) {
228
300
  const sl = this.line, sc = this.col;
229
301
  const ch = this.peek();
230
- if (ch === " " || ch === " " || ch === "\r") {
231
- this.advance();
232
- this.hadSpace = true;
233
- continue;
234
- }
235
- if (ch === "\n") {
302
+ if (isHoconNewline(ch)) {
236
303
  this.advance();
237
304
  if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
238
305
  this.push("newline", "\n", sl, sc);
239
306
  }
240
307
  continue;
241
308
  }
309
+ if (isHoconWhitespace(ch)) {
310
+ this.advance();
311
+ this.hadSpace = true;
312
+ continue;
313
+ }
242
314
  if (ch === "/" && this.peek(1) === "/") {
243
315
  while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
244
316
  this.hadSpace = true;
@@ -268,16 +340,16 @@ var Lexer = class {
268
340
  if (ch === "$" && this.peek(1) === "{") {
269
341
  this.advance();
270
342
  this.advance();
271
- const optional = this.peek() === "?";
272
- if (optional) this.advance();
273
- let path2 = "";
274
- while (this.pos < this.input.length && this.peek() !== "}") {
275
- if (this.peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
276
- path2 += this.advance();
277
- }
278
- if (this.peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
279
- this.advance();
280
- this.push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
343
+ const payload = this.parseSubstBody(sl, sc);
344
+ const value = payload.segments.map((s) => {
345
+ const t = s.text;
346
+ if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
347
+ const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
348
+ return `"${escaped}"`;
349
+ }
350
+ return t;
351
+ }).join(".");
352
+ this.pushSubst(payload, value, sl, sc);
281
353
  continue;
282
354
  }
283
355
  if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
@@ -312,58 +384,7 @@ var Lexer = class {
312
384
  }
313
385
  if (ch === '"') {
314
386
  this.advance();
315
- let value = "";
316
- while (this.pos < this.input.length && this.peek() !== '"') {
317
- if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
318
- if (this.peek() === "\\") {
319
- this.advance();
320
- const esc = this.advance();
321
- switch (esc) {
322
- case "n":
323
- value += "\n";
324
- break;
325
- case "t":
326
- value += " ";
327
- break;
328
- case "r":
329
- value += "\r";
330
- break;
331
- case '"':
332
- value += '"';
333
- break;
334
- case "\\":
335
- value += "\\";
336
- break;
337
- case "/":
338
- value += "/";
339
- break;
340
- case "b":
341
- value += "\b";
342
- break;
343
- case "f":
344
- value += "\f";
345
- break;
346
- case "u": {
347
- if (this.pos + 4 > this.input.length) {
348
- throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
349
- }
350
- const hex = this.input.slice(this.pos, this.pos + 4);
351
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
352
- throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
353
- }
354
- value += String.fromCharCode(parseInt(hex, 16));
355
- for (let i = 0; i < 4; i++) this.advance();
356
- break;
357
- }
358
- default:
359
- throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
360
- }
361
- } else {
362
- value += this.advance();
363
- }
364
- }
365
- if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
366
- this.advance();
387
+ const value = this.readQuotedStringBody(sl, sc);
367
388
  this.push("string", value, sl, sc, true);
368
389
  continue;
369
390
  }
@@ -397,22 +418,255 @@ var Lexer = class {
397
418
  this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
398
419
  this.hadSpace = false;
399
420
  }
421
+ pushSubst(payload, value, l, c) {
422
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
423
+ this.hadSpace = false;
424
+ }
425
+ /**
426
+ * Read body of a quoted string. Opening `"` must already be consumed.
427
+ * Returns decoded string. Throws ParseError on unterminated/invalid-escape.
428
+ * openLine/openCol are the position of the opening `"` for error reporting.
429
+ */
430
+ readQuotedStringBody(openLine, openCol) {
431
+ let value = "";
432
+ while (this.pos < this.input.length && this.peek() !== '"') {
433
+ if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
434
+ if (this.peek() === "\\") {
435
+ const escCol = this.col;
436
+ this.advance();
437
+ if (this.pos >= this.input.length) {
438
+ throw new ParseError("unterminated string", openLine, openCol);
439
+ }
440
+ const esc = this.advance();
441
+ switch (esc) {
442
+ case "n":
443
+ value += "\n";
444
+ break;
445
+ case "t":
446
+ value += " ";
447
+ break;
448
+ case "r":
449
+ value += "\r";
450
+ break;
451
+ case '"':
452
+ value += '"';
453
+ break;
454
+ case "\\":
455
+ value += "\\";
456
+ break;
457
+ case "/":
458
+ value += "/";
459
+ break;
460
+ case "b":
461
+ value += "\b";
462
+ break;
463
+ case "f":
464
+ value += "\f";
465
+ break;
466
+ case "u": {
467
+ if (this.pos + 4 > this.input.length) {
468
+ throw new ParseError("invalid unicode escape", openLine, escCol);
469
+ }
470
+ const hex = this.input.slice(this.pos, this.pos + 4);
471
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
472
+ throw new ParseError("invalid unicode escape", openLine, escCol);
473
+ }
474
+ const code = parseInt(hex, 16);
475
+ value += String.fromCharCode(code);
476
+ for (let i = 0; i < 4; i++) this.advance();
477
+ break;
478
+ }
479
+ default:
480
+ throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
481
+ }
482
+ } else {
483
+ value += this.advance();
484
+ }
485
+ }
486
+ if (this.pos >= this.input.length || this.peek() !== '"') {
487
+ throw new ParseError("unterminated string", openLine, openCol);
488
+ }
489
+ this.advance();
490
+ return value;
491
+ }
492
+ /**
493
+ * Parse the body of a `${...}` substitution (called after `${` has been consumed).
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).
499
+ */
500
+ parseSubstBody(startLine, startCol) {
501
+ let optional = false;
502
+ if (this.peek() === "?") {
503
+ this.advance();
504
+ optional = true;
505
+ }
506
+ let curText = "";
507
+ let curStarted = false;
508
+ let curLine = 0;
509
+ let curCol = 0;
510
+ let pendingWs = "";
511
+ const segments = [];
512
+ let lastDot = null;
513
+ let listSuffix = false;
514
+ while (true) {
515
+ if (this.pos >= this.input.length) {
516
+ throw new ParseError("unterminated substitution", startLine, startCol);
517
+ }
518
+ const ch = this.peek();
519
+ if (ch === "}") {
520
+ this.advance();
521
+ pendingWs = "";
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;
540
+ } else if (ch === '"') {
541
+ const qLine = startLine;
542
+ const qCol = this.col;
543
+ if (curStarted) {
544
+ curText += pendingWs;
545
+ }
546
+ pendingWs = "";
547
+ this.advance();
548
+ const decoded = this.readQuotedStringBody(qLine, qCol);
549
+ curText += decoded;
550
+ if (!curStarted) {
551
+ curLine = qLine;
552
+ curCol = qCol;
553
+ curStarted = true;
554
+ }
555
+ } else if (isUnquotedSubstChar(ch)) {
556
+ if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
557
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
558
+ throw new ParseError(
559
+ `unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
560
+ startLine,
561
+ this.col
562
+ );
563
+ }
564
+ const uCol = this.col;
565
+ if (curStarted) {
566
+ curText += pendingWs;
567
+ }
568
+ pendingWs = "";
569
+ if (!curStarted) {
570
+ curLine = startLine;
571
+ curCol = uCol;
572
+ curStarted = true;
573
+ }
574
+ while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
575
+ curText += this.advance();
576
+ }
577
+ } else if (ch === ".") {
578
+ const dotCol = this.col;
579
+ pendingWs = "";
580
+ if (!curStarted) {
581
+ throw new ParseError("empty segment in path", startLine, dotCol);
582
+ }
583
+ segments.push({ text: curText, line: curLine, col: curCol });
584
+ curText = "";
585
+ curStarted = false;
586
+ curLine = 0;
587
+ curCol = 0;
588
+ lastDot = [startLine, dotCol];
589
+ this.advance();
590
+ } else if (isHoconNewline(ch)) {
591
+ throw new ParseError("unterminated substitution", startLine, startCol);
592
+ } else if (isHoconWhitespace(ch)) {
593
+ pendingWs += ch;
594
+ this.advance();
595
+ } else {
596
+ throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
597
+ }
598
+ }
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
+ }
608
+ }
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;
640
+ }
400
641
  };
401
642
  function tokenize(input) {
402
643
  return new Lexer(input).tokenize();
403
644
  }
645
+ function isUnquotedSubstChar(ch) {
646
+ if (ch === "" || isHoconWhitespace(ch)) return false;
647
+ if ('"\\'.includes(ch)) return false;
648
+ if ("{}[]".includes(ch)) return false;
649
+ if (":=,+#`^?!@*&$.".includes(ch)) return false;
650
+ return true;
651
+ }
652
+ function isDecimalDigit(ch) {
653
+ return ch >= "0" && ch <= "9";
654
+ }
404
655
  function isUnquotedStart(ch) {
405
- return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
656
+ if (ch === "" || isHoconWhitespace(ch)) return false;
657
+ if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
658
+ return true;
406
659
  }
407
660
  function isUnquotedContinue(ch, nextFn) {
408
- if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
661
+ if (ch === "" || isHoconWhitespace(ch)) return false;
662
+ if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
409
663
  if (ch === "+" && nextFn() === "=") return false;
410
664
  if (ch === "/" && nextFn() === "/") return false;
411
665
  return true;
412
666
  }
413
667
 
414
668
  // src/internal/parser/parser.ts
415
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
669
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
416
670
  var Parser = class {
417
671
  constructor(tokens) {
418
672
  this.tokens = tokens;
@@ -470,6 +724,14 @@ var Parser = class {
470
724
  const t = this.peek();
471
725
  if (t.kind === "eof" || t.kind === "rbrace") break;
472
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
+ }
473
735
  this.advance();
474
736
  fields.push(this.parseInclude());
475
737
  this.skip("newline");
@@ -478,7 +740,14 @@ var Parser = class {
478
740
  continue;
479
741
  }
480
742
  const keyPos = this.currentPos();
481
- 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
+ }
482
751
  this.skip("newline");
483
752
  let append = false;
484
753
  const sep = this.peek();
@@ -508,18 +777,31 @@ var Parser = class {
508
777
  parseKey() {
509
778
  const segments = [];
510
779
  let trailingDot = false;
780
+ let firstWasQuoted = false;
511
781
  while (true) {
512
782
  const t = this.peek();
513
783
  if (t.kind === "string") {
514
784
  this.advance();
785
+ if (segments.length === 0) firstWasQuoted = true;
515
786
  segments.push(t.value);
516
787
  trailingDot = false;
517
788
  } else if (t.kind === "unquoted") {
518
789
  this.advance();
519
- const parts = t.value.split(".");
790
+ const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
791
+ const parts = raw.split(".");
520
792
  const filtered = parts.filter((s) => s.length > 0);
793
+ for (const seg of filtered) {
794
+ if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
795
+ const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
796
+ throw new ParseError(
797
+ `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
798
+ t.line,
799
+ t.col
800
+ );
801
+ }
802
+ }
521
803
  segments.push(...filtered);
522
- trailingDot = t.value.endsWith(".");
804
+ trailingDot = raw.endsWith(".");
523
805
  } else {
524
806
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
525
807
  break;
@@ -531,9 +813,13 @@ var Parser = class {
531
813
  trailingDot = true;
532
814
  continue;
533
815
  }
816
+ if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
817
+ trailingDot = true;
818
+ continue;
819
+ }
534
820
  break;
535
821
  }
536
- return segments;
822
+ return { segments, firstWasQuoted };
537
823
  }
538
824
  parseInclude() {
539
825
  const p = this.currentPos();
@@ -606,9 +892,13 @@ var Parser = class {
606
892
  } else if (t.kind === "lbracket") {
607
893
  this.advance();
608
894
  node = this.parseArray();
609
- } else if (t.kind === "subst" || t.kind === "opt_subst") {
895
+ } else if (t.kind === "subst") {
610
896
  this.advance();
611
- node = { kind: "subst", path: t.value, optional: t.kind === "opt_subst", pos: { line: t.line, col: t.col } };
897
+ if (!t.subst) {
898
+ throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
899
+ }
900
+ const payload = t.subst;
901
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
612
902
  } else if (t.kind === "string" || t.kind === "triple_string") {
613
903
  this.advance();
614
904
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -659,6 +949,20 @@ function parseTokens(tokens) {
659
949
  return new Parser(tokens).parse();
660
950
  }
661
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
+
662
966
  // src/internal/resolver/types.ts
663
967
  var separatorValues = /* @__PURE__ */ new WeakSet();
664
968
  function isSubst(v) {
@@ -678,64 +982,20 @@ function makeResObj() {
678
982
  }
679
983
 
680
984
  // src/internal/resolver/utils.ts
681
- function parseSubstPath(raw) {
682
- const segments = [];
683
- let i = 0;
684
- while (i < raw.length) {
685
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
686
- if (i >= raw.length) break;
687
- if (raw[i] === '"') {
688
- i++;
689
- let seg = "";
690
- while (i < raw.length && raw[i] !== '"') {
691
- if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
692
- seg += raw[i + 1];
693
- i += 2;
694
- } else {
695
- seg += raw[i];
696
- i++;
697
- }
698
- }
699
- if (i < raw.length) i++;
700
- segments.push(seg);
701
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
702
- if (i < raw.length && raw[i] === ".") i++;
703
- } else if (raw[i] === ".") {
704
- segments.push("");
705
- i++;
706
- } else {
707
- let seg = "";
708
- while (i < raw.length && raw[i] !== ".") {
709
- seg += raw[i];
710
- i++;
711
- }
712
- segments.push(seg.trim());
713
- if (i < raw.length && raw[i] === ".") i++;
714
- }
715
- }
716
- return segments;
717
- }
718
985
  function segmentsToKey(segments) {
719
986
  return segments.map((s) => {
720
- if (s === "" || /[^a-zA-Z0-9\-_]/.test(s)) {
721
- return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
987
+ const t = s.text;
988
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
989
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
722
990
  }
723
- return s;
991
+ return t;
724
992
  }).join(".");
725
993
  }
726
994
  function lookupPath(root, segments) {
727
995
  const [head, ...tail] = segments;
728
- if (head === void 0 || head === "") {
729
- if (segments.length > 0) {
730
- const val2 = root.fields.get("");
731
- if (val2 === void 0) return void 0;
732
- if (tail.length === 0) return val2;
733
- if (isResObj(val2)) return lookupPath(val2, tail);
734
- return void 0;
735
- }
736
- return void 0;
737
- }
738
- const val = root.fields.get(head);
996
+ if (head === void 0) return void 0;
997
+ const key = head.text;
998
+ const val = root.fields.get(key);
739
999
  if (val === void 0) return void 0;
740
1000
  if (tail.length === 0) return val;
741
1001
  if (isResObj(val)) return lookupPath(val, tail);
@@ -744,7 +1004,7 @@ function lookupPath(root, segments) {
744
1004
  function lookupResObj(root, segments) {
745
1005
  let cur = root;
746
1006
  for (const seg of segments) {
747
- const val = cur.fields.get(seg);
1007
+ const val = cur.fields.get(seg.text);
748
1008
  if (val === void 0 || !isResObj(val)) return void 0;
749
1009
  cur = val;
750
1010
  }
@@ -804,6 +1064,7 @@ var nodePath = __toESM(require("path"), 1);
804
1064
  // src/internal/properties/properties.ts
805
1065
  function parseProperties(input) {
806
1066
  const root = /* @__PURE__ */ Object.create(null);
1067
+ const pairs = [];
807
1068
  for (const line of input.split("\n")) {
808
1069
  const trimmed = line.trim();
809
1070
  if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
@@ -812,6 +1073,10 @@ function parseProperties(input) {
812
1073
  const key = trimmed.slice(0, sepIdx).trim();
813
1074
  const value = trimmed.slice(sepIdx + 1).trim();
814
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) {
815
1080
  setNested(root, key.split("."), value);
816
1081
  }
817
1082
  return root;
@@ -835,6 +1100,7 @@ function setNested(obj, segments, value) {
835
1100
  }
836
1101
  const last = segments[segments.length - 1];
837
1102
  if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1103
+ if (typeof current[last] === "object" && current[last] !== null) return;
838
1104
  current[last] = value;
839
1105
  }
840
1106
  function propertiesToHoconValue(input) {
@@ -948,7 +1214,9 @@ var IncludeLoader = class {
948
1214
  if (candidate.endsWith(".properties")) {
949
1215
  return hoconValueToResObj(propertiesToHoconValue(content));
950
1216
  }
951
- const ast = parseTokens(tokenize(content));
1217
+ const tokens = tokenize(content);
1218
+ assertNonEmptyDocument(tokens, candidate);
1219
+ const ast = parseTokens(tokens);
952
1220
  return this.onBuildResObj(ast, {
953
1221
  env,
954
1222
  baseDir: nodePath.dirname(candidate),
@@ -972,7 +1240,9 @@ var IncludeLoader = class {
972
1240
  if (candidate.endsWith(".properties")) {
973
1241
  return hoconValueToResObj(propertiesToHoconValue(content));
974
1242
  }
975
- const ast = parseTokens(tokenize(content));
1243
+ const tokens = tokenize(content);
1244
+ assertNonEmptyDocument(tokens, candidate);
1245
+ const ast = parseTokens(tokens);
976
1246
  return this.onBuildResObjAsync(ast, {
977
1247
  env,
978
1248
  baseDir: nodePath.dirname(candidate),
@@ -1021,7 +1291,7 @@ var StructureBuilder = class _StructureBuilder {
1021
1291
  return;
1022
1292
  }
1023
1293
  const [head, ...tail] = field.key;
1024
- if (!head) return;
1294
+ if (head === void 0) return;
1025
1295
  if (tail.length > 0) {
1026
1296
  const syntheticAst = {
1027
1297
  kind: "object",
@@ -1063,7 +1333,7 @@ var StructureBuilder = class _StructureBuilder {
1063
1333
  return;
1064
1334
  }
1065
1335
  const [head, ...tail] = field.key;
1066
- if (!head) return;
1336
+ if (head === void 0) return;
1067
1337
  if (tail.length > 0) {
1068
1338
  const syntheticAst = {
1069
1339
  kind: "object",
@@ -1109,9 +1379,9 @@ var StructureBuilder = class _StructureBuilder {
1109
1379
  return inner;
1110
1380
  }
1111
1381
  case "subst":
1112
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), 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 };
1113
1383
  case "concat":
1114
- 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 };
1115
1385
  case "include":
1116
1386
  return { kind: "scalar", raw: "null", valueType: "null" };
1117
1387
  }
@@ -1133,13 +1403,13 @@ var StructureBuilder = class _StructureBuilder {
1133
1403
  case "object":
1134
1404
  return await this.buildAsync(ast, pathPrefix);
1135
1405
  case "subst":
1136
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), 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 };
1137
1407
  case "concat": {
1138
1408
  const nodes = [];
1139
1409
  for (const n of ast.nodes) {
1140
1410
  nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
1141
1411
  }
1142
- return { _kind: "concat-placeholder", nodes };
1412
+ return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
1143
1413
  }
1144
1414
  case "include":
1145
1415
  return { kind: "scalar", raw: "null", valueType: "null" };
@@ -1148,7 +1418,8 @@ var StructureBuilder = class _StructureBuilder {
1148
1418
  // ---- Relativize substitution paths for nested includes ----
1149
1419
  relativizeSubstPaths(val, prefixSegments) {
1150
1420
  if (isSubst(val)) {
1151
- val.segments = [...prefixSegments, ...val.segments];
1421
+ const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
1422
+ val.segments = [...prefixAsSegments, ...val.segments];
1152
1423
  val.prefixLen += prefixSegments.length;
1153
1424
  return;
1154
1425
  }
@@ -1184,6 +1455,23 @@ var StructureBuilder = class _StructureBuilder {
1184
1455
  }
1185
1456
  };
1186
1457
 
1458
+ // src/value/numeric-array.ts
1459
+ function numericObjectToArray(value) {
1460
+ if (value.kind !== "object") return null;
1461
+ if (value.fields.size === 0) return null;
1462
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
1463
+ const eligible = [];
1464
+ for (const [key, val] of value.fields) {
1465
+ if (!canonicalInt.test(key)) continue;
1466
+ const n = Number(key);
1467
+ if (!Number.isInteger(n) || n > 2147483647) continue;
1468
+ eligible.push({ n, v: val });
1469
+ }
1470
+ if (eligible.length === 0) return null;
1471
+ eligible.sort((a, b) => a.n - b.n);
1472
+ return eligible.map((e) => e.v);
1473
+ }
1474
+
1187
1475
  // src/internal/resolver/substitution-resolver.ts
1188
1476
  var SubstitutionResolver = class {
1189
1477
  constructor(root, opts) {
@@ -1191,6 +1479,19 @@ var SubstitutionResolver = class {
1191
1479
  this.opts = opts;
1192
1480
  this.resolving = /* @__PURE__ */ new Set();
1193
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();
1194
1495
  }
1195
1496
  resolve() {
1196
1497
  return this.resolveResObj(this.root);
@@ -1229,7 +1530,14 @@ var SubstitutionResolver = class {
1229
1530
  }
1230
1531
  resolveVal(v, scope) {
1231
1532
  if (isSubst(v)) return this.resolveSubst(v, scope);
1232
- 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
+ }
1233
1541
  if (isAppend(v)) return this.resolveAppend(v, scope);
1234
1542
  if (isResObj(v)) return this.resolveResObj(v);
1235
1543
  const hv = v;
@@ -1243,23 +1551,20 @@ var SubstitutionResolver = class {
1243
1551
  }
1244
1552
  return hv;
1245
1553
  }
1246
- segmentsEqual(a, b) {
1247
- return a.length === b.length && a.every((seg, i) => seg === b[i]);
1248
- }
1249
1554
  resolveSubst(s, scope) {
1250
- const key = segmentsToKey(s.segments);
1555
+ const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1251
1556
  if (this.cache.has(key)) return this.cache.get(key);
1252
1557
  if (this.resolving.has(key)) {
1253
1558
  let prior;
1254
- if (s.prefixLen > 0) {
1255
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1559
+ if (s.segments.length > 1) {
1560
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1256
1561
  const parentScope = lookupResObj(
1257
1562
  this.root,
1258
1563
  s.segments.slice(0, s.segments.length - 1)
1259
1564
  );
1260
1565
  prior = parentScope?.priorValues.get(leafSeg);
1261
1566
  } else {
1262
- const rootSeg = s.segments[0] ?? "";
1567
+ const rootSeg = s.segments[0]?.text ?? "";
1263
1568
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1264
1569
  }
1265
1570
  if (prior !== void 0) {
@@ -1283,34 +1588,36 @@ var SubstitutionResolver = class {
1283
1588
  try {
1284
1589
  const found = lookupPath(this.root, s.segments);
1285
1590
  if (found !== void 0) {
1286
- if (isSubst(found) || isConcat(found)) {
1287
- const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1288
- (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1289
- );
1290
- if (isSelfRef) {
1291
- let prior;
1292
- if (s.prefixLen > 0) {
1293
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1294
- const parentScope = lookupResObj(
1295
- this.root,
1296
- s.segments.slice(0, s.segments.length - 1)
1297
- );
1298
- prior = parentScope?.priorValues.get(leafSeg);
1299
- } else {
1300
- const rootSeg = s.segments[0] ?? "";
1301
- prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1302
- }
1303
- if (prior !== void 0) {
1304
- const result2 = this.resolveVal(prior, scope);
1305
- if (result2 !== void 0) this.cache.set(key, result2);
1306
- return result2;
1307
- }
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);
1308
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
+ );
1309
1616
  }
1310
1617
  let result = this.resolveVal(found, scope);
1311
1618
  const effectiveLen = s.segments.length - s.prefixLen;
1312
1619
  if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1313
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1620
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1314
1621
  let prior;
1315
1622
  if (s.prefixLen > 0) {
1316
1623
  const parentScope = lookupResObj(
@@ -1334,8 +1641,23 @@ var SubstitutionResolver = class {
1334
1641
  if (result !== void 0) this.cache.set(key, result);
1335
1642
  return result;
1336
1643
  }
1337
- const envKey = s.segments.join(".");
1338
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).join(".")] : void 0);
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
+ }
1659
+ const envKey = s.segments.map((seg) => seg.text).join(".");
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);
1339
1661
  if (envVal !== void 0) {
1340
1662
  const result = { kind: "scalar", raw: envVal, valueType: "string" };
1341
1663
  this.cache.set(key, result);
@@ -1352,37 +1674,106 @@ var SubstitutionResolver = class {
1352
1674
  this.resolving.delete(key);
1353
1675
  }
1354
1676
  }
1355
- 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) {
1356
1713
  const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1357
1714
  if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1358
1715
  if (resolved.length === 1) return resolved[0];
1359
1716
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1360
- if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1361
- const merged = /* @__PURE__ */ new Map();
1362
- for (const v of nonSep) {
1363
- if (v.kind === "object") {
1364
- for (const [k, val] of v.fields) {
1365
- const existing = merged.get(k);
1366
- if (existing?.kind === "object" && val.kind === "object") {
1367
- merged.set(k, deepMergeHoconValues(existing, val));
1368
- } else {
1369
- merged.set(k, val);
1370
- }
1371
- }
1717
+ if (nonSep.length === 0) {
1718
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1719
+ return { kind: "scalar", raw: str, valueType: "string" };
1720
+ }
1721
+ const joinPair = (left, right) => {
1722
+ if (left.kind === "object" && right.kind === "object") {
1723
+ return deepMergeHoconValues(left, right);
1724
+ }
1725
+ if (left.kind === "array" && right.kind === "object") {
1726
+ const converted = numericObjectToArray(right);
1727
+ if (converted !== null) {
1728
+ return { kind: "array", items: [...left.items, ...converted] };
1372
1729
  }
1730
+ throw new ResolveError(
1731
+ "cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
1732
+ "",
1733
+ line,
1734
+ col
1735
+ );
1373
1736
  }
1374
- return { kind: "object", fields: merged };
1375
- }
1376
- if (resolved.some((v) => v.kind === "array")) {
1377
- const items = [];
1378
- for (const v of resolved) {
1379
- if (v.kind === "array") items.push(...v.items);
1380
- else items.push(v);
1737
+ if (left.kind === "object" && right.kind === "array") {
1738
+ const converted = numericObjectToArray(left);
1739
+ if (converted !== null) {
1740
+ return { kind: "array", items: [...converted, ...right.items] };
1741
+ }
1742
+ throw new ResolveError(
1743
+ "cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
1744
+ "",
1745
+ line,
1746
+ col
1747
+ );
1381
1748
  }
1382
- return { kind: "array", items };
1749
+ if (left.kind === "array" && right.kind === "array") {
1750
+ return { kind: "array", items: [...left.items, ...right.items] };
1751
+ }
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
+ );
1759
+ }
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
+ );
1767
+ }
1768
+ return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
1769
+ };
1770
+ const [head, ...tail] = nonSep;
1771
+ const folded = tail.reduce(joinPair, head);
1772
+ if (folded.kind === "scalar") {
1773
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1774
+ return { kind: "scalar", raw: str, valueType: "string" };
1383
1775
  }
1384
- const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1385
- return { kind: "scalar", raw: str, valueType: "string" };
1776
+ return folded;
1386
1777
  }
1387
1778
  resolveAppend(a, scope) {
1388
1779
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
@@ -1441,6 +1832,7 @@ var Config = class _Config {
1441
1832
  }
1442
1833
  const result = parseBytes(v.raw, unit);
1443
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);
1444
1836
  return result;
1445
1837
  }
1446
1838
  getConfig(path2) {
@@ -1452,6 +1844,10 @@ var Config = class _Config {
1452
1844
  getList(path2) {
1453
1845
  const v = this.lookupNode(path2);
1454
1846
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
1847
+ if (v.kind === "object") {
1848
+ const converted = numericObjectToArray(v);
1849
+ if (converted !== null) return converted.map(hoconToJs);
1850
+ }
1455
1851
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
1456
1852
  return v.items.map(hoconToJs);
1457
1853
  }
@@ -1576,6 +1972,7 @@ function defaultReadFileSync(filePath) {
1576
1972
  }
1577
1973
  function buildResolveContext(input, opts) {
1578
1974
  const tokens = tokenize(input);
1975
+ assertNonEmptyDocument(tokens, "input");
1579
1976
  const ast = parseTokens(tokens);
1580
1977
  const resolveOpts = {
1581
1978
  env: getEnv(opts),