Skip to content

cpp emitter: string-helper guards scoped per package (#189, PR B) - #220

Merged
gafferongames merged 1 commit into
mainfrom
fix-189-string-helper-guard
Sep 1, 2026
Merged

cpp emitter: string-helper guards scoped per package (#189, PR B)#220
gafferongames merged 1 commit into
mainfrom
fix-189-string-helper-guard

Conversation

@gafferongames

Copy link
Copy Markdown
Contributor

PR B of the #189 protocol, licensed by bench/LOCK's one-shot carve (owner ruling 2026-08-31).

The defect

The C++ emitter's string helpers (schema_utf8_valid, schema_interior_null) are emitted inside namespace <package> but guarded by TU-wide include guards (SCHEMA_UTF8_VALID_DEFINED, SCHEMA_INTERIOR_NULL_DEFINED). Two generated units from two packages, both with string(N) fields, in one translation unit: the first unit sets the guard, the second unit's namespace never gets its helpers, and its string wire functions do not compile.

The fix

internal/codegen/cpp/utf8.go and internal/codegen/cpp/nullscan.go: the guards now carry the package name — SCHEMA_<PKG>_UTF8_VALID_DEFINED / SCHEMA_<PKG>_INTERIOR_NULL_DEFINED — one copy per package per TU, following the emitFlagAppendHelper pattern that already did this correctly in the same emitter. Formatting conventions unchanged; the helper bodies are byte-identical.

C-side verdict: NOT defective, left untouched. The C emitter's helpers are file-scope static with trailing-underscore names, so the TU-wide guard is exactly right there — the second unit's functions call the first emission's definition (this is also what commit f1c0f8c recorded: "the C leg needs no guard workaround"). The SCHEMA_WRITE_INLINE / SCHEMA_READ_INLINE macro blocks in C++ are preprocessor macros, not namespace-local symbols — TU-wide guards are correct for them and they are untouched too.

Regression test (red on main, green here)

test/guard/: two packages (alpha, beta), both with string(15) fields, generated at build time into build/guard-generated/ (test-only corpus — never part of the committed generated/ tree) and included into ONE translation unit (test/guard/main.cpp, new make leg build/schema_test_guard). Round-trips a string through each namespace.

Red — old emitter (this branch with the two emitter files stashed back to main's state), verbatim:

c++ -std=c++17 -Wall -Wextra -Werror -ffp-contract=off -I../serialize -Ibuild/guard-generated test/guard/main.cpp -o build/schema_test_guard
build/guard-generated/BetaWire.h:172:23: error: use of undeclared identifier 'schema_utf8_valid'; did you mean 'alpha::schema_utf8_valid'?
build/guard-generated/BetaWire.h:183:10: error: use of undeclared identifier 'schema_interior_null'; did you mean 'alpha::schema_interior_null'?
2 errors generated.
make: *** [build/schema_test_guard] Error 1

Green — with the fix (git stash pop, regenerate, rebuild):

./bin/schema generate --lang cpp --out build/guard-generated test/guard/Alpha.schema
./bin/schema generate --lang cpp --out build/guard-generated test/guard/Beta.schema
c++ -std=c++17 -Wall -Wextra -Werror -ffp-contract=off -I../serialize -Ibuild/guard-generated test/guard/main.cpp -o build/schema_test_guard
$ ./build/schema_test_guard
OK

bench/cpp workaround

Already gone: the #undef SCHEMA_UTF8_VALID_DEFINED / #undef SCHEMA_INTERIOR_NULL_DEFINED workaround was removed by #204 (commit 3cf6b90) when the example units left the bench TU. grep -rn "SCHEMA_UTF8_VALID_DEFINED\|SCHEMA_INTERIOR_NULL_DEFINED" bench/ matches nothing today; there is nothing left to remove.

Generated-diff inventory

make test green (all nine legs); make generated-current prints generated/ tree is current. The regeneration diff is exactly the guard lines — 6 lines in each of 4 generated files (plus the same 6 in their 3 text-golden mirrors), nothing else:

  • generated/cpp/ClausesWire.h, generated/cpp/JoinsWire.h, generated/cpp/WireWire.h (package example):
    • #ifndef/#define/#endif // SCHEMA_UTF8_VALID_DEFINEDSCHEMA_EXAMPLE_UTF8_VALID_DEFINED
    • #ifndef/#define/#endif // SCHEMA_INTERIOR_NULL_DEFINEDSCHEMA_EXAMPLE_INTERIOR_NULL_DEFINED
  • generated/bench/cpp/BenchWire.h (package bench): same six lines → SCHEMA_BENCH_*
  • testdata/golden/cpp/{ClausesWire.h,JoinsWire.h,WireWire.h}: text mirrors of the above
  • No C outputs changed. Nothing under testdata/wire changed (git diff main -- testdata/wire is empty). Ludicrous corpora carry no strings, so no helper emissions there.

Reproduction control (bench/LOCK control 2)

The frozen shapes' c/cpp rows re-run in-sitting on this branch (bench/run.sh --only cpp, --only c, Release O3, quiet box) against the sitting-2 reference bench/results/2026-09-01-sitting2-arm64-macbook.csv (medians, msgs/sec):

row reference this branch ratio
cpp bench_mixed write 7,442,587 7,559,675 +1.57%
cpp bench_mixed round_trip 3,492,440 3,550,572 +1.66%
cpp bitpacker write 58,460 60,071 +2.76%
cpp bitpacker read 89,999 92,563 +2.85%
c bench_mixed write 7,220,881 7,299,110 +1.08%
c bench_mixed round_trip 3,584,859 3,575,579 −0.26%
c bitpacker write 60,022 59,778 −0.41%
c bitpacker read 56,751 56,098 −1.15%

All rows within ±2.9% of the reference — inside the ~5% band. The control CSVs went to scratch files; nothing under bench/results/ is touched.

Wire bytes

Byte-unchanged everywhere: the change is include-guard text in C++ headers only — zero effect on any emitted codec logic, and the wire goldens under testdata/wire are untouched, byte for byte.

PR C (the single-file bench/LOCK prefix restoration) follows once this merges, per the protocol.

🤖 Generated with Claude Code

The emitted string helpers (schema_utf8_valid, schema_interior_null) are
namespace-local, but their include guards were TU-wide — so the SECOND
generated unit included into one translation unit lost its helpers and its
string wire functions did not compile. The guards now carry the package name
(SCHEMA_<PKG>_UTF8_VALID_DEFINED / SCHEMA_<PKG>_INTERIOR_NULL_DEFINED),
following the emitFlagAppendHelper pattern already in the emitter: one copy
per package per TU.

C is NOT defective and stays untouched: its helpers are file-scope static
with trailing-underscore names, so the TU-wide guard is exactly right there
(the second unit's functions call the first emission's definition).

Regression test: test/guard — two packages, both with string(N) fields,
generated at build time into build/ and included into ONE translation unit
(build/schema_test_guard). Red on the old emitter, green here.

The bench/cpp leg-local workaround (#undef of the two guards) was already
removed by #204 when the example units left the bench TU; nothing remains.

Licensed by bench/LOCK's one-shot carve (owner ruling 2026-08-31, #189).
Generated diff: guard lines only, cpp outputs only; wire bytes unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gafferongames
gafferongames marked this pull request as ready for review September 1, 2026 01:03
@gafferongames
gafferongames merged commit 9e0cb2c into main Sep 1, 2026
8 checks passed
@gafferongames
gafferongames deleted the fix-189-string-helper-guard branch September 1, 2026 01:04
gafferongames added a commit that referenced this pull request Sep 1, 2026
The protocol closes: PR A suspended the emitter and generated-c/cpp
prefixes for exactly one licensed change, PR B (#220) landed the
string-helper guard fix under the reproduction control (c/cpp rows
within 2.9% of the sitting-2 reference), and this single-file edit
restores the lock. One modernization against the pre-carve list: the
cpp ludicrous output lives at generated/cpp/ludicrous today, already
covered by the generated/cpp/ prefix, so the historical
generated/cpp-ludicrous/ line (which matches nothing) is not restored.

Co-authored-by: Rowan Claude <rowan@mas-bandwidth.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant