Skip to content

Map Rust u8/u16 to Go uint8/uint16 instead of signed int - #292

Open
jaideeppyne wants to merge 1 commit into
1Password:mainfrom
jaideeppyne:fix/go-unsigned-int-types
Open

jaideeppyne wants to merge 1 commit into
1Password:mainfrom
jaideeppyne:fix/go-unsigned-int-types

Conversation

@jaideeppyne

Copy link
Copy Markdown

What

The Go generator maps Rust u8 and u16 to signed Go int, even though it maps u32 → uint32 and u64 → uint64.

// core/src/language/go.rs
SpecialRustType::I8
| SpecialRustType::U8      // unsigned, but folded into signed `int`
| SpecialRustType::U16     // unsigned, but folded into signed `int`
| SpecialRustType::I32
| ... => "int".into(),
SpecialRustType::U32 => "uint32".into(),

Why it's a bug

  • Internally inconsistent: u32/u64 keep their unsignedness but u8/u16 don't — and Go has native uint8/uint16, so there's no Go-idiom reason to widen them to signed int.
  • The repo's own fixture proves the intended contract. use_correct_integer_types feeds u8/u16 and expects type fidelity: Swift emits UInt8/UInt16, Kotlin emits UByte/UShort. Only Go drops the signedness.
  • Interop consequence: the generated Go type is signed where the Rust type is unsigned, so a Go service can assign -1 or a value above the type's range into an "unsigned" field; serde on the Rust side then rejects it on deserialize. The correct uint8/uint16 mapping lets Go's own type system enforce what serde already requires.

Currently, right next to each other in use_correct_integer_types/output.go:

E int    `json:"e"`   // Rust u8
F int    `json:"f"`   // Rust u16
G uint32 `json:"g"`   // Rust u32

How

             SpecialRustType::I8
-            | SpecialRustType::U8
-            | SpecialRustType::U16
             | SpecialRustType::I32
             | SpecialRustType::I16
             | SpecialRustType::ISize
             | SpecialRustType::USize => "int".into(),
+            SpecialRustType::U8 => "uint8".into(),
+            SpecialRustType::U16 => "uint16".into(),
             SpecialRustType::U32 => "uint32".into(),

(Kept surgical to the unambiguous unsigned bug; usize → int is a separate, platform-width-dependent question and left out of scope.)

Tests

Regenerated the Go snapshots with UPDATE_EXPECT=1. The diff is only int → uint8/uint16 for u8/u16 fields across 7 fixtures (e.g. Age, Red, OptionalU16 → *uint16, and use_correct_integer_types's E/F); i32 fields such as ExtraSpecialField1 correctly stay int. The snapshot suite is the regression guard — reverting only go.rs (keeping the corrected snapshots) makes the Go snapshot tests fail. Full typeshare-core suite passes (361 tests), cargo fmt --check is clean.


Disclosure: this change was prepared with AI assistance and reviewed/verified by me before submission.

The Go generator folded u8 and u16 into the signed `int` arm, while u32 maps
to `uint32` and u64 to `uint64`. This silently drops the unsignedness for the
two smallest unsigned types, which is internally inconsistent and lets a Go
service place a negative or out-of-range value into a field that is unsigned on
the Rust side — serde then rejects it on deserialization.

The repo's own `use_correct_integer_types` fixture already establishes the
intended contract: for the same u8/u16 inputs, Swift emits UInt8/UInt16 and
Kotlin emits UByte/UShort. Map u8 -> uint8 and u16 -> uint16 so Go matches.

Regenerated the Go snapshots; the only changes are int -> uint8/uint16 for u8/u16
fields (i32 fields stay `int`). Full typeshare-core suite passes.

This branch has not been deployed

No deployments
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