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
170 changes: 83 additions & 87 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ format changes.

## [Unreleased]

## [1.1.0] — 2026-09-11

### Added

- **pxf: quoted entry names and keyed repeated fields** (#18; draft `-01`
§3.13, protowire#116, reference protowire-go#50). The vendored
`proto/pxf/annotations.proto` gains `(pxf.key) = 1316`, matching
canonical. The grammar accepts a string at entry-name position with
`=` and `{` everywhere (`Assignment::key_quoted`, `Block::name_quoted`
retain the spelling for `FormatDocument`); the schema layer rejects it
outside a keyed repeated field's block. A repeated message-typed field
whose `(pxf.key)` names a singular string field of the element message
decodes from the keyed block form — `children { greeting { … } }` or
`children = { greeting = { … } }` — with the entry name as the key,
entry order as list order, and duplicate names, the empty string, and
a disagreeing explicit key assignment as errors; the anonymous list
form still decodes, with an explicit empty key rejected. `Marshal`
writes the keyed form whenever every key is present, non-empty and
distinct (names bare when identifier-safe, quoted otherwise, the key
field omitted from the entry body), and the anonymous form otherwise.
`CanonicalizeKeyed(Document*, const Descriptor*)` (new
`protowire/pxf/keyed.h`) is the schema-aware AST rewrite behind `fmt`:
eligible anonymous bindings become keyed blocks, `name = { }` becomes
`name { }`, identifier-safe quoted names are unquoted, redundant key
assignments are dropped. `ValidateFile` reports a misplaced
`(pxf.key)` as `ViolationKind::kKeyOption` with a `detail`;
`KeyFieldName`, `KeyField` and `IsKeyed` are exported from
`protowire/pxf/annotations.h`. The spec repo's `testdata/keyed/` is
vendored and every fixture is pinned.

### Fixed

- **pxf: bind-time validation covers the import closure** (#34; draft
Expand All @@ -28,7 +58,7 @@ format changes.
quarter of a small document's decode; the walk now costs ~160 ns.
`UnmarshalOptions::skip_validate` remains the escape hatch for callers
that validated once.

- **pb: repeated numeric fields are marshalled packed** (#32). `Marshal`
wrote one tag + value per element for every element type, so
`ListHolder { values: [1, 2, 3] }` was `08 01 08 02 08 03` where the
Expand All @@ -41,8 +71,6 @@ format changes.
canonical envelope has no repeated numerics, so its bytes are
unchanged.

### Fixed

- **pxf: a dotted string map key is written bare** (#36; protowire#313,
decided as (a)). The identifier-safe test `Marshal` and
`FormatDocument` apply to a string map key stopped at `[A-Za-z0-9_]`,
Expand All @@ -55,33 +83,59 @@ format changes.
`fmt-dotted-keys`, is vendored and pinned; the decoder always read
`a.b:` as one identifier, so only the writers move.

### Added
- **pb: a map entry always carries key and value, zero-valued or not**
(#24; protowire#295, decided on protowire-go#105). `pb.h`'s map branch
applied proto3 zero-skip inside the entry, so `metadata { "": "" }`
serialised as an empty entry (`22022a00`) where protobuf-go, protoc and
C++ protobuf write `22062a040a001200`. Field 1 and field 2 of every
entry are now written unconditionally; a value held through
`std::optional` or a smart pointer that is unset is written as its zero
value. `dump_envelope --vector zero-map-entry` prints the bytes for the
cross-port gate's golden, and an unknown vector name exits 3 with
`not-implemented: <name>`.

- **pxf: quoted entry names and keyed repeated fields** (#18; draft `-01`
§3.13, protowire#116, reference protowire-go#50). The vendored
`proto/pxf/annotations.proto` gains `(pxf.key) = 1316`, matching
canonical. The grammar accepts a string at entry-name position with
`=` and `{` everywhere (`Assignment::key_quoted`, `Block::name_quoted`
retain the spelling for `FormatDocument`); the schema layer rejects it
outside a keyed repeated field's block. A repeated message-typed field
whose `(pxf.key)` names a singular string field of the element message
decodes from the keyed block form — `children { greeting { … } }` or
`children = { greeting = { … } }` — with the entry name as the key,
entry order as list order, and duplicate names, the empty string, and
a disagreeing explicit key assignment as errors; the anonymous list
form still decodes, with an explicit empty key rejected. `Marshal`
writes the keyed form whenever every key is present, non-empty and
distinct (names bare when identifier-safe, quoted otherwise, the key
field omitted from the entry body), and the anonymous form otherwise.
`CanonicalizeKeyed(Document*, const Descriptor*)` (new
`protowire/pxf/keyed.h`) is the schema-aware AST rewrite behind `fmt`:
eligible anonymous bindings become keyed blocks, `name = { }` becomes
`name { }`, identifier-safe quoted names are unquoted, redundant key
assignments are dropped. `ValidateFile` reports a misplaced
`(pxf.key)` as `ViolationKind::kKeyOption` with a `detail`;
`KeyFieldName`, `KeyField` and `IsKeyed` are exported from
`protowire/pxf/annotations.h`. The spec repo's `testdata/keyed/` is
vendored and every fixture is pinned.
- **pxf fmt: the quotes on a string map key spelled like a keyword or an
integer are kept** (#27; draft `-01` § Entries and Keys, "Canonical
spelling of map keys", protowire#306). `FormatDocument` wrote every
identifier-shaped key bare, so `"true": "v"` on a `map<string, V>`
became `true: "v"` — a bool key, which no longer binds on a string
`K`. `MapEntry` gains `key_quoted`; a quoted key is unquoted only when
it is identifier-safe and not `null` / `true` / `false`, and a bare key
stays bare (so `404:` is no longer requoted on the way through fmt).
The marshaller uses the same test (`IsIdentifierSafe`, exported from
`protowire/pxf/format.h`), so `"123"`, `"true"` and `"null"` string
keys are written quoted; it also sorts bool keys (false, true). The
spec repo's `testdata/map-keys/` fixtures are vendored and pinned.

- **pxf: bool map keys bind in exactly the grammar's spellings**
(absorbed into #27; protowire#284). The decoder bound any non-`true`
key on a `map<bool, V>` to `false` — `t`, `yes`, `"TRUE"`, `"0"` all
silently became a key the author did not write. A bool key is now
`true` / `false` bare (the keyword spelling, newly accepted as a map
key by the parser and decoder), `0` / `1` bare, or `"true"` /
`"false"` quoted, and anything else is an error naming the key and
field; on a string `K` the bare keyword is an error that says to
write it quoted.

- **pxf: the lexer reads fractional and `µs` duration literals** (#20).
`1.5ms`, `1.234567ms`, `312.5µs`, `1h30m0.5s` and `-2.5s` — the forms
every port's encoder writes for a `google.protobuf.Duration` that is
not a whole multiple of its largest unit — tokenised as a float
followed by an identifier (or an integer followed by a stray byte for
the two-byte `µ`), so this port could not read its own output for any
measured latency. `LexNumber` now consumes an optional fraction first
and takes the duration branch when a time unit follows; the duration
scan admits `.` before a digit and the `C2 B5` micro sign (U+00B5
only — U+03BC is not in the grammar). Mirrors protowire-go#76.

- **detail: negative durations split toward zero, and the int64 edges
round-trip** (absorbed into #20). `ParseDuration` normalised nanos into
`[0, 1e9)`, so `-1ns` decoded to `seconds=-1, nanos=999999999`, which
`google.protobuf.Duration` forbids (nanos must carry the sign of the
value) and no other port reads back as `-1ns`; it now splits like
`time.Duration`. `FormatDuration` and `ParseDuration` take the
magnitude in unsigned arithmetic, so `INT64_MIN` formats as
`-2562047h47m16.854775808s` instead of a placeholder and reads back.

### Security

Expand Down Expand Up @@ -138,64 +192,6 @@ format changes.
and the three `pb/decimal-*` rows the manifest skipped for this
port.

### Fixed

- **pb: a map entry always carries key and value, zero-valued or not**
(#24; protowire#295, decided on protowire-go#105). `pb.h`'s map branch
applied proto3 zero-skip inside the entry, so `metadata { "": "" }`
serialised as an empty entry (`22022a00`) where protobuf-go, protoc and
C++ protobuf write `22062a040a001200`. Field 1 and field 2 of every
entry are now written unconditionally; a value held through
`std::optional` or a smart pointer that is unset is written as its zero
value. `dump_envelope --vector zero-map-entry` prints the bytes for the
cross-port gate's golden, and an unknown vector name exits 3 with
`not-implemented: <name>`.

- **pxf fmt: the quotes on a string map key spelled like a keyword or an
integer are kept** (#27; draft `-01` § Entries and Keys, "Canonical
spelling of map keys", protowire#306). `FormatDocument` wrote every
identifier-shaped key bare, so `"true": "v"` on a `map<string, V>`
became `true: "v"` — a bool key, which no longer binds on a string
`K`. `MapEntry` gains `key_quoted`; a quoted key is unquoted only when
it is identifier-safe and not `null` / `true` / `false`, and a bare key
stays bare (so `404:` is no longer requoted on the way through fmt).
The marshaller uses the same test (`IsIdentifierSafe`, exported from
`protowire/pxf/format.h`), so `"123"`, `"true"` and `"null"` string
keys are written quoted; it also sorts bool keys (false, true). The
spec repo's `testdata/map-keys/` fixtures are vendored and pinned.

- **pxf: bool map keys bind in exactly the grammar's spellings**
(absorbed into #27; protowire#284). The decoder bound any non-`true`
key on a `map<bool, V>` to `false` — `t`, `yes`, `"TRUE"`, `"0"` all
silently became a key the author did not write. A bool key is now
`true` / `false` bare (the keyword spelling, newly accepted as a map
key by the parser and decoder), `0` / `1` bare, or `"true"` /
`"false"` quoted, and anything else is an error naming the key and
field; on a string `K` the bare keyword is an error that says to
write it quoted.



- **pxf: the lexer reads fractional and `µs` duration literals** (#20).
`1.5ms`, `1.234567ms`, `312.5µs`, `1h30m0.5s` and `-2.5s` — the forms
every port's encoder writes for a `google.protobuf.Duration` that is
not a whole multiple of its largest unit — tokenised as a float
followed by an identifier (or an integer followed by a stray byte for
the two-byte `µ`), so this port could not read its own output for any
measured latency. `LexNumber` now consumes an optional fraction first
and takes the duration branch when a time unit follows; the duration
scan admits `.` before a digit and the `C2 B5` micro sign (U+00B5
only — U+03BC is not in the grammar). Mirrors protowire-go#76.

- **detail: negative durations split toward zero, and the int64 edges
round-trip** (absorbed into #20). `ParseDuration` normalised nanos into
`[0, 1e9)`, so `-1ns` decoded to `seconds=-1, nanos=999999999`, which
`google.protobuf.Duration` forbids (nanos must carry the sign of the
value) and no other port reads back as `-1ns`; it now splits like
`time.Duration`. `FormatDuration` and `ParseDuration` take the
magnitude in unsigned arithmetic, so `INT64_MIN` formats as
`-2562047h47m16.854775808s` instead of a placeholder and reads back.

## [1.0.0] — 2026-05-13

First major-version cut. Implements the three one-time spec changes
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(protowire VERSION 1.0.0 LANGUAGES CXX)
project(protowire VERSION 1.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Cutting a release:
1. Bump `project(... VERSION X.Y.Z ...)` in `CMakeLists.txt`.
2. Add a `## [X.Y.Z]` section to `CHANGELOG.md`.
3. Tag `vX.Y.Z` on `main`.
4. The `.github/workflows/release.yml` workflow will produce build
artifacts and post a GitHub Release. (Conan / vcpkg distribution is
intentionally deferred to a later milestone.)
4. Publish a GitHub Release on the tag (`gh release create vX.Y.Z`)
whose body summarises the new `CHANGELOG.md` section. There is no
release workflow and no build artifacts; consumers build from source
via the cmake flow above. (Conan / vcpkg distribution is intentionally
deferred to a later milestone.)
Loading