Skip to content

pxf: bind-time validation covers the import closure (#34) - #39

Merged
trendvidia merged 2 commits into
mainfrom
pxf-validate-closure
Sep 10, 2026
Merged

trendvidia merged 2 commits into
mainfrom
pxf-validate-closure

Conversation

@trendvidia

@trendvidia trendvidia commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

Fixes #34.

What

ValidateFile / ValidateDescriptor walked the declaring file only; draft -01 § Scope of Bind-Time Checks requires the transitive import closure and forbids reporting a schema conformant on the declaring file alone. They now walk the closure, each path once (the diamond A → B, C → D reports D once), Violation::file names the declaring file, and results sort by file then element (stable, so a field's reserved-name and (pxf.key) violations keep walk order). The per-decode gate in Unmarshal therefore refuses a schema whose import is non-conformant, whether or not the document reaches the imported type.

Cost, measured rather than inferred. descriptor.proto is in the closure of every annotated schema (through pxf/annotations.proto) and cannot carry a PXF violation. Walking it cost 1.1 µs per call against a 3.9 µs bench_pxf decode of the 652-byte bench document — a quarter of a small decode, though a small fraction of a real one. google/protobuf/* is therefore skipped in the walk, and the assumption is pinned by PxfSchema.GoogleProtobufFilesAreConformant, which validates descriptor.proto and five well-known types directly (no reserved names, no (pxf.key)) and fails the day that stops being true. With the skip:

closure ns per ValidateFile
schema + pxf/annotations.proto + descriptor.proto, full walk ~1230
same, google/protobuf skipped ~160

bench_pxf unmarshal on the spec's bench fixture (652 bytes; imports timestamp and duration, both skipped) moves from 3892–3902 ns/op (three runs before) to 3998–4042 ns/op (two runs after): about +3%, consistent with the ~100 ns the closure bookkeeping (path list, dependency loop) adds per decode. On a small document that is a visible fraction; on a real document it is a fixed ~100 ns. Callers decoding one schema repeatedly can set skip_validate after validating once.

I did not add the descriptor-keyed cache the reference has: a freed DescriptorPool can hand a new file descriptor the same address, and this repository's tests compile a fresh pool per fixture, so such a cache would return a stale result for a different schema. UnmarshalOptions::skip_validate remains the escape hatch for callers that validated once.

Evidence

  • PxfSchema.ImportedFileViolationsReportedWithDeclaringFile: A imports B; B's null field and misplaced (pxf.key) are reported with file == "b_closure.proto", through both ValidateFile and ValidateDescriptor, and Unmarshal of an empty document against A's root is refused naming B.
  • PxfSchema.DiamondImportReportedOnceAndSortedByFile: D reported once; order a, b, d.
  • PxfSchema.AnnotationsClosureIsConformant, PxfSchema.GoogleProtobufFilesAreConformant.
  • All prior schema tests unchanged. ctest --test-dir build → 100% tests passed out of 297. clang-format --dry-run -Werror clean.

Note: this stacks on nothing; #38 (#32) is independent.

https://claude.ai/code/session_019rmcvyuzWRwq4e13b1KmB4

ValidateFile and ValidateDescriptor walked the declaring file only,
where draft -01 § Scope of Bind-Time Checks says the schema a check
covers is that file together with its transitive imports, and that a
tool MUST NOT report a schema conformant having checked only the
declaring file. A field named null or a misplaced (pxf.key) in an
imported .proto bound without a violation and was silently unhonoured
when the decoder reached it.

The walk now covers the closure, each path once (the diamond A → B, C
→ D reports D once); Violation::file names the declaring file, and the
result sorts by file, then element, stable. google/protobuf/* files are
skipped: they carry no PXF annotation and no reserved name, and
descriptor.proto — in the closure of every annotated schema through
pxf/annotations.proto — measured 1.1 µs to walk against a 3.9 µs decode
of the bench document. With the skip the closure walk costs ~160 ns.
The assumption is pinned by PxfSchema.GoogleProtobufFilesAreConformant,
which validates each of those files directly. No cache keyed by
descriptor pointer, as the reference has: a freed DescriptorPool can
hand a new file the same address, which this repository's tests do on
every fixture.

Claude-Session: https://claude.ai/code/session_019rmcvyuzWRwq4e13b1KmB4
@trendvidia
trendvidia merged commit c221b3b into main Sep 10, 2026
10 checks passed
@trendvidia
trendvidia deleted the pxf-validate-closure branch September 10, 2026 06:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pxf: ValidateFile / ValidateDescriptor check the declaring file only; draft -01 § Scope of Bind-Time Checks requires the import closure

2 participants