Skip to content
Draft
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
94 changes: 63 additions & 31 deletions cmd/generate-bindings/solana/anchor-go/generator/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ func (g *Generator) gen_constants() (*OutputFile, error) {
_ = ty
// "type":{"array":["u8",23]},"value":"[115, 101, 110, 100, 95, 119, 105, 116, 104, 95, 115, 119, 97, 112, 95, 100, 101, 108, 101, 103, 97, 116, 101]"
var b []any
err := json.Unmarshal([]byte(co.Value), &b)
dec := json.NewDecoder(strings.NewReader(co.Value))
dec.UseNumber()
err := dec.Decode(&b)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal array constants[%d] %s: %w", coi, spew.Sdump(co), err)
}
Expand Down Expand Up @@ -260,36 +262,66 @@ func (g *Generator) gen_constants() (*OutputFile, error) {
}).Op("{").ListFunc(func(byteGroup *Group) {
for _, val := range b[:] {
switch ty.Type.(type) {
case *idltype.U8:
byteGroup.Lit(byte(val.(float64)))
case *idltype.I8:
byteGroup.Lit(int8(val.(float64)))
case *idltype.U16:
byteGroup.Lit(uint16(val.(float64)))
case *idltype.I16:
byteGroup.Lit(int16(val.(float64)))
case *idltype.U32:
byteGroup.Lit(uint32(val.(float64)))
case *idltype.I32:
byteGroup.Lit(int32(val.(float64)))
case *idltype.U64:
byteGroup.Lit(uint64(val.(float64)))
case *idltype.I64:
byteGroup.Lit(int64(val.(float64)))
case *idltype.F32:
// TODO: is this correct? Are they encoded as strings?
v, err := strconv.ParseFloat(val.(string), 32)
if err != nil {
panic(fmt.Errorf("failed to parse f32 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(float32(v))
case *idltype.F64:
// TODO: is this correct? Are they encoded as strings?
v, err := strconv.ParseFloat(val.(string), 64)
if err != nil {
panic(fmt.Errorf("failed to parse f64 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(v)
case *idltype.U8:
v, err := strconv.ParseUint(val.(json.Number).String(), 10, 8)
if err != nil {
panic(fmt.Errorf("failed to parse u8 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(byte(v))
case *idltype.I8:
v, err := strconv.ParseInt(val.(json.Number).String(), 10, 8)
if err != nil {
panic(fmt.Errorf("failed to parse i8 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(int8(v))
case *idltype.U16:
v, err := strconv.ParseUint(val.(json.Number).String(), 10, 16)
if err != nil {
panic(fmt.Errorf("failed to parse u16 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(uint16(v))
case *idltype.I16:
v, err := strconv.ParseInt(val.(json.Number).String(), 10, 16)
if err != nil {
panic(fmt.Errorf("failed to parse i16 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(int16(v))
case *idltype.U32:
v, err := strconv.ParseUint(val.(json.Number).String(), 10, 32)
if err != nil {
panic(fmt.Errorf("failed to parse u32 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(uint32(v))
case *idltype.I32:
v, err := strconv.ParseInt(val.(json.Number).String(), 10, 32)
if err != nil {
panic(fmt.Errorf("failed to parse i32 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(int32(v))
case *idltype.U64:
v, err := strconv.ParseUint(val.(json.Number).String(), 10, 64)
if err != nil {
panic(fmt.Errorf("failed to parse u64 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(uint64(v))
case *idltype.I64:
v, err := strconv.ParseInt(val.(json.Number).String(), 10, 64)
if err != nil {
panic(fmt.Errorf("failed to parse i64 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(int64(v))
case *idltype.F32:
v, err := strconv.ParseFloat(val.(json.Number).String(), 32)
if err != nil {
panic(fmt.Errorf("failed to parse f32 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(float32(v))
case *idltype.F64:
v, err := strconv.ParseFloat(val.(json.Number).String(), 64)
if err != nil {
panic(fmt.Errorf("failed to parse f64 in constants[%d] %s: %w", coi, spew.Sdump(co), err))
}
byteGroup.Lit(v)
case *idltype.String:
v, err := strconv.Unquote(val.(string))
if err != nil {
Expand Down
102 changes: 102 additions & 0 deletions cmd/generate-bindings/solana/anchor-go/generator/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,108 @@ func TestGenConstantsErrorCases(t *testing.T) {
})
}

// TestGenConstantsLargeU64I64ArrayPrecision verifies that u64 and i64 array
// elements above 2^53 are emitted with full 64-bit precision. json.Unmarshal
// into []any decodes numbers as float64, which silently rounds integers larger
// than 2^53. This test catches that: if the generator still uses float64 casts,
// the expected exact values will not appear in the generated code.
func TestGenConstantsLargeU64I64ArrayPrecision(t *testing.T) {
t.Run("u64 array with values above 2^53", func(t *testing.T) {
constants := []idl.IdlConst{
{
Name: "LARGE_U64_ARRAY",
Ty: &idltype.Array{
Type: &idltype.U64{},
Size: &idltype.IdlArrayLenValue{Value: 4},
},
// 2^53 = 9007199254740992 is the last integer float64 represents exactly.
// 2^53+1 and 2^53+3 are NOT representable in float64 and will be rounded
// to 2^53 and 2^53+4 respectively if parsed through float64.
Value: "[9007199254740993, 9007199254740995, 18446744073709551615, 9007199254740992]",
},
}

idlData := &idl.Idl{Constants: constants}
gen := &Generator{idl: idlData, options: &GeneratorOptions{Package: "test"}}

outputFile, err := gen.gen_constants()
require.NoError(t, err)

generatedCode := outputFile.File.GoString()

// 2^53+1 = 0x20000000000001 — NOT exactly representable in float64
assert.Contains(t, generatedCode, "uint64(0x20000000000001)",
"9007199254740993 (2^53+1) was rounded; float64 precision loss in u64 array element")
// 2^53+3 = 0x20000000000003 — NOT exactly representable in float64
assert.Contains(t, generatedCode, "uint64(0x20000000000003)",
"9007199254740995 (2^53+3) was rounded; float64 precision loss in u64 array element")
// max u64 = 0xffffffffffffffff
assert.Contains(t, generatedCode, "uint64(0xffffffffffffffff)",
"18446744073709551615 (max u64) was rounded; float64 precision loss in u64 array element")
// 2^53 exactly representable — should always work
assert.Contains(t, generatedCode, "uint64(0x20000000000000)",
"9007199254740992 (2^53) should be emitted correctly")
})

t.Run("i64 array with values above 2^53", func(t *testing.T) {
constants := []idl.IdlConst{
{
Name: "LARGE_I64_ARRAY",
Ty: &idltype.Array{
Type: &idltype.I64{},
Size: &idltype.IdlArrayLenValue{Value: 4},
},
Value: "[9007199254740993, -9007199254740993, 9223372036854775807, -9223372036854775808]",
},
}

idlData := &idl.Idl{Constants: constants}
gen := &Generator{idl: idlData, options: &GeneratorOptions{Package: "test"}}

outputFile, err := gen.gen_constants()
require.NoError(t, err)

generatedCode := outputFile.File.GoString()

// 2^53+1 positive
assert.Contains(t, generatedCode, "int64(9007199254740993)",
"9007199254740993 (2^53+1) was rounded; float64 precision loss in i64 array element")
// 2^53+1 negative
assert.Contains(t, generatedCode, "int64(-9007199254740993)",
"-9007199254740993 was rounded; float64 precision loss in i64 array element")
// max i64
assert.Contains(t, generatedCode, "int64(9223372036854775807)",
"max i64 was rounded; float64 precision loss in i64 array element")
// min i64
assert.Contains(t, generatedCode, "int64(-9223372036854775808)",
"min i64 was rounded; float64 precision loss in i64 array element")
})

t.Run("u32 array is not affected", func(t *testing.T) {
// u32 max = 4294967295 < 2^53, so float64 is fine
constants := []idl.IdlConst{
{
Name: "U32_ARRAY",
Ty: &idltype.Array{
Type: &idltype.U32{},
Size: &idltype.IdlArrayLenValue{Value: 2},
},
Value: "[4294967295, 0]",
},
}

idlData := &idl.Idl{Constants: constants}
gen := &Generator{idl: idlData, options: &GeneratorOptions{Package: "test"}}

outputFile, err := gen.gen_constants()
require.NoError(t, err)

generatedCode := outputFile.File.GoString()
assert.Contains(t, generatedCode, "uint32(0xffffffff)")
assert.Contains(t, generatedCode, "uint32(0x0)")
})
}

// TestGenConstantsRealWorldExamples 测试真实世界的例子
func TestGenConstantsRealWorldExamples(t *testing.T) {
t.Run("Solana program constants", func(t *testing.T) {
Expand Down