From aca27394e3d0b1875be25ed527c4f6efaaba9702 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Tue, 4 Aug 2020 11:46:56 +0200 Subject: [PATCH 1/7] First pass on generation for kinded unions. So far, the generation itself runs, but the result will not yet compile. I just want a checkpoint here. (Most of this was written a few days ago already.) --- schema/gen/go/genUnionReprKinded.go | 436 ++++++++++++++++++++++++++++ schema/gen/go/generate.go | 2 + schema/tmpBuilders.go | 5 + schema/type.go | 16 + schema/typeMethods.go | 45 +++ 5 files changed, 504 insertions(+) create mode 100644 schema/gen/go/genUnionReprKinded.go diff --git a/schema/gen/go/genUnionReprKinded.go b/schema/gen/go/genUnionReprKinded.go new file mode 100644 index 00000000..b7afbc06 --- /dev/null +++ b/schema/gen/go/genUnionReprKinded.go @@ -0,0 +1,436 @@ +package gengo + +import ( + "io" + + "github.com/ipld/go-ipld-prime/schema" + "github.com/ipld/go-ipld-prime/schema/gen/go/mixins" +) + +var _ TypeGenerator = &unionReprKindedGenerator{} + +// Kinded union representations are quite wild: their behavior varies almost completely per inhabitant, +// and their implementation is generally delegating directly to something else, +// rather than having an intermediate node (like most unions do, and like the type-level view of this same value will). +// +// This also means any error values can be a little weird: +// sometimes they'll have the union's type name, but sometimes they'll have the inhabitant's type name instead; +// this depends on whether the error is an ErrWrongKind that was found while checking the method for appropriateness on the union's inhabitant +// versus if the error came from the union inhabitant itself after delegation occured. + +func NewUnionReprKindedGenerator(pkgName string, typ *schema.TypeUnion, adjCfg *AdjunctCfg) TypeGenerator { + return unionReprKindedGenerator{ + unionGenerator{ + adjCfg, + mixins.MapTraits{ + pkgName, + string(typ.Name()), + adjCfg.TypeSymbol(typ), + }, + pkgName, + typ, + }, + } +} + +type unionReprKindedGenerator struct { + unionGenerator +} + +func (g unionReprKindedGenerator) GetRepresentationNodeGen() NodeGenerator { + return unionReprKindedReprGenerator{ + g.AdjCfg, + g.PkgName, + g.Type, + } +} + +type unionReprKindedReprGenerator struct { + // Note that there's no MapTraits (or any other FooTraits) mixin in this one! + // This is no accident: *None* of them apply! + + AdjCfg *AdjunctCfg + PkgName string + Type *schema.TypeUnion +} + +func (unionReprKindedReprGenerator) IsRepr() bool { return true } // hint used in some generalized templates. + +func (g unionReprKindedReprGenerator) EmitNodeType(w io.Writer) { + // The type is structurally the same, but will have a different set of methods. + doTemplate(` + type _{{ .Type | TypeSymbol }}__Repr _{{ .Type | TypeSymbol }} + `, w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeTypeAssertions(w io.Writer) { + doTemplate(` + var _ ipld.Node = &_{{ .Type | TypeSymbol }}__Repr{} + `, w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodReprKind(w io.Writer) { + // FIXME wow +} + +// A bunch of these methods could be improved by doing a gen-time switch for whether any of the possible members are the relevant kind at all; +// currently in the cases where there's no relevant members, we generate switch blocks that are empty except for their default... +// which works, but is arguably a little strange. +// I haven't checked if this dummy switch has any actual performance implications: +// I haven't tested if this produces unconditional assembly, +// nor if it successfully removes the access of the tag, +// though one might imagine a sufficiently clever compiler ought to do both of those things. +// Regardless, the gsloc is reducable. (Slightly. There are also bigger gains to be made elsewhere, I'm sure.) + +func kindedUnionNodeMethodTemplateMunge( + methodSig string, condClause string, retClause string, +) string { + // We really could just... call the methods directly (and elide the switch entirely all the time), in the case of the "interface" implementation strategy. + // We don't, though, because that would deprive us of getting the union type's name in the wrong-kind errors... + // and in addition to that being sadface in general, it would be downright unacceptable if that behavior varied based on implementation strategy. + return ` + func (n *_{{ .Type | TypeSymbol }}__Repr) ` + methodSig + ` { + {{- if (eq (.AdjCfg.UnionMemlayout .Type) "embedAll") }} + switch n.tag { + {{- range $i, $member := .Type.Members }} + ` + condClause + ` + case {{ add $i 1 }}: + return n.x{{ add $i 1 }}.Representation()` + retClause + ` + {{- end}} + {{- end}} + {{- else if (eq (.AdjCfg.UnionMemlayout .Type) "interface") }} + switch n2 := n.x.(type) { + {{- range $i, $member := .Type.Members }} + ` + condClause + ` + case {{ $member | TypeSymbol }}: + return n2.Representation()` + retClause + ` + {{- end}} + {{- end}} + {{- end}} + default: + return nil, ipld.ErrWrongKind{doozy} // ... is this a good example of where ErrFoo.TypeName should indeed be freetext, so it can say "ThisUnion.Repr(inhabitant not FooMap)" ...? + } + } + ` + // TODO damnit I need the method name again for error messages. ... maybe make the error be a clause, hm. +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByString(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupByString(key string) (ipld.Node, error)`, + `{{- if eq $member.RepresentationBehavior.String "map" }}`, + `.LookupByString(key)`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByIndex(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupByIndex(idx int) (ipld.Node, error)`, + `{{- if eq $member.RepresentationBehavior.String "list" }}`, + `.LookupByIndex(idx)`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByNode(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupByNode(key ipld.Node) (ipld.Node, error)`, + `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, + `.LookupByNode(key)`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodLookupBySegment(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupBySegment(seg ipld.PathSegment) (ipld.Node, error)`, + `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, + `.LookupBySegment(seg)`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodMapIterator(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `MapIterator() ipld.MapIterator`, + `{{- if eq $member.RepresentationBehavior.String "map" }}`, + `.MapIterator()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodListIterator(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `ListIterator() ipld.ListIterator`, + `{{- if eq $member.RepresentationBehavior.String "list" }}`, + `.ListIterator()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodLength(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `Length() int`, + `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, + `.Length()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodIsAbsent(w io.Writer) { + doTemplate(` + func (n *_{{ .Type | TypeSymbol }}__Repr) IsAbsent() bool { + return false + } + `, w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodIsNull(w io.Writer) { + doTemplate(` + func (n *_{{ .Type | TypeSymbol }}__Repr) IsNull() bool { + return false + } + `, w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodAsBool(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsBool() (bool, error)`, + `{{- if eq $member.RepresentationBehavior.String "bool" }}`, + `.AsBool()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodAsInt(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsInt() (int, error)`, + `{{- if eq $member.RepresentationBehavior.String "int" }}`, + `.AsInt()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodAsFloat(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsFloat() (float64, error)`, + `{{- if eq $member.RepresentationBehavior.String "float" }}`, + `.AsFloat()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodAsString(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsString() (string, error)`, + `{{- if eq $member.RepresentationBehavior.String "string" }}`, + `.AsString()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodAsBytes(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsBytes() ([]byte, error)`, + `{{- if eq $member.RepresentationBehavior.String "bytes" }}`, + `.AsBytes()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodAsLink(w io.Writer) { + doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsLink() (ipld.Link, error)`, + `{{- if eq $member.RepresentationBehavior.String "link" }}`, + `.AsLink()`, + ), w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodeMethodPrototype(w io.Writer) { + emitNodeMethodPrototype_typical(w, g.AdjCfg, g) +} + +func (g unionReprKindedReprGenerator) EmitNodePrototypeType(w io.Writer) { + emitNodePrototypeType_typical(w, g.AdjCfg, g) +} + +// --- NodeBuilder and NodeAssembler ---> + +func (g unionReprKindedReprGenerator) GetNodeBuilderGenerator() NodeBuilderGenerator { + return unionReprKindedReprBuilderGenerator{ + g.AdjCfg, + g.PkgName, + g.Type, + } +} + +type unionReprKindedReprBuilderGenerator struct { + AdjCfg *AdjunctCfg + PkgName string + Type *schema.TypeUnion +} + +func (unionReprKindedReprBuilderGenerator) IsRepr() bool { return true } // hint used in some generalized templates. + +func (g unionReprKindedReprBuilderGenerator) EmitNodeBuilderType(w io.Writer) { + emitEmitNodeBuilderType_typical(w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeBuilderMethods(w io.Writer) { + emitNodeBuilderMethods_typical(w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerType(w io.Writer) { + // Much of this is familiar: the 'w', the 'm' are all as usual. + // Some things may look a little odd here compared to all other assemblers: + // we're kinda halfway between what's conventionally seen for a scalar and what's conventionally seen for a recursive. + // There's no 'maState' or 'laState'-typed fields (which feels like a scalar) because even if we end up acting like a map or list, that state is in the relevant child assembler. + // We don't even have a 'cm' field, because we can get away with something really funky: we can just copy our own 'm' _pointer_ into children; our doneness and their doneness is the same. + // We never have to worry about maybeism of our children; the nullable and optional modifiers aren't possible on union members. + // (We *do* still have to consider null values though, as null is still a kind, and thus can be routed to one of our members!) + // 'ca' is as it is in the type-level assembler: technically, not super necessary, except that it allows minimizing the amount of work that resetting needs to do. + doTemplate(` + type _{{ .Type | TypeSymbol }}__ReprAssembler struct { + w *_{{ .Type | TypeSymbol }} + m *schema.Maybe + + {{- range $i, $member := .Type.Members }} + ca{{ add $i 1 }} {{ if (eq (dot.AdjCfg.UnionMemlayout dot.Type) "interface") }}*{{end}}_{{ $member | TypeSymbol }}__ReprAssembler + {{end -}} + ca uint + } + `, w, g.AdjCfg, g) + doTemplate(` + func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) reset() { + na.state = maState_initial + switch na.ca { + case 0: + return + {{- range $i, $member := .Type.Members }} + case {{ add $i 1 }}: + na.ca{{ add $i 1 }}.reset() + {{end -}} + default: + panic("unreachable") + } + } + `, w, g.AdjCfg, g) +} + +func kindedUnionNodeAssemblerMethodTemplateMunge( + methodSig string, condClause string, retClause string, +) string { + // The value pointed to by `na.m` isn't modified here, because we're sharing it with the child, who should do so. + // This also means that value gets checked twice -- once by us, because we need to halt if we've already been used -- + // and also a second time by the child when we delegate to it, which, unbeknownst to it, is irrelevant. + // I don't see a good way to remedy this shy of making more granular (unexported!) methods. (Might be worth it.) + // This probably also isn't the same for all of the assembler methods: the methods we delegate to aren't doing as many check branches when they're for scalars, + // because they expected to be used in contexts where many values of the 'm' enum aren't reachable -- an expectation we've suddenly subverted with this path! + return ` + func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) ` + methodSig + ` { + switch *na.m { + case schema.Maybe_Value, schema.Maybe_Null: + panic("invalid state: cannot assign into assembler that's already finished") + case midvalue: + panic("invalid state: cannot assign into assembler that's already working on a larger structure!") + } + {{- range $i, $member := .Type.Members }} + ` + condClause + ` + {{- if dot.Type | MaybeUsesPtr }} + if na.w == nil { + na.w = &_{{ dot.Type | TypeSymbol }}{} + } + {{- end}} + na.ca = {{ add $i 1 }} + {{- if (eq (dot.AdjCfg.UnionMemlayout dot.Type) "embedAll") }} + na.w.tag = {{ add $i 1 }} + na.ca{{ add $i 1 }}.w = &na.w.x{{ add $i 1 }} + na.ca{{ add $i 1 }}.m = na.m + return na.ca{{ add $i 1 }}.BeginMap(sizeHint) + {{- else if (eq (dot.AdjCfg.UnionMemlayout dot.Type) "interface") }} + x := &_{{ $member | TypeSymbol }}{} + na.w.x = x + if na.ca{{ add $i 1 }} == nil { + na.ca{{ add $i 1 }} = &_{{ $member | TypeSymbol }}__ReprAssembler{} + } + na.ca{{ add $i 1 }}.w = x + na.ca{{ add $i 1 }}.m = na.m + return na.ca{{ add $i 1 }}` + retClause + ` + {{- end}} + {{- end}} + {{- end}} + // TODO i think you finally Need a method for if-no-members-match-this-kind for the default rejection to compile this time. + return nil, ipld.ErrWrongKind{doozy} + } + ` +} + +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodBeginMap(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `BeginMap(sizeHint int) (ipld.MapAssembler, error)`, + `{{- if eq $member.RepresentationBehavior.String "map" }}`, + `.BeginMap(sizeHint)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodBeginList(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `BeginList(sizeHint int) (ipld.ListAssembler, error)`, + `{{- if eq $member.RepresentationBehavior.String "list" }}`, + `.BeginList(sizeHint)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignNull(w io.Writer) { + // TODO: I think this may need some special handling to account for if our union is itself used in a nullable circumstance; that should overrule this behavior. + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignNull() error `, + `{{- if eq $member.RepresentationBehavior.String "null" }}`, + `.AssignNull()`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignBool(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignBool(v bool) error `, + `{{- if eq $member.RepresentationBehavior.String "bool" }}`, + `.AssignBool(v)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignInt(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignInt(v int) error `, + `{{- if eq $member.RepresentationBehavior.String "int" }}`, + `.AssignInt(v)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignFloat(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignFloat(v float64) error `, + `{{- if eq $member.RepresentationBehavior.String "float" }}`, + `.AssignFloat(v)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignString(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignString(v string) error `, + `{{- if eq $member.RepresentationBehavior.String "string" }}`, + `.AssignString(v)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignBytes(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignBytes(v []byte) error `, + `{{- if eq $member.RepresentationBehavior.String "bytes" }}`, + `.AssignBytes(v)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignLink(w io.Writer) { + doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignLink(v ipld.Link) error `, + `{{- if eq $member.RepresentationBehavior.String "link" }}`, + `.AssignLink(v)`, + ), w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignNode(w io.Writer) { + // TODO this is too wild for me at the moment, come back to it shortly + // it's basically got some of the body of kindedUnionNodeAssemblerMethodTemplateMunge, but repeated many more times. + // it also needs to handle nulls gingerly. + // and also handle pumping the full copy in the case of lists or maps. + + // this is gonna have a fun ErrWrongKind value too -- we might actually have to make a non-static set of acceptable kinds :D that's a first. +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodPrototype(w io.Writer) { + doTemplate(` + func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) Prototype() ipld.NodePrototype { + return _{{ .Type | TypeSymbol }}__ReprPrototype{} + } + `, w, g.AdjCfg, g) +} +func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerOtherBits(w io.Writer) { + // somewhat shockingly: nothing. +} diff --git a/schema/gen/go/generate.go b/schema/gen/go/generate.go index ead1b0e7..75e1454b 100644 --- a/schema/gen/go/generate.go +++ b/schema/gen/go/generate.go @@ -50,6 +50,8 @@ func Generate(pth string, pkgName string, ts schema.TypeSystem, adjCfg *AdjunctC switch t2.RepresentationStrategy().(type) { case schema.UnionRepresentation_Keyed: EmitEntireType(NewUnionReprKeyedGenerator(pkgName, t2, adjCfg), f) + case schema.UnionRepresentation_Kinded: + EmitEntireType(NewUnionReprKindedGenerator(pkgName, t2, adjCfg), f) default: panic("unrecognized union representation strategy") } diff --git a/schema/tmpBuilders.go b/schema/tmpBuilders.go index 98caa6d1..3b93599a 100644 --- a/schema/tmpBuilders.go +++ b/schema/tmpBuilders.go @@ -2,6 +2,8 @@ package schema import ( "fmt" + + "github.com/ipld/go-ipld-prime" ) // Everything in this file is __a temporary hack__ and will be __removed__. @@ -102,6 +104,9 @@ func SpawnUnion(name TypeName, members []TypeName, repr UnionRepresentation) *Ty func SpawnUnionRepresentationKeyed(table map[string]TypeName) UnionRepresentation_Keyed { return UnionRepresentation_Keyed{table} } +func SpawnUnionRepresentationKinded(table map[ipld.ReprKind]TypeName) UnionRepresentation_Kinded { + return UnionRepresentation_Kinded{table} +} // The methods relating to TypeSystem are also mutation-heavy and placeholdery. diff --git a/schema/type.go b/schema/type.go index cf83a951..2d47163d 100644 --- a/schema/type.go +++ b/schema/type.go @@ -73,6 +73,22 @@ type Type interface { // can vary in representation kind based on their value (specifically, // kinded-representation unions have this property). Kind() Kind + + // RepresentationBehavior returns a description of how the representation + // of this type will behave in terms of the IPLD Data Model. + // This property varies based on the representation strategy of a type. + // + // In one case, the representation behavior cannot be known statically, + // and varies based on the data: kinded unions have this trait. + // + // This property is used by kinded unions, which require that their members + // all have distinct representation behavior. + // (It follows that a kinded union cannot have another kinded union as a member.) + // + // You may also be interested in a related property that might have been called "TypeBehavior". + // However, this method doesn't exist, because it's a deterministic property of `Kind()`! + // You can use `Kind.ActsLike()` to get type-level behavioral information. + RepresentationBehavior() ipld.ReprKind } var ( diff --git a/schema/typeMethods.go b/schema/typeMethods.go index 859bfec3..87440633 100644 --- a/schema/typeMethods.go +++ b/schema/typeMethods.go @@ -1,5 +1,9 @@ package schema +import ( + ipld "github.com/ipld/go-ipld-prime" +) + /* cookie-cutter standard interface stuff */ func (t *typeBase) _Type(ts *TypeSystem) { @@ -20,6 +24,47 @@ func (TypeUnion) Kind() Kind { return Kind_Union } func (TypeStruct) Kind() Kind { return Kind_Struct } func (TypeEnum) Kind() Kind { return Kind_Enum } +func (TypeBool) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_Bool } +func (TypeString) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_String } +func (TypeBytes) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_Bytes } +func (TypeInt) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_Int } +func (TypeFloat) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_Float } +func (TypeMap) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_Map } +func (TypeList) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_List } +func (TypeLink) RepresentationBehavior() ipld.ReprKind { return ipld.ReprKind_Link } +func (t TypeUnion) RepresentationBehavior() ipld.ReprKind { + switch t.representation.(type) { + case UnionRepresentation_Keyed: + return ipld.ReprKind_Map + case UnionRepresentation_Kinded: + return ipld.ReprKind_Invalid // you can't know with this one, until you see the value (and thus can its inhabitant's behavior)! + case UnionRepresentation_Envelope: + return ipld.ReprKind_Map + case UnionRepresentation_Inline: + return ipld.ReprKind_Map + default: + panic("unreachable") + } +} +func (t TypeStruct) RepresentationBehavior() ipld.ReprKind { + switch t.representation.(type) { + case StructRepresentation_Map: + return ipld.ReprKind_Map + case StructRepresentation_Tuple: + return ipld.ReprKind_List + case StructRepresentation_StringPairs: + return ipld.ReprKind_String + case StructRepresentation_Stringjoin: + return ipld.ReprKind_String + default: + panic("unreachable") + } +} +func (t TypeEnum) RepresentationBehavior() ipld.ReprKind { + // TODO: this should have a representation strategy switch too; sometimes that will indicate int representation behavior. + return ipld.ReprKind_String +} + /* interesting methods per Type type */ // beware: many of these methods will change when we successfully bootstrap self-hosting. From 8f18f5bafd7308dd84eb8497d48ea4ffcde1b628 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Tue, 4 Aug 2020 11:48:28 +0200 Subject: [PATCH 2/7] Working notes on how to DRY in this code. --- schema/gen/go/HACKME_dry.md | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 schema/gen/go/HACKME_dry.md diff --git a/schema/gen/go/HACKME_dry.md b/schema/gen/go/HACKME_dry.md new file mode 100644 index 00000000..eeb08d33 --- /dev/null +++ b/schema/gen/go/HACKME_dry.md @@ -0,0 +1,82 @@ +HACKME: "don't repeat yourself": how-to (and, limits of that goal) +================================================================== + +Which kind of extraction applies? +--------------------------------- + +Things vary in how identical they are. + +- **Textually identical**: Some code is textually identical between different types, + varying only in the most simple and obvious details, like the actual type name it's attached to. + - These cases can often be extracted on the generation side... + - We tend to put them in `genparts{*}.go` files. + - But the output is still pretty duplicacious. + +- **Textually identical minus simple variations**: Some code is textually *nearly* identical, + but varies in relatively minor ways (such as whether or not the "Repr" is part of munges, and "Representation()" calls are made, etc). + - These cases can often be extracted on the generation side... + - We tend to put them in `genparts{*}.go` files. + - There's just a bit more `{{ various templating }}` injected in them, compared to other textually identical templates. + - But the output is still pretty duplicacious. + +- **Linkologically identical**: When code is not _only_ textually identical, + but also refers to identical types. + - These cases can be extracted on the generation side... + - but it may be questionable to do so: if its terse enough in the output, there's that much less incentive to make a template-side shorthand for it. + - The output in this case can actually be deduplicated! + - It's possible we haven't bothered yet. **That doesn't mean it's not worth it**; we probably just haven't had time yet. PRs welcome. + - How? + - functions? This is the most likely to apply. + - embedded types? We haven't seen many cases where this can help, yet (unfortunately?). + - shared constants? + - It's not always easy to do this. + - We usually put something in the "minima" file. + - We don't currently have a way to toggle whether whole features or shared constants are emitted in the minima file. Todo? + - This requires keeping state that records what's necessary as we go, so that we can do them all together at the end. + - May also require varying the imports at the top of the minima file. (But: by doing it only here, we can avoid that complexity in every other file.) + - **This is actually pretty rare**. _Things that are textually identical are not necessarily linkologically identical_. + - One can generally turn things that are textually identical into linkologically identical by injecting an interface into the types... + - ... but this isn't always a *good* idea: + - if this would cause more allocations? Yeah, very no. + - even if this can be done without a heap allocation, it probably means inlining and other optimizations will become impossible for the compiler to perform, and often, we're not okay with the performance implications of that either. + +- **Identical if it wasn't for debugability**: In some cases, code varies only by some small constants... + and really, those constants could be removed entirely. If... we didn't care about debugging. Which we do. + - This is really the same as "textually identical minus simple variations", but worth talking about briefly just because of the user story around it. + - A bunch of the error-thunking methods on Node and NodeAssemblers exemplify this. + - It's really annoying that we can't remove this boilerplate entirely from the generated code. + - It's also basically impossible, because we *want* information that varies per type in those error messages. + + +What mechanism of extraction should be used? +-------------------------------------------- + +- (for gen-side dry) gen-side functions + - this is most of what we've done so far +- (for gen-side dry) sub-templates + - we currently don't really use this at all +- (for gen-side dry) template concatenation + - we currently don't really use this at all either +- (for output-side dry) output side functions + - some of this: see "minima" file. +- (for output-side dry) output side embeds + - we currently don't really use this at all (it hasn't really turned out applicable in any cases yet). + + +Don't overdo it +--------------- + +I'd rather have longer templates than harder-to-read and harder-to-maintain templates. + +There's a balance to this and it's tricky to pick out. + +A good heuristic to consider might be: are we extracting this thing because we can? +Or because if we made changes to this thing in the future, we'd expect to need to make that change in every single place we've extracted it from, +which therefore makes the extraction a net win for maintainability? +If it's the latter: then yes, extract it. +If it's not clear: maybe let it be. + +(It may be the case that the preferable balance for DRYing changes over time as we keep maintaining things. +We'll see; but it's certainly the case that the first draft of this package has favored length heavily. +There was a lot of "it's not clear" when the maintainability heuristic was applied during the first writing of this; +that may change! If so, that's great.) From 792f8937c9a7bbc6f938904582fc50d612f4ba13 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Tue, 4 Aug 2020 13:01:33 +0200 Subject: [PATCH 3/7] Generate reasonable ReprKind method for kinded unions. --- schema/gen/go/genUnionReprKinded.go | 21 ++++++++++++++++++++- schema/gen/go/templateUtil.go | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/schema/gen/go/genUnionReprKinded.go b/schema/gen/go/genUnionReprKinded.go index b7afbc06..1984d282 100644 --- a/schema/gen/go/genUnionReprKinded.go +++ b/schema/gen/go/genUnionReprKinded.go @@ -70,7 +70,26 @@ func (g unionReprKindedReprGenerator) EmitNodeTypeAssertions(w io.Writer) { } func (g unionReprKindedReprGenerator) EmitNodeMethodReprKind(w io.Writer) { - // FIXME wow + doTemplate(` + func (n *_{{ .Type | TypeSymbol }}__Repr) ReprKind() ipld.ReprKind { + {{- if (eq (.AdjCfg.UnionMemlayout .Type) "embedAll") }} + switch n.tag { + {{- range $i, $member := .Type.Members }} + case {{ add $i 1 }}: + return {{ $member.RepresentationBehavior | KindSymbol }} + {{- end}} + {{- else if (eq (.AdjCfg.UnionMemlayout .Type) "interface") }} + switch n2 := n.x.(type) { + {{- range $i, $member := .Type.Members }} + case {{ $member | TypeSymbol }}: + return {{ $member.RepresentationBehavior | KindSymbol }} + {{- end}} + {{- end}} + default: + panic("unreachable") + } + } + `, w, g.AdjCfg, g) } // A bunch of these methods could be improved by doing a gen-time switch for whether any of the possible members are the relevant kind at all; diff --git a/schema/gen/go/templateUtil.go b/schema/gen/go/templateUtil.go index 9a49c2dc..adefd91e 100644 --- a/schema/gen/go/templateUtil.go +++ b/schema/gen/go/templateUtil.go @@ -54,6 +54,30 @@ func doTemplate(tmplstr string, w io.Writer, adjCfg *AdjunctCfg, data interface{ panic("invalid enumeration value!") } }, + "KindSymbol": func(k ipld.ReprKind) string { + switch k { + case ipld.ReprKind_Map: + return "ipld.ReprKind_Map" + case ipld.ReprKind_List: + return "ipld.ReprKind_List" + case ipld.ReprKind_Null: + return "ipld.ReprKind_Null" + case ipld.ReprKind_Bool: + return "ipld.ReprKind_Bool" + case ipld.ReprKind_Int: + return "ipld.ReprKind_Int" + case ipld.ReprKind_Float: + return "ipld.ReprKind_Float" + case ipld.ReprKind_String: + return "ipld.ReprKind_String" + case ipld.ReprKind_Bytes: + return "ipld.ReprKind_Bytes" + case ipld.ReprKind_Link: + return "ipld.ReprKind_Link" + default: + panic("invalid enumeration value!") + } + }, "add": func(a, b int) int { return a + b }, "title": func(s string) string { return strings.Title(s) }, }). From d2f22711c1fe05e701a97219076cd747d451d1ee Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Tue, 4 Aug 2020 13:02:45 +0200 Subject: [PATCH 4/7] Correctly generate all rejection branches in kinded union node accessors. This template munging business got nastier as time proceded. I think we could drastically reduce the amount of redundancy in the arugments to `kindedUnionNodeMethodTemplateMunge` if we re-did that function with a more structural understanding of what's going on, and made some basic understanding of what zero values are per golang type, etc. That can be future work, though. Today, I want working kinded unions, and I just want them done enough to use; kludge tolerance high. --- schema/gen/go/HACKME_dry.md | 2 +- schema/gen/go/genUnionReprKinded.go | 70 +++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/schema/gen/go/HACKME_dry.md b/schema/gen/go/HACKME_dry.md index eeb08d33..cbcce801 100644 --- a/schema/gen/go/HACKME_dry.md +++ b/schema/gen/go/HACKME_dry.md @@ -56,7 +56,7 @@ What mechanism of extraction should be used? - (for gen-side dry) sub-templates - we currently don't really use this at all - (for gen-side dry) template concatenation - - we currently don't really use this at all either + - some of this: kinded union representations do this - (for output-side dry) output side functions - some of this: see "minima" file. - (for output-side dry) output side embeds diff --git a/schema/gen/go/genUnionReprKinded.go b/schema/gen/go/genUnionReprKinded.go index 1984d282..532748aa 100644 --- a/schema/gen/go/genUnionReprKinded.go +++ b/schema/gen/go/genUnionReprKinded.go @@ -102,11 +102,24 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodReprKind(w io.Writer) { // Regardless, the gsloc is reducable. (Slightly. There are also bigger gains to be made elsewhere, I'm sure.) func kindedUnionNodeMethodTemplateMunge( - methodSig string, condClause string, retClause string, + methodName string, // for error messages + methodSig string, // output literally + condClause string, // template condition for the member this should match on + retClause string, // clause returning the thing (how to delegate methodsig, generally) + appropriateKind string, // for error messages + nopeSentinel string, // for error return paths; generally the zero value for the first return type. + nopeSentinelOnly bool, // true if this method has no error return, just the sentinel. ) string { // We really could just... call the methods directly (and elide the switch entirely all the time), in the case of the "interface" implementation strategy. // We don't, though, because that would deprive us of getting the union type's name in the wrong-kind errors... // and in addition to that being sadface in general, it would be downright unacceptable if that behavior varied based on implementation strategy. + // + // This error text doesn't tell us what the member kind is. This might read weirdly. + // It's possible we could try to cram a description of the inhabitant into the "TypeName" since it's stringy; but unclear if that's a good idea either. + errorClause := `return ` + nopeSentinel + if !nopeSentinelOnly { + errorClause += `, ipld.ErrWrongKind{TypeName: "{{ .PkgName }}.{{ .Type.Name }}.Repr", MethodName: "` + methodName + `", AppropriateKind: ` + appropriateKind + `, ActualKind: n.ReprKind()}` + } return ` func (n *_{{ .Type | TypeSymbol }}__Repr) ` + methodSig + ` { {{- if (eq (.AdjCfg.UnionMemlayout .Type) "embedAll") }} @@ -127,66 +140,93 @@ func kindedUnionNodeMethodTemplateMunge( {{- end}} {{- end}} default: - return nil, ipld.ErrWrongKind{doozy} // ... is this a good example of where ErrFoo.TypeName should indeed be freetext, so it can say "ThisUnion.Repr(inhabitant not FooMap)" ...? + ` + errorClause + ` } } ` - // TODO damnit I need the method name again for error messages. ... maybe make the error be a clause, hm. } func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByString(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupByString`, `LookupByString(key string) (ipld.Node, error)`, `{{- if eq $member.RepresentationBehavior.String "map" }}`, `.LookupByString(key)`, + `ipld.ReprKindSet_JustMap`, + `nil`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByIndex(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupByIndex`, `LookupByIndex(idx int) (ipld.Node, error)`, `{{- if eq $member.RepresentationBehavior.String "list" }}`, `.LookupByIndex(idx)`, + `ipld.ReprKindSet_JustList`, + `nil`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByNode(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupByNode`, `LookupByNode(key ipld.Node) (ipld.Node, error)`, `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, `.LookupByNode(key)`, + `ipld.ReprKindSet_Recursive`, + `nil`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodLookupBySegment(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `LookupBySegment`, `LookupBySegment(seg ipld.PathSegment) (ipld.Node, error)`, `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, `.LookupBySegment(seg)`, + `ipld.ReprKindSet_Recursive`, + `nil`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodMapIterator(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `MapIterator`, `MapIterator() ipld.MapIterator`, `{{- if eq $member.RepresentationBehavior.String "map" }}`, `.MapIterator()`, + `ipld.ReprKindSet_JustMap`, + `nil`, + true, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodListIterator(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `ListIterator`, `ListIterator() ipld.ListIterator`, `{{- if eq $member.RepresentationBehavior.String "list" }}`, `.ListIterator()`, + `ipld.ReprKindSet_JustList`, + `nil`, + true, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodLength(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `Length`, `Length() int`, `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, `.Length()`, + `ipld.ReprKindSet_Recursive`, + `-1`, + true, ), w, g.AdjCfg, g) } @@ -208,49 +248,73 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodIsNull(w io.Writer) { func (g unionReprKindedReprGenerator) EmitNodeMethodAsBool(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsBool`, `AsBool() (bool, error)`, `{{- if eq $member.RepresentationBehavior.String "bool" }}`, `.AsBool()`, + `ipld.ReprKindSet_JustBool`, + `false`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodAsInt(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsInt`, `AsInt() (int, error)`, `{{- if eq $member.RepresentationBehavior.String "int" }}`, `.AsInt()`, + `ipld.ReprKindSet_JustInt`, + `0`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodAsFloat(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsFloat`, `AsFloat() (float64, error)`, `{{- if eq $member.RepresentationBehavior.String "float" }}`, `.AsFloat()`, + `ipld.ReprKindSet_JustFloat`, + `0`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodAsString(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsString`, `AsString() (string, error)`, `{{- if eq $member.RepresentationBehavior.String "string" }}`, `.AsString()`, + `ipld.ReprKindSet_JustString`, + `""`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodAsBytes(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsBytes`, `AsBytes() ([]byte, error)`, `{{- if eq $member.RepresentationBehavior.String "bytes" }}`, `.AsBytes()`, + `ipld.ReprKindSet_JustBytes`, + `nil`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprGenerator) EmitNodeMethodAsLink(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( + `AsLink`, `AsLink() (ipld.Link, error)`, `{{- if eq $member.RepresentationBehavior.String "link" }}`, `.AsLink()`, + `ipld.ReprKindSet_JustLink`, + `nil`, + false, ), w, g.AdjCfg, g) } From fcbb4d80fb77c605328b9ea4c430cf8b64244c7e Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Tue, 4 Aug 2020 16:12:58 +0200 Subject: [PATCH 5/7] generation error path corrections for assemblers for kinded unions --- schema/gen/go/genUnionReprKinded.go | 42 ++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/schema/gen/go/genUnionReprKinded.go b/schema/gen/go/genUnionReprKinded.go index 532748aa..f3e6651a 100644 --- a/schema/gen/go/genUnionReprKinded.go +++ b/schema/gen/go/genUnionReprKinded.go @@ -366,20 +366,19 @@ func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerType(w io.Writer) {{- range $i, $member := .Type.Members }} ca{{ add $i 1 }} {{ if (eq (dot.AdjCfg.UnionMemlayout dot.Type) "interface") }}*{{end}}_{{ $member | TypeSymbol }}__ReprAssembler - {{end -}} + {{- end}} ca uint } `, w, g.AdjCfg, g) doTemplate(` func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) reset() { - na.state = maState_initial switch na.ca { case 0: return {{- range $i, $member := .Type.Members }} case {{ add $i 1 }}: na.ca{{ add $i 1 }}.reset() - {{end -}} + {{- end}} default: panic("unreachable") } @@ -388,7 +387,11 @@ func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerType(w io.Writer) } func kindedUnionNodeAssemblerMethodTemplateMunge( - methodSig string, condClause string, retClause string, + methodName string, // for error messages + methodSig string, // output literally + condClause string, // template condition for the member this should match on + retClause string, // clause returning the thing (how to delegate methodsig, generally) + twoReturns bool, // true if a nil should be returned as well as the error ) string { // The value pointed to by `na.m` isn't modified here, because we're sharing it with the child, who should do so. // This also means that value gets checked twice -- once by us, because we need to halt if we've already been used -- @@ -396,6 +399,14 @@ func kindedUnionNodeAssemblerMethodTemplateMunge( // I don't see a good way to remedy this shy of making more granular (unexported!) methods. (Might be worth it.) // This probably also isn't the same for all of the assembler methods: the methods we delegate to aren't doing as many check branches when they're for scalars, // because they expected to be used in contexts where many values of the 'm' enum aren't reachable -- an expectation we've suddenly subverted with this path! + // + // FUTURE: The error returns here are deeply questionable, and not as helpful as they could be. + // ErrNotUnionStructure is about the most applicable thing so far, but it's very freetext. + // ErrWrongKind wouldn't fit at all: assumes that we can say what kind of node we have, but this is the one case in the whole system where *we can't*; also, assumes there's one actual correct kind, and that too is false here! + maybeNilComma := "" + if twoReturns { + maybeNilComma += "nil," + } return ` func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) ` + methodSig + ` { switch *na.m { @@ -416,7 +427,7 @@ func kindedUnionNodeAssemblerMethodTemplateMunge( na.w.tag = {{ add $i 1 }} na.ca{{ add $i 1 }}.w = &na.w.x{{ add $i 1 }} na.ca{{ add $i 1 }}.m = na.m - return na.ca{{ add $i 1 }}.BeginMap(sizeHint) + return na.ca{{ add $i 1 }}` + retClause + ` {{- else if (eq (dot.AdjCfg.UnionMemlayout dot.Type) "interface") }} x := &_{{ $member | TypeSymbol }}{} na.w.x = x @@ -429,74 +440,91 @@ func kindedUnionNodeAssemblerMethodTemplateMunge( {{- end}} {{- end}} {{- end}} - // TODO i think you finally Need a method for if-no-members-match-this-kind for the default rejection to compile this time. - return nil, ipld.ErrWrongKind{doozy} + return ` + maybeNilComma + ` schema.ErrNotUnionStructure{TypeName: "{{ .PkgName }}.{{ .Type.Name }}.Repr", Detail: "` + methodName + ` called but is not valid for any of the kinds that are valid members of this union"} } ` } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodBeginMap(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `BeginMap`, `BeginMap(sizeHint int) (ipld.MapAssembler, error)`, `{{- if eq $member.RepresentationBehavior.String "map" }}`, `.BeginMap(sizeHint)`, + true, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodBeginList(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `BeginList`, `BeginList(sizeHint int) (ipld.ListAssembler, error)`, `{{- if eq $member.RepresentationBehavior.String "list" }}`, `.BeginList(sizeHint)`, + true, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignNull(w io.Writer) { // TODO: I think this may need some special handling to account for if our union is itself used in a nullable circumstance; that should overrule this behavior. doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignNull`, `AssignNull() error `, `{{- if eq $member.RepresentationBehavior.String "null" }}`, `.AssignNull()`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignBool(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignBool`, `AssignBool(v bool) error `, `{{- if eq $member.RepresentationBehavior.String "bool" }}`, `.AssignBool(v)`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignInt(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignInt`, `AssignInt(v int) error `, `{{- if eq $member.RepresentationBehavior.String "int" }}`, `.AssignInt(v)`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignFloat(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignFloat`, `AssignFloat(v float64) error `, `{{- if eq $member.RepresentationBehavior.String "float" }}`, `.AssignFloat(v)`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignString(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignString`, `AssignString(v string) error `, `{{- if eq $member.RepresentationBehavior.String "string" }}`, `.AssignString(v)`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignBytes(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignBytes`, `AssignBytes(v []byte) error `, `{{- if eq $member.RepresentationBehavior.String "bytes" }}`, `.AssignBytes(v)`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignLink(w io.Writer) { doTemplate(kindedUnionNodeAssemblerMethodTemplateMunge( + `AssignLink`, `AssignLink(v ipld.Link) error `, `{{- if eq $member.RepresentationBehavior.String "link" }}`, `.AssignLink(v)`, + false, ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignNode(w io.Writer) { From ecb4744cd60502c57417e9f59292b48f465fd460 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Wed, 5 Aug 2020 23:39:30 +0200 Subject: [PATCH 6/7] Remainder of gen for kinded union representations. The AssignNode method is now supplied; and the previous hacky shrug regarding switch statements with no applicable cases also had to go. (Turns out that the no-applicable-cases thing coincidentally worked for the "embedall" internal implementation mode; but didn't fly for the "interface" internal style, because that would end up with variable declarations made but not referenced. Harrumph.) --- schema/gen/go/genUnionReprKinded.go | 131 ++++++++++++++++++++++++---- schema/gen/go/templateUtil.go | 24 +++++ schema/typeMethods.go | 6 ++ 3 files changed, 144 insertions(+), 17 deletions(-) diff --git a/schema/gen/go/genUnionReprKinded.go b/schema/gen/go/genUnionReprKinded.go index f3e6651a..f283c7dd 100644 --- a/schema/gen/go/genUnionReprKinded.go +++ b/schema/gen/go/genUnionReprKinded.go @@ -79,7 +79,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodReprKind(w io.Writer) { return {{ $member.RepresentationBehavior | KindSymbol }} {{- end}} {{- else if (eq (.AdjCfg.UnionMemlayout .Type) "interface") }} - switch n2 := n.x.(type) { + switch n.x.(type) { {{- range $i, $member := .Type.Members }} case {{ $member | TypeSymbol }}: return {{ $member.RepresentationBehavior | KindSymbol }} @@ -92,19 +92,11 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodReprKind(w io.Writer) { `, w, g.AdjCfg, g) } -// A bunch of these methods could be improved by doing a gen-time switch for whether any of the possible members are the relevant kind at all; -// currently in the cases where there's no relevant members, we generate switch blocks that are empty except for their default... -// which works, but is arguably a little strange. -// I haven't checked if this dummy switch has any actual performance implications: -// I haven't tested if this produces unconditional assembly, -// nor if it successfully removes the access of the tag, -// though one might imagine a sufficiently clever compiler ought to do both of those things. -// Regardless, the gsloc is reducable. (Slightly. There are also bigger gains to be made elsewhere, I'm sure.) - func kindedUnionNodeMethodTemplateMunge( methodName string, // for error messages methodSig string, // output literally - condClause string, // template condition for the member this should match on + someSwitchClause string, // template condition for if *any* switch clause should be present + condClause string, // template condition for the member this should match on when in the range retClause string, // clause returning the thing (how to delegate methodsig, generally) appropriateKind string, // for error messages nopeSentinel string, // for error return paths; generally the zero value for the first return type. @@ -116,12 +108,18 @@ func kindedUnionNodeMethodTemplateMunge( // // This error text doesn't tell us what the member kind is. This might read weirdly. // It's possible we could try to cram a description of the inhabitant into the "TypeName" since it's stringy; but unclear if that's a good idea either. + + // These template concatenations have evolved into a mess very quickly. This entire thing should be replaced. + // String concatenations of template clauses is an atrociously unhygenic way to compose things; + // it looked like we could limp by with it for a while, but it's gotten messier faster than expected. + errorClause := `return ` + nopeSentinel if !nopeSentinelOnly { errorClause += `, ipld.ErrWrongKind{TypeName: "{{ .PkgName }}.{{ .Type.Name }}.Repr", MethodName: "` + methodName + `", AppropriateKind: ` + appropriateKind + `, ActualKind: n.ReprKind()}` } return ` func (n *_{{ .Type | TypeSymbol }}__Repr) ` + methodSig + ` { + ` + someSwitchClause + ` {{- if (eq (.AdjCfg.UnionMemlayout .Type) "embedAll") }} switch n.tag { {{- range $i, $member := .Type.Members }} @@ -140,8 +138,11 @@ func kindedUnionNodeMethodTemplateMunge( {{- end}} {{- end}} default: + {{- end}} ` + errorClause + ` + ` + someSwitchClause + ` } + {{- end}} } ` } @@ -150,6 +151,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByString(w io.Writer) doTemplate(kindedUnionNodeMethodTemplateMunge( `LookupByString`, `LookupByString(key string) (ipld.Node, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "map") }}`, `{{- if eq $member.RepresentationBehavior.String "map" }}`, `.LookupByString(key)`, `ipld.ReprKindSet_JustMap`, @@ -162,6 +164,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByIndex(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `LookupByIndex`, `LookupByIndex(idx int) (ipld.Node, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "list") }}`, `{{- if eq $member.RepresentationBehavior.String "list" }}`, `.LookupByIndex(idx)`, `ipld.ReprKindSet_JustList`, @@ -174,6 +177,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodLookupByNode(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `LookupByNode`, `LookupByNode(key ipld.Node) (ipld.Node, error)`, + `{{- if or (.Type.RepresentationStrategy.GetMember (Kind "map")) (.Type.RepresentationStrategy.GetMember (Kind "list")) }}`, `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, `.LookupByNode(key)`, `ipld.ReprKindSet_Recursive`, @@ -186,6 +190,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodLookupBySegment(w io.Writer) doTemplate(kindedUnionNodeMethodTemplateMunge( `LookupBySegment`, `LookupBySegment(seg ipld.PathSegment) (ipld.Node, error)`, + `{{- if or (.Type.RepresentationStrategy.GetMember (Kind "map")) (.Type.RepresentationStrategy.GetMember (Kind "list")) }}`, `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, `.LookupBySegment(seg)`, `ipld.ReprKindSet_Recursive`, @@ -198,6 +203,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodMapIterator(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `MapIterator`, `MapIterator() ipld.MapIterator`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "map") }}`, `{{- if eq $member.RepresentationBehavior.String "map" }}`, `.MapIterator()`, `ipld.ReprKindSet_JustMap`, @@ -210,6 +216,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodListIterator(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `ListIterator`, `ListIterator() ipld.ListIterator`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "list") }}`, `{{- if eq $member.RepresentationBehavior.String "list" }}`, `.ListIterator()`, `ipld.ReprKindSet_JustList`, @@ -222,6 +229,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodLength(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `Length`, `Length() int`, + `{{- if or (.Type.RepresentationStrategy.GetMember (Kind "map")) (.Type.RepresentationStrategy.GetMember (Kind "list")) }}`, `{{- if or (eq $member.RepresentationBehavior.String "map") (eq $member.RepresentationBehavior.String "list") }}`, `.Length()`, `ipld.ReprKindSet_Recursive`, @@ -250,6 +258,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodAsBool(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `AsBool`, `AsBool() (bool, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "bool") }}`, `{{- if eq $member.RepresentationBehavior.String "bool" }}`, `.AsBool()`, `ipld.ReprKindSet_JustBool`, @@ -262,6 +271,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodAsInt(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `AsInt`, `AsInt() (int, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "int") }}`, `{{- if eq $member.RepresentationBehavior.String "int" }}`, `.AsInt()`, `ipld.ReprKindSet_JustInt`, @@ -274,6 +284,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodAsFloat(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `AsFloat`, `AsFloat() (float64, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "float") }}`, `{{- if eq $member.RepresentationBehavior.String "float" }}`, `.AsFloat()`, `ipld.ReprKindSet_JustFloat`, @@ -286,6 +297,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodAsString(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `AsString`, `AsString() (string, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "string") }}`, `{{- if eq $member.RepresentationBehavior.String "string" }}`, `.AsString()`, `ipld.ReprKindSet_JustString`, @@ -298,6 +310,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodAsBytes(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `AsBytes`, `AsBytes() ([]byte, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "bytes") }}`, `{{- if eq $member.RepresentationBehavior.String "bytes" }}`, `.AsBytes()`, `ipld.ReprKindSet_JustBytes`, @@ -310,6 +323,7 @@ func (g unionReprKindedReprGenerator) EmitNodeMethodAsLink(w io.Writer) { doTemplate(kindedUnionNodeMethodTemplateMunge( `AsLink`, `AsLink() (ipld.Link, error)`, + `{{- if .Type.RepresentationStrategy.GetMember (Kind "link") }}`, `{{- if eq $member.RepresentationBehavior.String "link" }}`, `.AsLink()`, `ipld.ReprKindSet_JustLink`, @@ -528,12 +542,95 @@ func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignLink(w ), w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodAssignNode(w io.Writer) { - // TODO this is too wild for me at the moment, come back to it shortly - // it's basically got some of the body of kindedUnionNodeAssemblerMethodTemplateMunge, but repeated many more times. - // it also needs to handle nulls gingerly. - // and also handle pumping the full copy in the case of lists or maps. - - // this is gonna have a fun ErrWrongKind value too -- we might actually have to make a non-static set of acceptable kinds :D that's a first. + // This is a very mundane AssignNode: it just calls out to the other methods on this type. + // However, even that is a little more exciting than usual: because we can't *necessarily* reject any kind of arg, + // we have the whole barrage of switch cases here. We then leave any particular rejections to those methods. + // Several cases could be statically replaced with errors and it would be an improvement. + // + // Errors are problematic again, same as is noted in kindedUnionNodeAssemblerMethodTemplateMunge. + // We also end up returning errors with other method names due to how we delegate; unfortunate. + doTemplate(` + func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) AssignNode(v ipld.Node) error { + if v.IsNull() { + return na.AssignNull() + } + if v2, ok := v.(*_{{ .Type | TypeSymbol }}); ok { + switch *na.m { + case schema.Maybe_Value, schema.Maybe_Null: + panic("invalid state: cannot assign into assembler that's already finished") + case midvalue: + panic("invalid state: cannot assign null into an assembler that's already begun working on recursive structures!") + } + {{- if .Type | MaybeUsesPtr }} + if na.w == nil { + na.w = v2 + *na.m = schema.Maybe_Value + return nil + } + {{- end}} + *na.w = *v2 + *na.m = schema.Maybe_Value + return nil + } + switch v.ReprKind() { + case ipld.ReprKind_Bool: + v2, _ := v.AsBool() + return na.AssignBool(v2) + case ipld.ReprKind_Int: + v2, _ := v.AsInt() + return na.AssignInt(v2) + case ipld.ReprKind_Float: + v2, _ := v.AsFloat() + return na.AssignFloat(v2) + case ipld.ReprKind_String: + v2, _ := v.AsString() + return na.AssignString(v2) + case ipld.ReprKind_Bytes: + v2, _ := v.AsBytes() + return na.AssignBytes(v2) + case ipld.ReprKind_Map: + na, err := na.BeginMap(v.Length()) + if err != nil { + return err + } + itr := v.MapIterator() + for !itr.Done() { + k, v, err := itr.Next() + if err != nil { + return err + } + if err := na.AssembleKey().AssignNode(k); err != nil { + return err + } + if err := na.AssembleValue().AssignNode(v); err != nil { + return err + } + } + return na.Finish() + case ipld.ReprKind_List: + na, err := na.BeginList(v.Length()) + if err != nil { + return err + } + itr := v.ListIterator() + for !itr.Done() { + _, v, err := itr.Next() + if err != nil { + return err + } + if err := na.AssembleValue().AssignNode(v); err != nil { + return err + } + } + return na.Finish() + case ipld.ReprKind_Link: + v2, _ := v.AsLink() + return na.AssignLink(v2) + default: + panic("unreachable") + } + } + `, w, g.AdjCfg, g) } func (g unionReprKindedReprBuilderGenerator) EmitNodeAssemblerMethodPrototype(w io.Writer) { doTemplate(` diff --git a/schema/gen/go/templateUtil.go b/schema/gen/go/templateUtil.go index adefd91e..ba6adab5 100644 --- a/schema/gen/go/templateUtil.go +++ b/schema/gen/go/templateUtil.go @@ -54,6 +54,30 @@ func doTemplate(tmplstr string, w io.Writer, adjCfg *AdjunctCfg, data interface{ panic("invalid enumeration value!") } }, + "Kind": func(s string) ipld.ReprKind { + switch s { + case "map": + return ipld.ReprKind_Map + case "list": + return ipld.ReprKind_List + case "null": + return ipld.ReprKind_Null + case "bool": + return ipld.ReprKind_Bool + case "int": + return ipld.ReprKind_Int + case "float": + return ipld.ReprKind_Float + case "string": + return ipld.ReprKind_String + case "bytes": + return ipld.ReprKind_Bytes + case "link": + return ipld.ReprKind_Link + default: + panic("invalid enumeration value!") + } + }, "KindSymbol": func(k ipld.ReprKind) string { switch k { case ipld.ReprKind_Map: diff --git a/schema/typeMethods.go b/schema/typeMethods.go index 87440633..41f06470 100644 --- a/schema/typeMethods.go +++ b/schema/typeMethods.go @@ -148,6 +148,12 @@ func (r UnionRepresentation_Keyed) GetDiscriminant(t Type) string { panic("that type isn't a member of this union") } +// GetMember returns type info for the member matching the kind argument, +// or may return nil if that kind is not mapped to a member of this union. +func (r UnionRepresentation_Kinded) GetMember(k ipld.ReprKind) TypeName { + return r.table[k] +} + // Fields returns a slice of descriptions of the object's fields. func (t TypeStruct) Fields() []StructField { a := make([]StructField, len(t.fields)) From 2ea4150a1959e798d732979aa8f1f67aef15affd Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Wed, 19 Aug 2020 13:25:09 +0200 Subject: [PATCH 7/7] Update feature table: kinded union supported! --- schema/gen/go/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/gen/go/README.md b/schema/gen/go/README.md index af7f9bbe..2e16748e 100644 --- a/schema/gen/go/README.md +++ b/schema/gen/go/README.md @@ -95,7 +95,7 @@ Legend: | ... type level | ✔ | ✔ | | ... keyed representation | ✔ | ✔ | | ... envelope representation | ✘ | ✘ | -| ... kinded representation | ✘ | ✘ | +| ... kinded representation | ✔ | ✔ | | ... inline representation | ✘ | ✘ | | ... byteprefix representation | ✘ | ✘ |