Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ What changed for someone using zig-hocon: the Zig package, the `hocon` command l

## [Unreleased]

### Added

- Twelve conformance cases from bug reports in lightbend/config and ekrich/sconfig where the spec has the answer: typesafe/config fails inside `${a} { … }`, turns integers past 2^63 into strings and accepts `\u+041`; pyhocon loses `+=` across an include and the text of a substituted number. zig-hocon passes all 469.

### Fixed

- An object after a substitution, `top = ${x} { … }`, is a merge, the same as `top = ${x}` and `top { … }` written as two fields. A `+=` inside it appends to the list `${x}` gives (`top = ${x} { arr += four }` was `[four]`, is `[one, four]`), a self-reference looks back at it, and `${top.n}` inside it sees the merged object instead of reporting a cycle. Eight new conformance cases pin it.
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The rest of this page reads these two files.

## Why zig-hocon

- **Correct.** Passes all 457 tests in the [conformance suite](https://htmlpreview.github.io/?https://github.com/lejmr/zig-hocon/blob/main/conformance/report.html), one test for each sentence of the spec.
- **Correct.** Passes all 469 tests in the [conformance suite](https://htmlpreview.github.io/?https://github.com/lejmr/zig-hocon/blob/main/conformance/report.html), one test for each sentence of the spec.
- **Your schema is a Zig struct.** HOCON is only what sits on disk; the struct says what it holds, with defaults, optionals, enums, durations and sizes.
- **Fast.** A 10 MB config in a quarter of a second.\*
- **Clear errors.** File, line, column and a caret, also for a value of the wrong type.
Expand Down Expand Up @@ -210,13 +210,13 @@ b.step("check-config", "Check the config against Config").dependOn(&check.step);

| implementation | against the spec | against typesafe/config 1.4.9 |
|---|---|---|
| Java <sub>typesafe/config 1.4.9</sub> | 88.4% (404/457) | 100% (457/457) |
| hocon2 <sub>hocon2 0.6.3</sub> | 84.7% (387/457) | 78.8% (360/457) |
| pulse-pyhocon <sub>pulse-pyhocon 0.2.1</sub> | 69.4% (317/457) | 67.0% (306/457) |
| py.hocon <sub>hocon-parser 1.14.2 (Python)</sub> | 87.1% (398/457) | 86.4% (395/457) |
| pyhocon <sub>pyhocon 0.3.63</sub> | 70.0% (320/457) | 65.6% (300/457) |
| rs.hocon <sub>hocon-parser 1.14.2 (Rust)</sub> | 87.1% (398/457) | 84.5% (386/457) |
| zig-hocon <sub>zig-hocon code@5b9031b</sub> | 100% (457/457) | 88.4% (404/457) |
| Java <sub>typesafe/config 1.4.9</sub> | 87.4% (410/469) | 100% (469/469) |
| hocon2 <sub>hocon2 0.6.3</sub> | 82.5% (387/469) | 76.8% (360/469) |
| pulse-pyhocon <sub>pulse-pyhocon 0.2.1</sub> | 67.6% (317/469) | 65.2% (306/469) |
| py.hocon <sub>hocon-parser 1.14.2 (Python)</sub> | 84.9% (398/469) | 84.2% (395/469) |
| pyhocon <sub>pyhocon 0.3.63</sub> | 69.1% (324/469) | 64.2% (301/469) |
| rs.hocon <sub>hocon-parser 1.14.2 (Rust)</sub> | 84.9% (398/469) | 82.3% (386/469) |
| zig-hocon <sub>zig-hocon code@d844ec9</sub> | 100% (469/469) | 87.4% (410/469) |

The table keeps the best parsers; the others measured, and why they are not in it: [conformance/README.md](conformance/README.md#not-in-the-table).

Expand Down
30 changes: 15 additions & 15 deletions conformance/maintaining/COVERAGE.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions conformance/maintaining/FINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ What the validation pass turned up and did **not** fix. The divergences it was c
- `unquoted-strings/025-digit-initial-invalid-number` — a value that begins with a digit but is not a valid number (`a = 1.2.3`, the shape of `127.0.0.1`) is an error: the initial number character and the number characters after it "must be parsed as a number value" (unquoted-strings.12, .13). pyhocon and typesafe/config read it as the string "1.2.3"; the row is `java: lenient`.
- `plus-equals-field-separator/009`–`012`, `self-referential-examples/027`–`030` — an object concatenated over a substitution, `top : ${x} { ... }`, is a merge (array-and-object-concatenation.6), the same as `top : ${x}` and `top { ... }` written as two fields. So a self-reference inside it, `+=` or the long form, looks back at the field the substitution gives (examples-of-self-referential-substitutions.3), and any other reference to `top.…` inside looks forward and sees the merged object (.14, .16). typesafe/config drops the inherited element of a `+=` (lightbend/config#375, #608; a fix is proposed in #882) and reports the rest as cycles (#586, #728); zig-hocon got the two-field form right and the one-value form wrong.
- `self-referential-examples/031`–`033` — layers are merged before they are resolved: an object a substitution gives on top hides a field of the object below it, which is then never evaluated (examples-of-self-referential-substitutions.8; lightbend/config#838, and #812 for the same inside `${base} { ... }`), and a reference in the object below looks forward at the merged value (.16). zig-hocon resolved the layer below first: it evaluated the hidden field and saw the old value.
- Bug reports in lightbend/config and ekrich/sconfig where the spec decides and no case was: an internal error in typesafe/config inside `${a} { ... }` and before a list substitution (`array-and-object-concatenation/020`–`022`), integers past 2^63 as strings and `\u` escapes with a sign or non-ASCII digits accepted (`unchanged-from-json/028`–`030`), and pyhocon's slips: a substituted number's text (`string-value-concatenation/023`), `+=` across an include (`include-merging/016`), an object over a string hidden by a later one (`self-referential-examples/034`), an include two objects deep (`include-substitution/020`), `+=` after `c = ${d}` (`plus-equals-field-separator/013`), and an undefined optional under a path (`substitutions/041`).

This pass ran *before* the tie-break rule in `PROCESS.md` was settled, so where a divergence says `expect` was left at java's value, the row is still unresolved — `fill-expected.py` now flags those with `review`.

Expand Down
16 changes: 8 additions & 8 deletions conformance/maintaining/SECTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ observable parse behaviour.
|---|---|---|---|
| 59 | Goals / Background | — | — |
| 92 | Definitions | — | — |
| 115 | Unchanged from JSON | `unchanged-from-json` | ✅ 27 cases |
| 115 | Unchanged from JSON | `unchanged-from-json` | ✅ 30 cases |
| 123 | Comments | `comments` | ✅ 14 cases |
| 128 | Omit root braces | `omit-root-braces` | ✅ 14 cases |
| 141 | Key-value separator | `key-value-separator` | ✅ 6 cases |
Expand All @@ -19,20 +19,20 @@ observable parse behaviour.
| 240 | Unquoted strings | `unquoted-strings` | ✅ 25 cases |
| 288 | Multi-line strings | `multi-line-strings` | ✅ 9 cases |
| 304 | Value concatenation | `value-concatenation` | ✅ 2 cases |
| 321 | String value concatenation | `string-value-concatenation` | ✅ 18 cases |
| 382 | Array and object concatenation | `array-and-object-concatenation` | ✅ 19 cases |
| 321 | String value concatenation | `string-value-concatenation` | ✅ 19 cases |
| 382 | Array and object concatenation | `array-and-object-concatenation` | ✅ 22 cases |
| 435 | Concatenation with whitespace and substitutions | `concatenation-whitespace-and-substitutions` | ✅ 9 cases |
| 444 | Arrays without commas or newlines | `arrays-without-commas-or-newlines` | ✅ 7 cases |
| 472 | Path expressions | `path-expressions` | ✅ 20 cases |
| 521 | Paths as keys | `paths-as-keys` | ✅ 11 cases |
| 574 | Substitutions | `substitutions` | ✅ 37 cases |
| 574 | Substitutions | `substitutions` | ✅ 38 cases |
| 653 | Self-referential substitutions | `self-referential-substitutions` | ✅ 5 cases |
| 719 | The += field separator | `plus-equals-field-separator` | ✅ 12 cases |
| 738 | Examples of self-referential substitutions | `self-referential-examples` | ✅ 33 cases |
| 719 | The += field separator | `plus-equals-field-separator` | ✅ 13 cases |
| 738 | Examples of self-referential substitutions | `self-referential-examples` | ✅ 34 cases |
| 893 | List values from environment variables | `list-values-from-environment-variables` | ✅ 6 cases |
| 921 | Include syntax | `include-syntax` | ✅ 15 cases |
| 982 | Include semantics: merging | `include-merging` | ✅ 15 cases |
| 1008 | Include semantics: substitution | `include-substitution` | ✅ 19 cases |
| 982 | Include semantics: merging | `include-merging` | ✅ 16 cases |
| 1008 | Include semantics: substitution | `include-substitution` | ✅ 20 cases |
| 1051 | Include semantics: missing and required files | `include-missing-and-required` | ✅ 6 cases |
| 1074 | Include semantics: file formats and extensions | `include-file-formats` | ✅ 23 cases |
| 1109 | Include semantics: locating resources | `include-locating` | ✅ 5 cases |
Expand Down
12 changes: 6 additions & 6 deletions conformance/report.html

Large diffs are not rendered by default.

58 changes: 38 additions & 20 deletions conformance/reports/Java.java.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
"mode": "java",
"java_version": "1.4.9",
"command": "conformance/adapters/oracle.sh hocon-java",
"ran_at": "2026-10-24T06:50:11Z",
"suite_digest": "34d5b6ef5070df01",
"total": 457,
"passed": 457,
"ran_at": "2026-10-24T07:45:49Z",
"suite_digest": "1762fcc881cbda49",
"total": 469,
"passed": 469,
"percent": 100.0,
"sections": {
"array-and-object-concatenation": {
"total": 19,
"passed": 19
"total": 22,
"passed": 22
},
"arrays-without-commas-or-newlines": {
"total": 7,
Expand Down Expand Up @@ -52,16 +52,16 @@
"passed": 5
},
"include-merging": {
"total": 15,
"passed": 15
"total": 16,
"passed": 16
},
"include-missing-and-required": {
"total": 6,
"passed": 6
},
"include-substitution": {
"total": 19,
"passed": 19
"total": 20,
"passed": 20
},
"include-syntax": {
"total": 15,
Expand Down Expand Up @@ -100,12 +100,12 @@
"passed": 7
},
"plus-equals-field-separator": {
"total": 12,
"passed": 12
"total": 13,
"passed": 13
},
"self-referential-examples": {
"total": 33,
"passed": 33
"total": 34,
"passed": 34
},
"self-referential-substitutions": {
"total": 5,
Expand All @@ -116,20 +116,20 @@
"passed": 5
},
"string-value-concatenation": {
"total": 18,
"passed": 18
"total": 19,
"passed": 19
},
"substitution-fallback-to-environment": {
"total": 8,
"passed": 8
},
"substitutions": {
"total": 37,
"passed": 37
"total": 38,
"passed": 38
},
"unchanged-from-json": {
"total": 27,
"passed": 27
"total": 30,
"passed": 30
},
"units-format": {
"total": 6,
Expand Down Expand Up @@ -168,6 +168,9 @@
"array-and-object-concatenation/017-newline-between-arrays-in-field-value-prevents-concatenation.conf": "pass",
"array-and-object-concatenation/018-object-with-inner-merge-concatenated-with-substitution.conf": "pass",
"array-and-object-concatenation/019-self-referential-object-concatenation-with-inner-merge.conf": "pass",
"array-and-object-concatenation/020-optional-override-inside-object-concatenation.conf": "pass",
"array-and-object-concatenation/021-substitution-over-duplicate-key-inside-object-concatenation.conf": "pass",
"array-and-object-concatenation/022-duplicate-key-in-list-element-before-list-substitution.conf": "pass",
"arrays-without-commas-or-newlines/001-newlines-instead-of-commas.conf": "pass",
"arrays-without-commas-or-newlines/002-whitespace-instead-of-comma-concatenates-into-one-element.conf": "pass",
"arrays-without-commas-or-newlines/003-newline-separates-two-arrays-into-elements.conf": "pass",
Expand Down Expand Up @@ -295,6 +298,7 @@
"include-merging/013-included-merge-stack-over-earlier-substitution/main.conf": "pass",
"include-merging/014-included-merge-stack-over-earlier-scalar/main.conf": "pass",
"include-merging/015-included-object-on-top-of-substitution-over-earlier-substitution/main.conf": "pass",
"include-merging/016-plus-equals-across-include-keeps-order/main.conf": "pass",
"include-missing-and-required/001-missing-include-ignored/main.conf": "pass",
"include-missing-and-required/002-missing-include-is-empty-object/main.conf": "pass",
"include-missing-and-required/003-missing-file-include-ignored/main.conf": "pass",
Expand All @@ -320,6 +324,7 @@
"include-substitution/017-substitution-inside-a-list-is-fixed-up/main.conf": "pass",
"include-substitution/018-substitution-in-a-merge-stack-is-fixed-up/main.conf": "pass",
"include-substitution/019-null-at-fixed-up-path-is-found/main.conf": "pass",
"include-substitution/020-include-two-objects-deep-is-fixed-up/main.conf": "pass",
"include-syntax/001-include-later-in-key-is-literal.conf": "pass",
"include-syntax/002-include-as-object-value-is-string.conf": "pass",
"include-syntax/003-include-as-array-element-is-string.conf": "pass",
Expand Down Expand Up @@ -427,6 +432,7 @@
"plus-equals-field-separator/010-appends-through-chained-object-concatenations.conf": "pass",
"plus-equals-field-separator/011-appends-deep-inside-object-concatenation.conf": "pass",
"plus-equals-field-separator/012-appends-to-a-list-defined-in-object-concatenation.conf": "pass",
"plus-equals-field-separator/013-appends-after-a-substituted-object.conf": "pass",
"self-referential-examples/001-isolated-self-reference-is-an-error.conf": "pass",
"self-referential-examples/002-self-reference-resolves-to-earlier-merged-value.conf": "pass",
"self-referential-examples/003-self-reference-before-any-value-is-undefined.conf": "pass",
Expand Down Expand Up @@ -460,6 +466,7 @@
"self-referential-examples/031-field-hidden-by-a-substituted-object.conf": "pass",
"self-referential-examples/032-field-hidden-inside-object-concatenation.conf": "pass",
"self-referential-examples/033-object-below-a-substitution-looks-forward.conf": "pass",
"self-referential-examples/034-object-over-scalar-hidden-by-later-object.conf": "pass",
"self-referential-substitutions/001-path-builds-on-older-value.conf": "pass",
"self-referential-substitutions/002-self-referential-concatenation.conf": "pass",
"self-referential-substitutions/003-object-containing-self-reference-is-not-self-referential.conf": "pass",
Expand Down Expand Up @@ -488,6 +495,7 @@
"string-value-concatenation/020-64-bit-number-kept-as-written.conf": "pass",
"string-value-concatenation/021-false-becomes-string-in-concatenation.conf": "pass",
"string-value-concatenation/022-quoted-and-unquoted-strings-concatenate.conf": "pass",
"string-value-concatenation/023-substituted-number-keeps-its-text.conf": "pass",
"substitution-fallback-to-environment/001-env-fallback-for-undefined-path.conf": "pass",
"substitution-fallback-to-environment/002-required-substitution-resolved-from-env.conf": "pass",
"substitution-fallback-to-environment/003-config-value-wins-over-env.conf": "pass",
Expand Down Expand Up @@ -533,6 +541,7 @@
"substitutions/038-whitespace-after-question-mark.conf": "pass",
"substitutions/039-undefined-optional-with-number-is-a-string.conf": "pass",
"substitutions/040-null-blocks-environment-lookup.conf": "pass",
"substitutions/041-undefined-optional-under-a-path-leaves-the-object.conf": "pass",
"unchanged-from-json/001-quoted-string-json-format.conf": "pass",
"unchanged-from-json/002-string-value-type.conf": "pass",
"unchanged-from-json/003-number-value-type.conf": "pass",
Expand Down Expand Up @@ -560,6 +569,9 @@
"unchanged-from-json/025-nan-is-not-a-number.conf": "pass",
"unchanged-from-json/026-surrogate-pair-escape.conf": "pass",
"unchanged-from-json/027-unpaired-surrogate-escape-is-kept.conf": "pass",
"unchanged-from-json/028-integer-beyond-64-bits-is-a-number.conf": "pass",
"unchanged-from-json/029-unicode-escape-with-a-sign-is-invalid.conf": "pass",
"unchanged-from-json/030-unicode-escape-with-non-ascii-digits-is-invalid.conf": "pass",
"units-format/001-number-value-is-default-unit.conf": "pass",
"units-format/002-quoted-string-with-no-unit-uses-default-unit.conf": "pass",
"units-format/003-string-with-unit-name-specifies-interpretation.conf": "pass",
Expand Down Expand Up @@ -610,6 +622,9 @@
"outputs": {
"array-and-object-concatenation/018-object-with-inner-merge-concatenated-with-substitution.conf": "rejected",
"array-and-object-concatenation/019-self-referential-object-concatenation-with-inner-merge.conf": "rejected",
"array-and-object-concatenation/020-optional-override-inside-object-concatenation.conf": "rejected",
"array-and-object-concatenation/021-substitution-over-duplicate-key-inside-object-concatenation.conf": "rejected",
"array-and-object-concatenation/022-duplicate-key-in-list-element-before-list-substitution.conf": "rejected",
"concatenation-whitespace-and-substitutions/007-zero-width-space-between-list-substitutions-is-error.conf": "{\"a\":[1,2],\"x\":[1],\"y\":[2]}",
"concatenation-whitespace-and-substitutions/009-undefined-optional-then-list-is-the-list.conf": "rejected",
"duplicate-keys-and-object-merging/010-null-in-referenced-key-history-does-not-block-merge.conf": "{\"a\":{\"x\":1},\"r\":{\"x\":1},\"v\":{\"x\":1}}",
Expand Down Expand Up @@ -658,6 +673,9 @@
"unchanged-from-json/012-number-with-trailing-dot-illegal.conf": "{\"a\":1}",
"unchanged-from-json/013-trailing-dot-before-exponent-illegal.conf": "{\"a\":-1000}",
"unchanged-from-json/014-negative-leading-zeros-illegal.conf": "{\"a\":-33}",
"unchanged-from-json/028-integer-beyond-64-bits-is-a-number.conf": "{\"a\":\"9223372036854775808\",\"b\":\"-9223372036854775809\",\"c\":\"123456789012345678901234567890\"}",
"unchanged-from-json/029-unicode-escape-with-a-sign-is-invalid.conf": "{\"a\":\"A\"}",
"unchanged-from-json/030-unicode-escape-with-non-ascii-digits-is-invalid.conf": "{\"a\":\"A\"}",
"unquoted-strings/012-unquoted-cannot-start-with-hyphen.conf": "{\"a\":\"-foo\"}",
"unquoted-strings/022-digit-then-exponent-letter-illegal.conf": "{\"a\":\"1e\"}",
"unquoted-strings/025-digit-initial-invalid-number.conf": "{\"a\":\"1.2.3\"}"
Expand Down
Loading
Loading