Skip to content

Latest commit

 

History

History
133 lines (102 loc) · 8.79 KB

File metadata and controls

133 lines (102 loc) · 8.79 KB

CLAUDE.md

Guidance for Claude Code when working on gobdotnet. Read this before making changes.

Session startup protocol

  1. Read PROGRESS.md first. It tracks which phase is active, what's done, and what the next session should start with. Work sessions may be interrupted by quota limits or handoffs — the progress file is how continuity is maintained.
  2. Skim PRD.md sections relevant to the current phase.
  3. Before ending a session, update PROGRESS.md:
    • Update "Current state" at the top.
    • Check off completed acceptance items in the active phase.
    • Append a session handoff entry at the bottom.
    • If you discovered work that doesn't fit a phase, log it under "Discovered work".

Do not skip ahead of the current phase. Each phase depends on the previous one being solid. If Phase 1 codec tests aren't passing, don't start Phase 2 wire types.

Project

Pure C# port of Go's encoding/gob binary serialization format. Sister library to pygob (Python). Full PRD lives in PRD.md — read it before making architectural decisions.

  • Target: .NET 10, no external runtime dependencies, AOT-compatible.
  • Test runner: xUnit. FsCheck.Xunit for property tests. Xunit.SkippableFact for Go cross-validation.
  • Wire format version: Go 1.22. New wire types (rare) go in minor versions.
  • Performance target: within 2× of Newtonsoft.Json. We do NOT compete with MessagePack-CSharp.

Solution layout

GobDotNet/                       # The library
GobDotNet.SourceGenerators/      # [GobStruct] → compile-time schema (netstandard2.0)
GobDotNet.Tests/                 # xUnit, property tests, Go cross-validation
  testdata/                      # .gob + .json sidecars — copied from pygob, evolves here
  go_verify/main.go              # Copied from pygob, evolves here
  generate_testdata.go           # Copied from pygob, evolves here
GobDotNet.Benchmarks/            # BenchmarkDotNet vs. Newtonsoft.Json
PRD.md                           # Design doc — authoritative
PROGRESS.md                      # Phase tracker — read and update every session
CLAUDE.md                        # This file

The testdata/ and go_verify/ files were copied from pygob at project start and evolve independently. Do not try to sync them back.

Core design rules

These are non-negotiable. If a change seems to require violating one, stop and ask.

  • Wire fidelity is the top priority. Any byte stream Go's encoder produces must decode correctly; any byte stream we produce must decode in Go. If a round-trip test passes but go_verify fails, the bug is ours.
  • Thread safety is expected. GobEncoder and GobDecoder serialize concurrent calls via an internal lock. Don't add APIs that assume single-threaded use.
  • Caller owns streams. GobEncoder and GobDecoder do NOT implement IDisposable. Do not add Dispose() methods.
  • EndOfStreamException for EOS. Decode() throws; TryDecode returns false. Never swallow EOS silently.
  • No external runtime dependencies. BCL only for the main library. Test and benchmark projects can depend on xUnit, FsCheck, BenchmarkDotNet, Newtonsoft.Json.
  • No BinaryReader/BinaryWriter — they default to little-endian. Use the manual GobReader/GobWriter built on Span<byte>.
  • Source generator is the primary path for [GobStruct]. Reflection is the documented fallback.

Gob wire format landmines

Bugs here are silent and expensive. Every change that touches encoding or decoding needs to keep these in mind:

  • First field delta is 1, not 0. Field indices start at -1. Delta=0 is the struct terminator.
  • Collection types have empty CommonType.Name. Field Id arrives with delta=2 (skipping the omitted Name).
  • Zero-valued fields are omitted on the wire and pre-populated with zero values on decode.
  • Floats are byte-reversed IEEE 754 then encoded as unsigned int. This is for trailing-zero compression.
  • Top-level non-struct values are singleton-wrapped: 0x00 value. The 0x00 precedes the value.
  • User type IDs start at 65. Go's pre-decrement allocates the first type as 64; we match Go's stated constant for determinism.
  • Interface inline type defs end on a positive type ID, not EOF and not 0x00. The positive int IS the concrete value's type ID.
  • Interface concrete values have an inner uint N byte-count wrapper inside the message payload. Different from top-level struct framing.
  • Nested struct fields are unwrapped — no type-def, no byte-count prefix, just raw delta-encoded bytes + 0x00.
  • BinaryMarshaler type names on the wire are unqualified ("Time", "UUID") — from Go's reflect.Type.Name(). This is distinct from interface{} concrete-type registration, which uses qualified names ("main.Point").
  • Go map iteration is non-deterministic. Never byte-compare map-containing gob output. Compare decoded values structurally.

When in doubt, re-read the "Lessons Learned" section of PRD.md.

Code conventions

  • Enable <Nullable>enable</Nullable> and annotate every public API.
  • Use record types for wire type structs (value equality is useful in tests).
  • Use sealed on all internal implementation classes and on public API classes where extension makes no sense.
  • Use ReadOnlySpan<byte> for decode paths in codec interfaces.
  • Use MemoryStream to build outgoing gob messages — byte counts fall out naturally when you flush.
  • Use (string Name, GobFieldType Type) value tuples for schema fields. No dedicated GobField class.
  • Collection expressions [] and [..] where they read naturally.
  • ArgumentNullException.ThrowIfNull(...) for parameter validation.
  • Exceptions: GobDecodeException / GobEncodeException for format errors; EndOfStreamException for EOS; InvalidCastException for type mismatches in Decode<T>(); GobEncodeException at schema derivation if a user tries to use BigInteger or other unsupported types.

Testing workflow

Four layers, in order of authority:

  1. Go → C# decoder tests — parametrized over testdata/*.gob with JSON sidecar expectations.
  2. C# → C# round-trip tests — catches asymmetric bugs.
  3. C# → Go go_verify cross-validation — authoritative proof of wire compatibility. Skipped (not failed) when Go isn't on PATH.
  4. Property-based tests via FsCheck — catches edge cases example-based tests miss.

Plus stress tests for thread safety and a dedicated benchmark suite.

dotnet test                                        # run everything
dotnet test --filter "Category=GoVerify"           # only cross-validation
dotnet test --logger "console;verbosity=detailed"  # verbose
dotnet run -c Release --project GobDotNet.Benchmarks
go run tests/generate_testdata.go                  # regenerate fixtures
echo "" | go run ./tests/go_verify struct_simple   # manual verifier check

Run go_verify tests early and often during encoder work. They catch symmetric bugs that C#-only round-trips will happily pass.

When making changes

  • New feature or wire-format behavior: update PRD.md in the same PR. The PRD is the source of truth.
  • Bug fix in the encoder or decoder: add a regression test under the appropriate layer AND a go_verify test if the bug affects cross-language output.
  • Changes to the source generator: run tests with both the generator path and the reflection fallback. The fallback must stay behaviorally equivalent.
  • Public API additions: think about thread safety. If the new method isn't idempotent under the internal lock, say so in the xmldoc.
  • Performance work: run benchmarks before and after. Commit results under GobDotNet.Benchmarks/results/.
  • Any substantive session: update PROGRESS.md before finishing.

What NOT to do

  • Don't add Dispose() to encoders/decoders.
  • Don't add convenience APIs that silently truncate data. BigInteger must fail loud.
  • Don't change type ID assignment strategy — byte-level test stability depends on starting at 65.
  • Don't use BinaryReader/BinaryWriter.
  • Don't accept a "round-trip passes in C#" as sufficient evidence — always verify with go_verify for encoder changes.
  • Don't add async APIs (EncodeAsync, DecodeAsync) — explicitly out of scope. If you think the library needs them, open an issue first.
  • Don't reintroduce a GobUInt wrapper type. Native ulong is sufficient.
  • Don't quietly "fix" observed wire-format quirks. If the Go source does it, we do it — that's the point.
  • Don't skip phases in PROGRESS.md. If you think a phase needs reordering, surface the choice rather than silently rearranging.

References

  • PRD.md — authoritative design doc.
  • PROGRESS.md — phase tracker, updated every session.
  • encoding/gob in the Go source tree — ultimate source of truth for wire format.
  • pygob repo — sister library, useful for comparing design choices and test coverage.