diff --git a/libevm/pseudo/type.go b/libevm/pseudo/type.go index 4cd1beec7a..8c453f4cb0 100644 --- a/libevm/pseudo/type.go +++ b/libevm/pseudo/type.go @@ -1,4 +1,15 @@ -// Package pseudo ... +// Package pseudo provides a bridge between generic and non-generic code via +// pseudo-types and pseudo-values. With careful usage, there is minimal +// reduction in type safety. +// +// Adding generic type parameters to anything (e.g. struct, function, etc) +// "pollutes" all code that uses the generic type. Refactoring all uses isn't +// always feasible, and a [Type] acts as an intermediate fix. Although their +// constructors are generic, they are not, and they are instead coupled with a +// generic [Value] that SHOULD be used for access. +// +// Packages typically SHOULD NOT expose a [Type] and SHOULD instead provide +// users with a type-safe [Value]. package pseudo import ( @@ -6,63 +17,101 @@ import ( "fmt" ) -// Zero ... -func Zero[T any]() (*Type, *Value[T]) { - var x T - return From[T](x) -} - -// From ... -func From[T any](x T) (*Type, *Value[T]) { - t := &Type{ - val: &concrete[T]{ - val: x, - }, - } - return t, NewValueUnsafe[T](t) -} - -// OnlyType ... -func OnlyType[T any](t *Type, _ *Value[T]) *Type { - return t -} - -// A Type ... +// A Type wraps a strongly-typed value without exposing information about its +// type. It can be used in lieu of a generic field / parameter. type Type struct { val value } -func (t *Type) Interface() any { return t.val.get() } - -// func (t *Type) Set(v any) error { return t.val.Set(v) } -// func (t *Type) MustSet(v any) { t.val.MustSet(v) } -func (t *Type) MarshalJSON() ([]byte, error) { return t.val.MarshalJSON() } -func (t *Type) UnmarshalJSON(b []byte) error { return t.val.UnmarshalJSON(b) } - -var ( - _ json.Marshaler = (*Type)(nil) - _ json.Unmarshaler = (*Type)(nil) -) - -func NewValueUnsafe[T any](t *Type) *Value[T] { - return &Value[T]{t: t} -} - -func NewValue[T any](t *Type) (*Value[T], error) { - var x T - if !t.val.canSetTo(x) { - return nil, fmt.Errorf("cannot create *Accessor[%T] with *Type carrying %T", x, t.val.get()) - } - return NewValueUnsafe[T](t), nil -} - +// A Value provides strongly-typed access to the payload carried by a [Type]. type Value[T any] struct { t *Type } -func (a *Value[T]) Get() T { return a.t.val.get().(T) } +// A Pseudo type couples a [Type] and a [Value]. If returned by a constructor +// from this package, both wrap the same payload. +type Pseudo[T any] struct { + Type *Type + Value *Value[T] +} + +// TypeAndValue is a convenience function for splitting the contents of `p`, +// typically at construction. +func (p *Pseudo[T]) TypeAndValue() (*Type, *Value[T]) { + return p.Type, p.Value +} + +// From returns a Pseudo[T] constructed from `v`. +func From[T any](v T) *Pseudo[T] { + t := &Type{ + val: &concrete[T]{ + val: v, + }, + } + return &Pseudo[T]{t, MustNewValue[T](t)} +} + +// Zero is equivalent to [From] called with the [zero value] of type `T`. Note +// that pointers, slices, maps, etc. will therefore be nil. +// +// [zero value]: https://go.dev/tour/basics/12 +func Zero[T any]() *Pseudo[T] { + var x T + return From[T](x) +} + +// Interface returns the wrapped value as an `any`, equivalent to +// [reflect.Value.Interface]. Prefer [Value.Get]. +func (t *Type) Interface() any { return t.val.get() } + +// NewValue constructs a [Value] from a [Type], first confirming that `t` wraps +// a payload of type `T`. +func NewValue[T any](t *Type) (*Value[T], error) { + var x T + if !t.val.canSetTo(x) { + return nil, fmt.Errorf("cannot create *Value[%T] with *Type carrying %T", x, t.val.get()) + } + return &Value[T]{t}, nil +} + +// MustNewValue is equivalent to [NewValue] except that it panics instead of +// returning an error. +func MustNewValue[T any](t *Type) *Value[T] { + v, err := NewValue[T](t) + if err != nil { + panic(err) + } + return v +} + +// Get returns the value. +func (a *Value[T]) Get() T { return a.t.val.get().(T) } + +// Set sets the value. func (a *Value[T]) Set(v T) { a.t.val.mustSet(v) } +// MarshalJSON implements the [json.Marshaler] interface. +func (t *Type) MarshalJSON() ([]byte, error) { return t.val.MarshalJSON() } + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (t *Type) UnmarshalJSON(b []byte) error { return t.val.UnmarshalJSON(b) } + +// MarshalJSON implements the [json.Marshaler] interface. +func (v *Value[T]) MarshalJSON() ([]byte, error) { return v.t.MarshalJSON() } + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (v *Value[T]) UnmarshalJSON(b []byte) error { return v.t.UnmarshalJSON(b) } + +var _ = []interface { + json.Marshaler + json.Unmarshaler +}{ + (*Type)(nil), + (*Value[struct{}])(nil), + (*concrete[struct{}])(nil), +} + +// A value is a non-generic wrapper around a [concrete] struct. type value interface { get() any canSetTo(any) bool @@ -84,11 +133,14 @@ func (c *concrete[T]) canSetTo(v any) bool { return ok } -type InvalidTypeError[T any] struct { +// An invalidTypeError is returned by [conrete.set] if the value is incompatible +// with its type. This should never leave this package and exists only to +// provide precise testing of unhappy paths. +type invalidTypeError[T any] struct { SetTo any } -func (e *InvalidTypeError[T]) Error() string { +func (e *invalidTypeError[T]) Error() string { var t T return fmt.Sprintf("cannot set %T to %T", t, e.SetTo) } @@ -96,7 +148,9 @@ func (e *InvalidTypeError[T]) Error() string { func (c *concrete[T]) set(v any) error { vv, ok := v.(T) if !ok { - return &InvalidTypeError[T]{SetTo: v} + // Other invariants in this implementation (aim to) guarantee that this + // will never happen. + return &invalidTypeError[T]{SetTo: v} } c.val = vv return nil diff --git a/libevm/pseudo/type_test.go b/libevm/pseudo/type_test.go index 964969500f..27ecf7e497 100644 --- a/libevm/pseudo/type_test.go +++ b/libevm/pseudo/type_test.go @@ -15,7 +15,7 @@ func TestType(t *testing.T) { testType( t, "From[uint](314159)", - func() (*Type, *Value[uint]) { + func() *Pseudo[uint] { return From[uint](314159) }, 314159, 0, struct{}{}, @@ -24,20 +24,20 @@ func TestType(t *testing.T) { testType(t, "nil pointer", Zero[*float64], (*float64)(nil), new(float64), 0) } -func testType[T any](t *testing.T, name string, ctor func() (*Type, *Value[T]), init T, setTo T, invalid any) { +func testType[T any](t *testing.T, name string, ctor func() *Pseudo[T], init T, setTo T, invalid any) { t.Run(name, func(t *testing.T) { - typ, val := ctor() + typ, val := ctor().TypeAndValue() assert.Equal(t, init, val.Get()) val.Set(setTo) assert.Equal(t, setTo, val.Get()) t.Run("set to invalid type", func(t *testing.T) { - wantErr := &InvalidTypeError[T]{SetTo: invalid} + wantErr := &invalidTypeError[T]{SetTo: invalid} assertError := func(t *testing.T, err any) { t.Helper() switch err := err.(type) { - case *InvalidTypeError[T]: + case *invalidTypeError[T]: assert.Equal(t, wantErr, err) default: t.Errorf("got error %v; want %v", err, wantErr) @@ -60,9 +60,20 @@ func testType[T any](t *testing.T, name string, ctor func() (*Type, *Value[T]), buf, err := json.Marshal(typ) require.NoError(t, err) - got, gotVal := Zero[T]() + got, gotVal := Zero[T]().TypeAndValue() require.NoError(t, json.Unmarshal(buf, &got)) assert.Equal(t, val.Get(), gotVal.Get()) }) }) } + +func ExamplePseudo_TypeAndValue() { + typ, val := From("hello").TypeAndValue() + + // But, if only one is needed: + typ = From("world").Type + val = From("this isn't coupled to the Type").Value + + _ = typ + _ = val +} diff --git a/params/config.libevm.go b/params/config.libevm.go index 23384b6de9..c212995d5c 100644 --- a/params/config.libevm.go +++ b/params/config.libevm.go @@ -56,12 +56,12 @@ type ExtraPayloadGetter[C any, R any] struct{} // FromChainConfig ... func (ExtraPayloadGetter[C, R]) FromChainConfig(c *ChainConfig) *C { - return pseudo.NewValueUnsafe[*C](c.extraPayload()).Get() + return pseudo.MustNewValue[*C](c.extraPayload()).Get() } // FromRules ... func (ExtraPayloadGetter[C, R]) FromRules(r *Rules) *R { - return pseudo.NewValueUnsafe[*R](r.extraPayload()).Get() + return pseudo.MustNewValue[*R](r.extraPayload()).Get() } func mustBeStruct[T any]() { @@ -160,19 +160,17 @@ func (r *Rules) extraPayload() *pseudo.Type { return r.extra } -func (Extras[C, R]) nilForChainConfig() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*C]()) } -func (Extras[C, R]) nilForRules() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*R]()) } +func (Extras[C, R]) nilForChainConfig() *pseudo.Type { return pseudo.Zero[*C]().Type } +func (Extras[C, R]) nilForRules() *pseudo.Type { return pseudo.Zero[*R]().Type } func (*Extras[C, R]) newForChainConfig() *pseudo.Type { var x C - return pseudo.OnlyType(pseudo.From(&x)) + return pseudo.From(&x).Type } func (e *Extras[C, R]) newForRules(c *ChainConfig, r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type { if e.NewForRules == nil { return e.nilForRules() } - return pseudo.OnlyType( - pseudo.From(e.NewForRules(c, r, c.extra.Interface().(*C), blockNum, isMerge, timestamp)), - ) + return pseudo.From(e.NewForRules(c, r, c.extra.Interface().(*C), blockNum, isMerge, timestamp)).Type } diff --git a/params/config.libevm_test.go b/params/config.libevm_test.go index cdbdf3cce6..bc94075038 100644 --- a/params/config.libevm_test.go +++ b/params/config.libevm_test.go @@ -56,9 +56,9 @@ func TestRegisterExtras(t *testing.T) { }, }) }, - ccExtra: pseudo.OnlyType(pseudo.From(&ccExtraA{ + ccExtra: pseudo.From(&ccExtraA{ A: "hello", - })), + }).Type, wantRulesExtra: &rulesExtraA{ A: "hello", }, @@ -68,9 +68,9 @@ func TestRegisterExtras(t *testing.T) { register: func() { RegisterExtras(Extras[ccExtraB, rulesExtraB]{}) }, - ccExtra: pseudo.OnlyType(pseudo.From(&ccExtraB{ + ccExtra: pseudo.From(&ccExtraB{ B: "world", - })), + }).Type, wantRulesExtra: (*rulesExtraB)(nil), }, { @@ -78,9 +78,9 @@ func TestRegisterExtras(t *testing.T) { register: func() { RegisterExtras(Extras[rawJSON, struct{}]{}) }, - ccExtra: pseudo.OnlyType(pseudo.From(&rawJSON{ + ccExtra: pseudo.From(&rawJSON{ RawMessage: []byte(`"hello, world"`), - })), + }).Type, wantRulesExtra: (*struct{})(nil), }, }