@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.js CHANGED
@@ -1,5 +1,26 @@
1
1
  // src/coerce.ts
2
2
  var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
3
+ function isHoconWs(cp) {
4
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
5
+ if (cp >= 28 && cp <= 31) return true;
6
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
7
+ if (cp === 5760) return true;
8
+ if (cp >= 8192 && cp <= 8202) return true;
9
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
10
+ if (cp === 12288) return true;
11
+ return false;
12
+ }
13
+ function trimHoconWs(s) {
14
+ let start = 0;
15
+ let end = s.length;
16
+ while (start < end && isHoconWs(s.charCodeAt(start))) {
17
+ start++;
18
+ }
19
+ while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
20
+ end--;
21
+ }
22
+ return s.slice(start, end);
23
+ }
3
24
  var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
4
25
  var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
5
26
  function coerceBoolean(value) {
@@ -45,22 +66,29 @@ var OUTPUT_DURATION_UNITS = {
45
66
  d: 864e5
46
67
  };
47
68
  function parseDuration(value, outputUnit = "ms") {
48
- const trimmed = value.trim();
69
+ const trimmed = trimHoconWs(value);
49
70
  let i = 0;
50
71
  while (i < trimmed.length) {
51
72
  const ch = trimmed[i];
52
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
73
+ if (i === 0 && (ch === "-" || ch === "+")) {
74
+ i++;
75
+ continue;
76
+ }
77
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
53
78
  i++;
54
79
  }
55
80
  if (i === 0) return NaN;
56
81
  const num = Number(trimmed.slice(0, i));
57
82
  if (Number.isNaN(num)) return NaN;
58
- const unit = trimmed.slice(i).trim().toLowerCase();
83
+ const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
84
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
85
+ if (divisor === void 0) return NaN;
86
+ if (unit === "") {
87
+ return num / divisor;
88
+ }
59
89
  const mult = DURATION_UNITS[unit];
60
90
  if (mult === void 0) return NaN;
61
91
  const ms = num * mult;
62
- const divisor = OUTPUT_DURATION_UNITS[outputUnit];
63
- if (divisor === void 0) return NaN;
64
92
  return ms / divisor;
65
93
  }
66
94
  var BYTE_UNITS = {
@@ -91,6 +119,22 @@ var BYTE_UNITS = {
91
119
  TiB: 1099511627776,
92
120
  tebibyte: 1099511627776,
93
121
  tebibytes: 1099511627776,
122
+ // S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
123
+ // Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
124
+ // Both upper- and lowercase accepted per Lightbend behaviour.
125
+ // Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
126
+ K: 1024,
127
+ k: 1024,
128
+ M: 1024 ** 2,
129
+ m: 1024 ** 2,
130
+ G: 1024 ** 3,
131
+ g: 1024 ** 3,
132
+ T: 1024 ** 4,
133
+ t: 1024 ** 4,
134
+ P: 1024 ** 5,
135
+ p: 1024 ** 5,
136
+ E: 1024 ** 6,
137
+ e: 1024 ** 6,
94
138
  // lowercase short-form aliases
95
139
  b: 1,
96
140
  kb: 1e3,
@@ -114,25 +158,39 @@ var OUTPUT_BYTE_UNITS = {
114
158
  TiB: 1099511627776
115
159
  };
116
160
  function parseBytes(value, outputUnit = "B") {
117
- const trimmed = value.trim();
161
+ const trimmed = trimHoconWs(value);
118
162
  let i = 0;
119
163
  while (i < trimmed.length) {
120
164
  const ch = trimmed[i];
121
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
165
+ if (i === 0 && (ch === "-" || ch === "+")) {
166
+ i++;
167
+ continue;
168
+ }
169
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
122
170
  i++;
123
171
  }
124
172
  if (i === 0) return NaN;
125
173
  const num = Number(trimmed.slice(0, i));
126
174
  if (Number.isNaN(num)) return NaN;
127
- const unit = trimmed.slice(i).trim();
175
+ const unit = trimHoconWs(trimmed.slice(i));
176
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
177
+ if (divisor === void 0) return NaN;
178
+ if (unit === "") {
179
+ const bytes2 = Math.trunc(num);
180
+ if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
181
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
182
+ }
183
+ return bytes2 / divisor;
184
+ }
128
185
  let mult = BYTE_UNITS[unit];
129
186
  if (mult === void 0) {
130
187
  mult = BYTE_UNITS[unit.toLowerCase()];
131
188
  }
132
189
  if (mult === void 0) return NaN;
133
190
  const bytes = num * mult;
134
- const divisor = OUTPUT_BYTE_UNITS[outputUnit];
135
- if (divisor === void 0) return NaN;
191
+ if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
192
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
193
+ }
136
194
  const result = bytes / divisor;
137
195
  return outputUnit === "B" ? Math.round(result) : result;
138
196
  }
@@ -166,6 +224,20 @@ var ConfigError = class extends Error {
166
224
  };
167
225
 
168
226
  // src/internal/lexer/lexer.ts
227
+ function isHoconWhitespace(ch) {
228
+ const cp = ch.codePointAt(0) ?? -1;
229
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
230
+ if (cp >= 28 && cp <= 31) return true;
231
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
232
+ if (cp === 5760) return true;
233
+ if (cp >= 8192 && cp <= 8202) return true;
234
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
235
+ if (cp === 12288) return true;
236
+ return false;
237
+ }
238
+ function isHoconNewline(ch) {
239
+ return ch === "\n";
240
+ }
169
241
  var SINGLE_CHAR_TOKENS = {
170
242
  "{": "lbrace",
171
243
  "}": "rbrace",
@@ -188,18 +260,18 @@ var Lexer = class {
188
260
  while (this.pos < this.input.length) {
189
261
  const sl = this.line, sc = this.col;
190
262
  const ch = this.peek();
191
- if (ch === " " || ch === " " || ch === "\r") {
192
- this.advance();
193
- this.hadSpace = true;
194
- continue;
195
- }
196
- if (ch === "\n") {
263
+ if (isHoconNewline(ch)) {
197
264
  this.advance();
198
265
  if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
199
266
  this.push("newline", "\n", sl, sc);
200
267
  }
201
268
  continue;
202
269
  }
270
+ if (isHoconWhitespace(ch)) {
271
+ this.advance();
272
+ this.hadSpace = true;
273
+ continue;
274
+ }
203
275
  if (ch === "/" && this.peek(1) === "/") {
204
276
  while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
205
277
  this.hadSpace = true;
@@ -229,16 +301,16 @@ var Lexer = class {
229
301
  if (ch === "$" && this.peek(1) === "{") {
230
302
  this.advance();
231
303
  this.advance();
232
- const optional = this.peek() === "?";
233
- if (optional) this.advance();
234
- let path2 = "";
235
- while (this.pos < this.input.length && this.peek() !== "}") {
236
- if (this.peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
237
- path2 += this.advance();
238
- }
239
- if (this.peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
240
- this.advance();
241
- this.push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
304
+ const payload = this.parseSubstBody(sl, sc);
305
+ const value = payload.segments.map((s) => {
306
+ const t = s.text;
307
+ if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
308
+ const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
309
+ return `"${escaped}"`;
310
+ }
311
+ return t;
312
+ }).join(".");
313
+ this.pushSubst(payload, value, sl, sc);
242
314
  continue;
243
315
  }
244
316
  if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
@@ -273,58 +345,7 @@ var Lexer = class {
273
345
  }
274
346
  if (ch === '"') {
275
347
  this.advance();
276
- let value = "";
277
- while (this.pos < this.input.length && this.peek() !== '"') {
278
- if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
279
- if (this.peek() === "\\") {
280
- this.advance();
281
- const esc = this.advance();
282
- switch (esc) {
283
- case "n":
284
- value += "\n";
285
- break;
286
- case "t":
287
- value += " ";
288
- break;
289
- case "r":
290
- value += "\r";
291
- break;
292
- case '"':
293
- value += '"';
294
- break;
295
- case "\\":
296
- value += "\\";
297
- break;
298
- case "/":
299
- value += "/";
300
- break;
301
- case "b":
302
- value += "\b";
303
- break;
304
- case "f":
305
- value += "\f";
306
- break;
307
- case "u": {
308
- if (this.pos + 4 > this.input.length) {
309
- throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
310
- }
311
- const hex = this.input.slice(this.pos, this.pos + 4);
312
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
313
- throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
314
- }
315
- value += String.fromCharCode(parseInt(hex, 16));
316
- for (let i = 0; i < 4; i++) this.advance();
317
- break;
318
- }
319
- default:
320
- throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
321
- }
322
- } else {
323
- value += this.advance();
324
- }
325
- }
326
- if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
327
- this.advance();
348
+ const value = this.readQuotedStringBody(sl, sc);
328
349
  this.push("string", value, sl, sc, true);
329
350
  continue;
330
351
  }
@@ -358,22 +379,255 @@ var Lexer = class {
358
379
  this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
359
380
  this.hadSpace = false;
360
381
  }
382
+ pushSubst(payload, value, l, c) {
383
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
384
+ this.hadSpace = false;
385
+ }
386
+ /**
387
+ * Read body of a quoted string. Opening `"` must already be consumed.
388
+ * Returns decoded string. Throws ParseError on unterminated/invalid-escape.
389
+ * openLine/openCol are the position of the opening `"` for error reporting.
390
+ */
391
+ readQuotedStringBody(openLine, openCol) {
392
+ let value = "";
393
+ while (this.pos < this.input.length && this.peek() !== '"') {
394
+ if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
395
+ if (this.peek() === "\\") {
396
+ const escCol = this.col;
397
+ this.advance();
398
+ if (this.pos >= this.input.length) {
399
+ throw new ParseError("unterminated string", openLine, openCol);
400
+ }
401
+ const esc = this.advance();
402
+ switch (esc) {
403
+ case "n":
404
+ value += "\n";
405
+ break;
406
+ case "t":
407
+ value += " ";
408
+ break;
409
+ case "r":
410
+ value += "\r";
411
+ break;
412
+ case '"':
413
+ value += '"';
414
+ break;
415
+ case "\\":
416
+ value += "\\";
417
+ break;
418
+ case "/":
419
+ value += "/";
420
+ break;
421
+ case "b":
422
+ value += "\b";
423
+ break;
424
+ case "f":
425
+ value += "\f";
426
+ break;
427
+ case "u": {
428
+ if (this.pos + 4 > this.input.length) {
429
+ throw new ParseError("invalid unicode escape", openLine, escCol);
430
+ }
431
+ const hex = this.input.slice(this.pos, this.pos + 4);
432
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
433
+ throw new ParseError("invalid unicode escape", openLine, escCol);
434
+ }
435
+ const code = parseInt(hex, 16);
436
+ value += String.fromCharCode(code);
437
+ for (let i = 0; i < 4; i++) this.advance();
438
+ break;
439
+ }
440
+ default:
441
+ throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
442
+ }
443
+ } else {
444
+ value += this.advance();
445
+ }
446
+ }
447
+ if (this.pos >= this.input.length || this.peek() !== '"') {
448
+ throw new ParseError("unterminated string", openLine, openCol);
449
+ }
450
+ this.advance();
451
+ return value;
452
+ }
453
+ /**
454
+ * Parse the body of a `${...}` substitution (called after `${` has been consumed).
455
+ * Implements Appendix A state machine from the cross-language spec.
456
+ *
457
+ * S13c extension: if the segment-collection loop encounters `[`, it delegates to
458
+ * `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
459
+ * ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
460
+ */
461
+ parseSubstBody(startLine, startCol) {
462
+ let optional = false;
463
+ if (this.peek() === "?") {
464
+ this.advance();
465
+ optional = true;
466
+ }
467
+ let curText = "";
468
+ let curStarted = false;
469
+ let curLine = 0;
470
+ let curCol = 0;
471
+ let pendingWs = "";
472
+ const segments = [];
473
+ let lastDot = null;
474
+ let listSuffix = false;
475
+ while (true) {
476
+ if (this.pos >= this.input.length) {
477
+ throw new ParseError("unterminated substitution", startLine, startCol);
478
+ }
479
+ const ch = this.peek();
480
+ if (ch === "}") {
481
+ this.advance();
482
+ pendingWs = "";
483
+ break;
484
+ } else if (ch === "[") {
485
+ if (!curStarted) {
486
+ throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
487
+ }
488
+ for (const w of pendingWs) {
489
+ if (w !== " " && w !== " ") {
490
+ throw new ParseError(
491
+ `only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
492
+ startLine,
493
+ this.col
494
+ );
495
+ }
496
+ }
497
+ segments.push({ text: curText, line: curLine, col: curCol });
498
+ pendingWs = "";
499
+ listSuffix = this.parseListSuffix(startLine);
500
+ break;
501
+ } else if (ch === '"') {
502
+ const qLine = startLine;
503
+ const qCol = this.col;
504
+ if (curStarted) {
505
+ curText += pendingWs;
506
+ }
507
+ pendingWs = "";
508
+ this.advance();
509
+ const decoded = this.readQuotedStringBody(qLine, qCol);
510
+ curText += decoded;
511
+ if (!curStarted) {
512
+ curLine = qLine;
513
+ curCol = qCol;
514
+ curStarted = true;
515
+ }
516
+ } else if (isUnquotedSubstChar(ch)) {
517
+ if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
518
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
519
+ throw new ParseError(
520
+ `unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
521
+ startLine,
522
+ this.col
523
+ );
524
+ }
525
+ const uCol = this.col;
526
+ if (curStarted) {
527
+ curText += pendingWs;
528
+ }
529
+ pendingWs = "";
530
+ if (!curStarted) {
531
+ curLine = startLine;
532
+ curCol = uCol;
533
+ curStarted = true;
534
+ }
535
+ while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
536
+ curText += this.advance();
537
+ }
538
+ } else if (ch === ".") {
539
+ const dotCol = this.col;
540
+ pendingWs = "";
541
+ if (!curStarted) {
542
+ throw new ParseError("empty segment in path", startLine, dotCol);
543
+ }
544
+ segments.push({ text: curText, line: curLine, col: curCol });
545
+ curText = "";
546
+ curStarted = false;
547
+ curLine = 0;
548
+ curCol = 0;
549
+ lastDot = [startLine, dotCol];
550
+ this.advance();
551
+ } else if (isHoconNewline(ch)) {
552
+ throw new ParseError("unterminated substitution", startLine, startCol);
553
+ } else if (isHoconWhitespace(ch)) {
554
+ pendingWs += ch;
555
+ this.advance();
556
+ } else {
557
+ throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
558
+ }
559
+ }
560
+ if (!listSuffix) {
561
+ if (curStarted) {
562
+ segments.push({ text: curText, line: curLine, col: curCol });
563
+ } else if (segments.length === 0) {
564
+ throw new ParseError("empty substitution path", startLine, startCol);
565
+ } else {
566
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
567
+ throw new ParseError("empty segment in path", errLine, errCol);
568
+ }
569
+ }
570
+ return { segments, optional, listSuffix };
571
+ }
572
+ /**
573
+ * Consume the literal `[]` suffix inside a substitution body.
574
+ * Called after the `[` has been detected (but not yet consumed).
575
+ * Requires the next character to be `[`, then exactly `]`, then `}`.
576
+ * Whitespace inside `[]` is not permitted (per spec Decision §1).
577
+ * Returns true (always — only called when listSuffix path is taken).
578
+ */
579
+ parseListSuffix(startLine) {
580
+ this.advance();
581
+ const afterBracket = this.peek();
582
+ if (afterBracket !== "]") {
583
+ const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
584
+ throw new ParseError(
585
+ `expected ']' after '[' in substitution list suffix (got ${desc})`,
586
+ startLine,
587
+ this.col
588
+ );
589
+ }
590
+ this.advance();
591
+ if (this.peek() !== "}") {
592
+ const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
593
+ throw new ParseError(
594
+ `expected '}' after '[]' in substitution list suffix (got ${desc})`,
595
+ startLine,
596
+ this.col
597
+ );
598
+ }
599
+ this.advance();
600
+ return true;
601
+ }
361
602
  };
362
603
  function tokenize(input) {
363
604
  return new Lexer(input).tokenize();
364
605
  }
606
+ function isUnquotedSubstChar(ch) {
607
+ if (ch === "" || isHoconWhitespace(ch)) return false;
608
+ if ('"\\'.includes(ch)) return false;
609
+ if ("{}[]".includes(ch)) return false;
610
+ if (":=,+#`^?!@*&$.".includes(ch)) return false;
611
+ return true;
612
+ }
613
+ function isDecimalDigit(ch) {
614
+ return ch >= "0" && ch <= "9";
615
+ }
365
616
  function isUnquotedStart(ch) {
366
- return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
617
+ if (ch === "" || isHoconWhitespace(ch)) return false;
618
+ if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
619
+ return true;
367
620
  }
368
621
  function isUnquotedContinue(ch, nextFn) {
369
- if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
622
+ if (ch === "" || isHoconWhitespace(ch)) return false;
623
+ if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
370
624
  if (ch === "+" && nextFn() === "=") return false;
371
625
  if (ch === "/" && nextFn() === "/") return false;
372
626
  return true;
373
627
  }
374
628
 
375
629
  // src/internal/parser/parser.ts
376
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
630
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
377
631
  var Parser = class {
378
632
  constructor(tokens) {
379
633
  this.tokens = tokens;
@@ -431,6 +685,14 @@ var Parser = class {
431
685
  const t = this.peek();
432
686
  if (t.kind === "eof" || t.kind === "rbrace") break;
433
687
  if (t.kind === "unquoted" && t.value === "include") {
688
+ const next = this.peek(1);
689
+ if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
690
+ throw new ParseError(
691
+ `'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
692
+ t.line,
693
+ t.col
694
+ );
695
+ }
434
696
  this.advance();
435
697
  fields.push(this.parseInclude());
436
698
  this.skip("newline");
@@ -439,7 +701,14 @@ var Parser = class {
439
701
  continue;
440
702
  }
441
703
  const keyPos = this.currentPos();
442
- const key = this.parseKey();
704
+ const { segments: key, firstWasQuoted } = this.parseKey();
705
+ if (key[0] === "include" && !firstWasQuoted) {
706
+ throw new ParseError(
707
+ `'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
708
+ keyPos.line,
709
+ keyPos.col
710
+ );
711
+ }
443
712
  this.skip("newline");
444
713
  let append = false;
445
714
  const sep = this.peek();
@@ -469,18 +738,31 @@ var Parser = class {
469
738
  parseKey() {
470
739
  const segments = [];
471
740
  let trailingDot = false;
741
+ let firstWasQuoted = false;
472
742
  while (true) {
473
743
  const t = this.peek();
474
744
  if (t.kind === "string") {
475
745
  this.advance();
746
+ if (segments.length === 0) firstWasQuoted = true;
476
747
  segments.push(t.value);
477
748
  trailingDot = false;
478
749
  } else if (t.kind === "unquoted") {
479
750
  this.advance();
480
- const parts = t.value.split(".");
751
+ const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
752
+ const parts = raw.split(".");
481
753
  const filtered = parts.filter((s) => s.length > 0);
754
+ for (const seg of filtered) {
755
+ if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
756
+ const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
757
+ throw new ParseError(
758
+ `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
759
+ t.line,
760
+ t.col
761
+ );
762
+ }
763
+ }
482
764
  segments.push(...filtered);
483
- trailingDot = t.value.endsWith(".");
765
+ trailingDot = raw.endsWith(".");
484
766
  } else {
485
767
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
486
768
  break;
@@ -492,9 +774,13 @@ var Parser = class {
492
774
  trailingDot = true;
493
775
  continue;
494
776
  }
777
+ if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
778
+ trailingDot = true;
779
+ continue;
780
+ }
495
781
  break;
496
782
  }
497
- return segments;
783
+ return { segments, firstWasQuoted };
498
784
  }
499
785
  parseInclude() {
500
786
  const p = this.currentPos();
@@ -567,9 +853,13 @@ var Parser = class {
567
853
  } else if (t.kind === "lbracket") {
568
854
  this.advance();
569
855
  node = this.parseArray();
570
- } else if (t.kind === "subst" || t.kind === "opt_subst") {
856
+ } else if (t.kind === "subst") {
571
857
  this.advance();
572
- node = { kind: "subst", path: t.value, optional: t.kind === "opt_subst", pos: { line: t.line, col: t.col } };
858
+ if (!t.subst) {
859
+ throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
860
+ }
861
+ const payload = t.subst;
862
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
573
863
  } else if (t.kind === "string" || t.kind === "triple_string") {
574
864
  this.advance();
575
865
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -620,6 +910,20 @@ function parseTokens(tokens) {
620
910
  return new Parser(tokens).parse();
621
911
  }
622
912
 
913
+ // src/internal/parser/empty-check.ts
914
+ function hasContentTokens(tokens) {
915
+ return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
916
+ }
917
+ function assertNonEmptyDocument(tokens, sourceDescriptor) {
918
+ if (!hasContentTokens(tokens)) {
919
+ throw new ParseError(
920
+ `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
921
+ 1,
922
+ 1
923
+ );
924
+ }
925
+ }
926
+
623
927
  // src/internal/resolver/types.ts
624
928
  var separatorValues = /* @__PURE__ */ new WeakSet();
625
929
  function isSubst(v) {
@@ -639,64 +943,20 @@ function makeResObj() {
639
943
  }
640
944
 
641
945
  // src/internal/resolver/utils.ts
642
- function parseSubstPath(raw) {
643
- const segments = [];
644
- let i = 0;
645
- while (i < raw.length) {
646
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
647
- if (i >= raw.length) break;
648
- if (raw[i] === '"') {
649
- i++;
650
- let seg = "";
651
- while (i < raw.length && raw[i] !== '"') {
652
- if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
653
- seg += raw[i + 1];
654
- i += 2;
655
- } else {
656
- seg += raw[i];
657
- i++;
658
- }
659
- }
660
- if (i < raw.length) i++;
661
- segments.push(seg);
662
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
663
- if (i < raw.length && raw[i] === ".") i++;
664
- } else if (raw[i] === ".") {
665
- segments.push("");
666
- i++;
667
- } else {
668
- let seg = "";
669
- while (i < raw.length && raw[i] !== ".") {
670
- seg += raw[i];
671
- i++;
672
- }
673
- segments.push(seg.trim());
674
- if (i < raw.length && raw[i] === ".") i++;
675
- }
676
- }
677
- return segments;
678
- }
679
946
  function segmentsToKey(segments) {
680
947
  return segments.map((s) => {
681
- if (s === "" || /[^a-zA-Z0-9\-_]/.test(s)) {
682
- return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
948
+ const t = s.text;
949
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
950
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
683
951
  }
684
- return s;
952
+ return t;
685
953
  }).join(".");
686
954
  }
687
955
  function lookupPath(root, segments) {
688
956
  const [head, ...tail] = segments;
689
- if (head === void 0 || head === "") {
690
- if (segments.length > 0) {
691
- const val2 = root.fields.get("");
692
- if (val2 === void 0) return void 0;
693
- if (tail.length === 0) return val2;
694
- if (isResObj(val2)) return lookupPath(val2, tail);
695
- return void 0;
696
- }
697
- return void 0;
698
- }
699
- const val = root.fields.get(head);
957
+ if (head === void 0) return void 0;
958
+ const key = head.text;
959
+ const val = root.fields.get(key);
700
960
  if (val === void 0) return void 0;
701
961
  if (tail.length === 0) return val;
702
962
  if (isResObj(val)) return lookupPath(val, tail);
@@ -705,7 +965,7 @@ function lookupPath(root, segments) {
705
965
  function lookupResObj(root, segments) {
706
966
  let cur = root;
707
967
  for (const seg of segments) {
708
- const val = cur.fields.get(seg);
968
+ const val = cur.fields.get(seg.text);
709
969
  if (val === void 0 || !isResObj(val)) return void 0;
710
970
  cur = val;
711
971
  }
@@ -765,6 +1025,7 @@ import * as nodePath from "path";
765
1025
  // src/internal/properties/properties.ts
766
1026
  function parseProperties(input) {
767
1027
  const root = /* @__PURE__ */ Object.create(null);
1028
+ const pairs = [];
768
1029
  for (const line of input.split("\n")) {
769
1030
  const trimmed = line.trim();
770
1031
  if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
@@ -773,6 +1034,10 @@ function parseProperties(input) {
773
1034
  const key = trimmed.slice(0, sepIdx).trim();
774
1035
  const value = trimmed.slice(sepIdx + 1).trim();
775
1036
  if (key === "") continue;
1037
+ pairs.push([key, value]);
1038
+ }
1039
+ pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1040
+ for (const [key, value] of pairs) {
776
1041
  setNested(root, key.split("."), value);
777
1042
  }
778
1043
  return root;
@@ -796,6 +1061,7 @@ function setNested(obj, segments, value) {
796
1061
  }
797
1062
  const last = segments[segments.length - 1];
798
1063
  if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1064
+ if (typeof current[last] === "object" && current[last] !== null) return;
799
1065
  current[last] = value;
800
1066
  }
801
1067
  function propertiesToHoconValue(input) {
@@ -909,7 +1175,9 @@ var IncludeLoader = class {
909
1175
  if (candidate.endsWith(".properties")) {
910
1176
  return hoconValueToResObj(propertiesToHoconValue(content));
911
1177
  }
912
- const ast = parseTokens(tokenize(content));
1178
+ const tokens = tokenize(content);
1179
+ assertNonEmptyDocument(tokens, candidate);
1180
+ const ast = parseTokens(tokens);
913
1181
  return this.onBuildResObj(ast, {
914
1182
  env,
915
1183
  baseDir: nodePath.dirname(candidate),
@@ -933,7 +1201,9 @@ var IncludeLoader = class {
933
1201
  if (candidate.endsWith(".properties")) {
934
1202
  return hoconValueToResObj(propertiesToHoconValue(content));
935
1203
  }
936
- const ast = parseTokens(tokenize(content));
1204
+ const tokens = tokenize(content);
1205
+ assertNonEmptyDocument(tokens, candidate);
1206
+ const ast = parseTokens(tokens);
937
1207
  return this.onBuildResObjAsync(ast, {
938
1208
  env,
939
1209
  baseDir: nodePath.dirname(candidate),
@@ -982,7 +1252,7 @@ var StructureBuilder = class _StructureBuilder {
982
1252
  return;
983
1253
  }
984
1254
  const [head, ...tail] = field.key;
985
- if (!head) return;
1255
+ if (head === void 0) return;
986
1256
  if (tail.length > 0) {
987
1257
  const syntheticAst = {
988
1258
  kind: "object",
@@ -1024,7 +1294,7 @@ var StructureBuilder = class _StructureBuilder {
1024
1294
  return;
1025
1295
  }
1026
1296
  const [head, ...tail] = field.key;
1027
- if (!head) return;
1297
+ if (head === void 0) return;
1028
1298
  if (tail.length > 0) {
1029
1299
  const syntheticAst = {
1030
1300
  kind: "object",
@@ -1070,9 +1340,9 @@ var StructureBuilder = class _StructureBuilder {
1070
1340
  return inner;
1071
1341
  }
1072
1342
  case "subst":
1073
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1343
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1074
1344
  case "concat":
1075
- return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1345
+ return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1076
1346
  case "include":
1077
1347
  return { kind: "scalar", raw: "null", valueType: "null" };
1078
1348
  }
@@ -1094,13 +1364,13 @@ var StructureBuilder = class _StructureBuilder {
1094
1364
  case "object":
1095
1365
  return await this.buildAsync(ast, pathPrefix);
1096
1366
  case "subst":
1097
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1367
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1098
1368
  case "concat": {
1099
1369
  const nodes = [];
1100
1370
  for (const n of ast.nodes) {
1101
1371
  nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
1102
1372
  }
1103
- return { _kind: "concat-placeholder", nodes };
1373
+ return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
1104
1374
  }
1105
1375
  case "include":
1106
1376
  return { kind: "scalar", raw: "null", valueType: "null" };
@@ -1109,7 +1379,8 @@ var StructureBuilder = class _StructureBuilder {
1109
1379
  // ---- Relativize substitution paths for nested includes ----
1110
1380
  relativizeSubstPaths(val, prefixSegments) {
1111
1381
  if (isSubst(val)) {
1112
- val.segments = [...prefixSegments, ...val.segments];
1382
+ const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
1383
+ val.segments = [...prefixAsSegments, ...val.segments];
1113
1384
  val.prefixLen += prefixSegments.length;
1114
1385
  return;
1115
1386
  }
@@ -1145,6 +1416,23 @@ var StructureBuilder = class _StructureBuilder {
1145
1416
  }
1146
1417
  };
1147
1418
 
1419
+ // src/value/numeric-array.ts
1420
+ function numericObjectToArray(value) {
1421
+ if (value.kind !== "object") return null;
1422
+ if (value.fields.size === 0) return null;
1423
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
1424
+ const eligible = [];
1425
+ for (const [key, val] of value.fields) {
1426
+ if (!canonicalInt.test(key)) continue;
1427
+ const n = Number(key);
1428
+ if (!Number.isInteger(n) || n > 2147483647) continue;
1429
+ eligible.push({ n, v: val });
1430
+ }
1431
+ if (eligible.length === 0) return null;
1432
+ eligible.sort((a, b) => a.n - b.n);
1433
+ return eligible.map((e) => e.v);
1434
+ }
1435
+
1148
1436
  // src/internal/resolver/substitution-resolver.ts
1149
1437
  var SubstitutionResolver = class {
1150
1438
  constructor(root, opts) {
@@ -1152,6 +1440,19 @@ var SubstitutionResolver = class {
1152
1440
  this.opts = opts;
1153
1441
  this.resolving = /* @__PURE__ */ new Set();
1154
1442
  this.cache = /* @__PURE__ */ new Map();
1443
+ // Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
1444
+ // Used by isSelfRef detection: fires only when found === a concat currently
1445
+ // mid-iteration (i.e. we are inside found's own resolution), preventing the
1446
+ // "foofoo" double-count without misfiring when an external field looks up the
1447
+ // same concat via a substitution (multi-reviewer convergence fix).
1448
+ //
1449
+ // Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
1450
+ // preservation for self-ref detection. Round-2 multi-agent-review surfaced a
1451
+ // false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
1452
+ // criterion was tightened to node-membership-in-iterating-set — strictly
1453
+ // narrower than path-equality. Spec amendment deferred to a follow-up
1454
+ // xx.hocon PR (see Phase 6 #3f close-out notes).
1455
+ this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1155
1456
  }
1156
1457
  resolve() {
1157
1458
  return this.resolveResObj(this.root);
@@ -1190,7 +1491,14 @@ var SubstitutionResolver = class {
1190
1491
  }
1191
1492
  resolveVal(v, scope) {
1192
1493
  if (isSubst(v)) return this.resolveSubst(v, scope);
1193
- if (isConcat(v)) return this.resolveConcat(v.nodes, scope);
1494
+ if (isConcat(v)) {
1495
+ this.resolvingConcats.add(v);
1496
+ try {
1497
+ return this.resolveConcat(v.nodes, scope, v.line, v.col);
1498
+ } finally {
1499
+ this.resolvingConcats.delete(v);
1500
+ }
1501
+ }
1194
1502
  if (isAppend(v)) return this.resolveAppend(v, scope);
1195
1503
  if (isResObj(v)) return this.resolveResObj(v);
1196
1504
  const hv = v;
@@ -1204,23 +1512,20 @@ var SubstitutionResolver = class {
1204
1512
  }
1205
1513
  return hv;
1206
1514
  }
1207
- segmentsEqual(a, b) {
1208
- return a.length === b.length && a.every((seg, i) => seg === b[i]);
1209
- }
1210
1515
  resolveSubst(s, scope) {
1211
- const key = segmentsToKey(s.segments);
1516
+ const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1212
1517
  if (this.cache.has(key)) return this.cache.get(key);
1213
1518
  if (this.resolving.has(key)) {
1214
1519
  let prior;
1215
- if (s.prefixLen > 0) {
1216
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1520
+ if (s.segments.length > 1) {
1521
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1217
1522
  const parentScope = lookupResObj(
1218
1523
  this.root,
1219
1524
  s.segments.slice(0, s.segments.length - 1)
1220
1525
  );
1221
1526
  prior = parentScope?.priorValues.get(leafSeg);
1222
1527
  } else {
1223
- const rootSeg = s.segments[0] ?? "";
1528
+ const rootSeg = s.segments[0]?.text ?? "";
1224
1529
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1225
1530
  }
1226
1531
  if (prior !== void 0) {
@@ -1244,34 +1549,36 @@ var SubstitutionResolver = class {
1244
1549
  try {
1245
1550
  const found = lookupPath(this.root, s.segments);
1246
1551
  if (found !== void 0) {
1247
- if (isSubst(found) || isConcat(found)) {
1248
- const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1249
- (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1250
- );
1251
- if (isSelfRef) {
1252
- let prior;
1253
- if (s.prefixLen > 0) {
1254
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1255
- const parentScope = lookupResObj(
1256
- this.root,
1257
- s.segments.slice(0, s.segments.length - 1)
1258
- );
1259
- prior = parentScope?.priorValues.get(leafSeg);
1260
- } else {
1261
- const rootSeg = s.segments[0] ?? "";
1262
- prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1263
- }
1264
- if (prior !== void 0) {
1265
- const result2 = this.resolveVal(prior, scope);
1266
- if (result2 !== void 0) this.cache.set(key, result2);
1267
- return result2;
1268
- }
1552
+ if (isConcat(found) && this.resolvingConcats.has(found)) {
1553
+ let prior;
1554
+ if (s.segments.length > 1) {
1555
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1556
+ const parentScope = lookupResObj(
1557
+ this.root,
1558
+ s.segments.slice(0, s.segments.length - 1)
1559
+ );
1560
+ prior = parentScope?.priorValues.get(leafSeg);
1561
+ } else {
1562
+ const rootSeg = s.segments[0]?.text ?? "";
1563
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1269
1564
  }
1565
+ if (prior !== void 0) {
1566
+ const result2 = this.resolveVal(prior, scope);
1567
+ if (result2 !== void 0) this.cache.set(key, result2);
1568
+ return result2;
1569
+ }
1570
+ if (s.optional) return void 0;
1571
+ throw new ResolveError(
1572
+ `could not resolve substitution: \${${key}}`,
1573
+ key,
1574
+ s.line,
1575
+ s.col
1576
+ );
1270
1577
  }
1271
1578
  let result = this.resolveVal(found, scope);
1272
1579
  const effectiveLen = s.segments.length - s.prefixLen;
1273
1580
  if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1274
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1581
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1275
1582
  let prior;
1276
1583
  if (s.prefixLen > 0) {
1277
1584
  const parentScope = lookupResObj(
@@ -1295,8 +1602,23 @@ var SubstitutionResolver = class {
1295
1602
  if (result !== void 0) this.cache.set(key, result);
1296
1603
  return result;
1297
1604
  }
1298
- const envKey = s.segments.join(".");
1299
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).join(".")] : void 0);
1605
+ if (s.listSuffix) {
1606
+ const result = this.resolveEnvList(s);
1607
+ if (result !== void 0) {
1608
+ this.cache.set(key, result);
1609
+ return result;
1610
+ }
1611
+ if (s.optional) return void 0;
1612
+ const envBase = s.segments.map((seg) => seg.text).join(".");
1613
+ throw new ResolveError(
1614
+ `could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1615
+ key,
1616
+ s.line,
1617
+ s.col
1618
+ );
1619
+ }
1620
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1621
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1300
1622
  if (envVal !== void 0) {
1301
1623
  const result = { kind: "scalar", raw: envVal, valueType: "string" };
1302
1624
  this.cache.set(key, result);
@@ -1313,37 +1635,106 @@ var SubstitutionResolver = class {
1313
1635
  this.resolving.delete(key);
1314
1636
  }
1315
1637
  }
1316
- resolveConcat(nodes, scope) {
1638
+ /**
1639
+ * S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
1640
+ *
1641
+ * Candidate bases follow the same fully-qualified→bare order used by the scalar
1642
+ * env fallback. The first base whose _0 key is present wins entirely — no
1643
+ * cross-base merging (spec §Relativized fallback precedence rules).
1644
+ *
1645
+ * Returns the HoconValue Array if any candidate base has _0, or undefined if
1646
+ * none do (caller applies optional/required handling).
1647
+ */
1648
+ resolveEnvList(s) {
1649
+ const bases = this.candidateBases(s);
1650
+ for (const base of bases) {
1651
+ const key0 = base + "_0";
1652
+ if (!(key0 in this.opts.env)) continue;
1653
+ const items = [];
1654
+ for (let i = 0; ; i++) {
1655
+ const k = base + "_" + i;
1656
+ if (!(k in this.opts.env)) break;
1657
+ items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
1658
+ }
1659
+ return { kind: "array", items };
1660
+ }
1661
+ return void 0;
1662
+ }
1663
+ /**
1664
+ * Return the candidate base names for env-var-list lookup, in precedence order.
1665
+ * Mirrors the fully-qualified→bare fallback used by the scalar env path.
1666
+ */
1667
+ candidateBases(s) {
1668
+ const full = s.segments.map((seg) => seg.text).join(".");
1669
+ if (s.prefixLen === 0) return [full];
1670
+ const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
1671
+ return [full, bare];
1672
+ }
1673
+ resolveConcat(nodes, scope, line = 0, col = 0) {
1317
1674
  const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1318
1675
  if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1319
1676
  if (resolved.length === 1) return resolved[0];
1320
1677
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1321
- if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1322
- const merged = /* @__PURE__ */ new Map();
1323
- for (const v of nonSep) {
1324
- if (v.kind === "object") {
1325
- for (const [k, val] of v.fields) {
1326
- const existing = merged.get(k);
1327
- if (existing?.kind === "object" && val.kind === "object") {
1328
- merged.set(k, deepMergeHoconValues(existing, val));
1329
- } else {
1330
- merged.set(k, val);
1331
- }
1332
- }
1678
+ if (nonSep.length === 0) {
1679
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1680
+ return { kind: "scalar", raw: str, valueType: "string" };
1681
+ }
1682
+ const joinPair = (left, right) => {
1683
+ if (left.kind === "object" && right.kind === "object") {
1684
+ return deepMergeHoconValues(left, right);
1685
+ }
1686
+ if (left.kind === "array" && right.kind === "object") {
1687
+ const converted = numericObjectToArray(right);
1688
+ if (converted !== null) {
1689
+ return { kind: "array", items: [...left.items, ...converted] };
1333
1690
  }
1691
+ throw new ResolveError(
1692
+ "cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
1693
+ "",
1694
+ line,
1695
+ col
1696
+ );
1334
1697
  }
1335
- return { kind: "object", fields: merged };
1336
- }
1337
- if (resolved.some((v) => v.kind === "array")) {
1338
- const items = [];
1339
- for (const v of resolved) {
1340
- if (v.kind === "array") items.push(...v.items);
1341
- else items.push(v);
1698
+ if (left.kind === "object" && right.kind === "array") {
1699
+ const converted = numericObjectToArray(left);
1700
+ if (converted !== null) {
1701
+ return { kind: "array", items: [...converted, ...right.items] };
1702
+ }
1703
+ throw new ResolveError(
1704
+ "cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
1705
+ "",
1706
+ line,
1707
+ col
1708
+ );
1342
1709
  }
1343
- return { kind: "array", items };
1710
+ if (left.kind === "array" && right.kind === "array") {
1711
+ return { kind: "array", items: [...left.items, ...right.items] };
1712
+ }
1713
+ if (left.kind === "array" || right.kind === "array") {
1714
+ throw new ResolveError(
1715
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
1716
+ "",
1717
+ line,
1718
+ col
1719
+ );
1720
+ }
1721
+ if (left.kind === "object" || right.kind === "object") {
1722
+ throw new ResolveError(
1723
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
1724
+ "",
1725
+ line,
1726
+ col
1727
+ );
1728
+ }
1729
+ return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
1730
+ };
1731
+ const [head, ...tail] = nonSep;
1732
+ const folded = tail.reduce(joinPair, head);
1733
+ if (folded.kind === "scalar") {
1734
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1735
+ return { kind: "scalar", raw: str, valueType: "string" };
1344
1736
  }
1345
- const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1346
- return { kind: "scalar", raw: str, valueType: "string" };
1737
+ return folded;
1347
1738
  }
1348
1739
  resolveAppend(a, scope) {
1349
1740
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
@@ -1402,6 +1793,7 @@ var Config = class _Config {
1402
1793
  }
1403
1794
  const result = parseBytes(v.raw, unit);
1404
1795
  if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
1796
+ if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
1405
1797
  return result;
1406
1798
  }
1407
1799
  getConfig(path2) {
@@ -1413,6 +1805,10 @@ var Config = class _Config {
1413
1805
  getList(path2) {
1414
1806
  const v = this.lookupNode(path2);
1415
1807
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
1808
+ if (v.kind === "object") {
1809
+ const converted = numericObjectToArray(v);
1810
+ if (converted !== null) return converted.map(hoconToJs);
1811
+ }
1416
1812
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
1417
1813
  return v.items.map(hoconToJs);
1418
1814
  }
@@ -1537,6 +1933,7 @@ function defaultReadFileSync(filePath) {
1537
1933
  }
1538
1934
  function buildResolveContext(input, opts) {
1539
1935
  const tokens = tokenize(input);
1936
+ assertNonEmptyDocument(tokens, "input");
1540
1937
  const ast = parseTokens(tokens);
1541
1938
  const resolveOpts = {
1542
1939
  env: getEnv(opts),