Commit 59c295db authored by Eric Myhre's avatar Eric Myhre

Beginning of struct gen; much ado about Maybes.

I *think* the notes on Maybes are now oscillating increasingly closely
around a consistent centroid.  But getting here has been a one heck of
a noodle-scratcher.
parent cbef5c3e
......@@ -65,9 +65,37 @@ underreviewed
- may also just want to shield it entirely.
- might be easier than having its enum component include an 'invalid' state which can kick you in the shins at runtime.
- roughly the same decision we previously considered for other types (aside from the word for such an enum already existing here).
- if we make the zero state be 'absent' rather than introducing a new 'invalid' state, that makes this less bad.
- can't think of significant arguments against this. returning pointers to maybes embedded in larger structures does seem to be the way to go anyway.
- it's an adjunct parameter whether the value field in the MaybeT is a pointer; may vary for each T.
- need to sometimes use pointers: for cycle-breaking!
- want customizability of optimism (oversize-allocation versus alloc-count-amortization).
- is it possible to want different Maybe implementation strategies in different areas? Perhaps, but hopefully vanishingly unlikely in practice. Do not want to support this; complexity add high.
- thinking this may want to default to useptr=yes. less likely to generate end user surprise. can opt into noptr fastness when/where you know it works.
- there are a couple very predictable places where we *do* want useptr=no, also.
- strings! and other scalars. essentially *never* useful to have a ptr to those inside their MaybeT.
- MaybeT should be an alias of `*_T_Maybe`.
- Sure, sometimes we could get along fine by passing them around by value.
- But sometimes -- when they contain a large value and don't use a ptr internally -- we don't want to.
- And we don't want that to then embroil into *this* MaybeT requiring you to *handle* it externally as a ptr at the same time when some MaybeT2 requires the opposite.
- Coincidentally, removes a fair number of conditionalizations of '&' when returning fields.
- Not a factor in the decision, but a nice bonus.
- A bunch of remaining conditionals in templates also all consistently move to the end of their area, which is... sort of pleasing.
- This means we get to "shield" it entirely.
- Not a factor in the decision (perhaps surprisingly). But doesn't hurt.
- The zero values of the maybe type already didn't threaten to reveal zeros of the T, as long as the field was private and the zero state for maybes isn't 'Maybe_Value'.
- We could also just use an exported `*MaybeT`. But I see no reason to favor this.
- Since we've already resolved to embed MaybeT values (rather than bitpack their states together), there's no cost to this.
- When speciated methods return a `MaybeT`, it's an internal pointer to existing memory.
- When creating a populated maybe...
- If it's a useptr=yes, this might mean you get two allocs... except I think the outer one should optimize out:
- the conversion into maybe should be inlinable, and that should make the whole thing escape-analyzable to get rid of the maybe alloc?
- If it's a useptr=no, construction is probably going to involve a big ol' memcopy.
- This is unfortunate, but I can't think of a way around it short of doing the full dance of a builder.
- This probably won't be an issue in practice, if we're imagining useptr=no is only likely to be used on fairly small values.
- We can also just try to avoid the freestanding creation of maybes entirely.
- The ideal look and field of speciated builders has not been determined. SetAbsent, etc, methods are certainly in bounds.
- Structs and lists can certainly amortize these.
- Maps would turn tricky, except we're already happy with having an internal slice in those too. So, it's covered.
- Interestingly, maps also have the ability to easily store all of (null,absent,value{...}) in them, without extra space.
- But considering the thing about slices-in-maps, that may not be relevant. We want quick linear iterator-friendly reads of Maybe state too.
......@@ -7,7 +7,7 @@ import (
)
type AdjunctCfg struct {
typeSymbolOverrides map[schema.Type]string
typeSymbolOverrides map[schema.TypeName]string
fieldSymbolLowerOverrides map[schema.StructField]string
fieldSymbolUpperOverrides map[schema.StructField]string
......@@ -26,7 +26,7 @@ type AdjunctCfg struct {
// etc.
// (Most such augmentations are not configurable.)
func (cfg *AdjunctCfg) TypeSymbol(t schema.Type) string {
if x, ok := cfg.typeSymbolOverrides[t]; ok {
if x, ok := cfg.typeSymbolOverrides[t.Name()]; ok {
return x
}
return string(t.Name()) // presumed already upper
......
package gengo
import (
"fmt"
"io"
wish "github.com/warpfork/go-wish"
)
// EmitInternalEnums creates a file with enum types used internal.
// For example, the state machine values used in map and list builders.
func EmitInternalEnums(packageName string, w io.Writer) {
fmt.Fprint(w, wish.Dedent(`
package `+packageName+`
// Code generated go-ipld-prime DO NOT EDIT.
type maState uint8
const (
maState_initial maState = iota
maState_midKey
maState_expectValue
maState_midValue
maState_finished
)
`))
}
......@@ -50,13 +50,14 @@ func (g stringGenerator) EmitNativeBuilder(w io.Writer) {
}
func (g stringGenerator) EmitNativeMaybe(w io.Writer) {
// REVIEW: can this be extracted to the mixins package? it doesn't even vary for kind.
// REVIEW: what conventions and interfaces are required around Maybe types is very non-finalized.
// REVIEW: can this be extracted to the mixins package? it doesn't appear to vary for kind.
// We *do* somtimes need to vary this for whether or not 'v' is a pointer. For strings: it's not. For others? Depends.
doTemplate(`
type Maybe{{ .Type | TypeSymbol }} struct {
type _{{ .Type | TypeSymbol }}__Maybe struct {
m schema.Maybe
n {{ .Type | TypeSymbol }}
v {{ .Type | TypeSymbol }}
}
type Maybe{{ .Type | TypeSymbol }} = *_{{ .Type | TypeSymbol }}__Maybe
func (m Maybe{{ .Type | TypeSymbol }}) IsNull() bool {
return m.m == schema.Maybe_Null
......@@ -71,7 +72,7 @@ func (g stringGenerator) EmitNativeMaybe(w io.Writer) {
if !m.Exists() {
panic("unbox of a maybe rejected")
}
return m.n
return m.v
}
`, w, g.AdjCfg, g)
}
......@@ -128,7 +129,7 @@ func (g stringGenerator) EmitNodeMethodAsString(w io.Writer) {
func (g stringGenerator) EmitNodeMethodStyle(w io.Writer) {
doTemplate(`
func ({{ .Type | TypeSymbol }}) Style() ipld.NodeStyle {
return nil // TODO
return _{{ .Type | TypeSymbol }}__Style{}
}
`, w, g.AdjCfg, g)
}
......
package gengo
import (
"io"
"github.com/ipld/go-ipld-prime/schema"
"github.com/ipld/go-ipld-prime/schema/gen/go/mixins"
)
type structGenerator struct {
AdjCfg *AdjunctCfg
mixins.MapTraits
PkgName string
Type schema.TypeStruct
}
// --- native content and specializations --->
func (g structGenerator) EmitNativeType(w io.Writer) {
doTemplate(`
type _{{ .Type | TypeSymbol }} struct {
{{- range $field := .Type.Fields}}
{{ $field | FieldSymbolLower }} _{{ $field.Type | TypeSymbol }}{{if $field.IsMaybe }}__Maybe{{end}}
{{- end}}
}
type {{ .Type | TypeSymbol }} = *_{{ .Type | TypeSymbol }}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNativeAccessors(w io.Writer) {
doTemplate(`
{{- $type := .Type -}} {{- /* ranging modifies dot, unhelpfully */ -}}
{{- range $field := .Type.Fields }}
func (n _{{ $type | TypeSymbol }}) Field{{ $field | FieldSymbolUpper }}() {{ if $field.IsMaybe }}Maybe{{end}}{{ $field.Type | TypeSymbol }} {
return &n.{{ $field | FieldSymbolLower }}
}
{{- end}}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNativeBuilder(w io.Writer) {
// Unclear what, if anything, goes here.
}
func (g structGenerator) EmitNativeMaybe(w io.Writer) {
// TODO maybes need a lot of questions answered
}
// --- type info --->
func (g structGenerator) EmitTypeConst(w io.Writer) {
doTemplate(`
// TODO EmitTypeConst
`, w, g.AdjCfg, g)
}
// --- TypedNode interface satisfaction --->
func (g structGenerator) EmitTypedNodeMethodType(w io.Writer) {
doTemplate(`
func ({{ .Type | TypeSymbol }}) Type() schema.Type {
return nil /*TODO:typelit*/
}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitTypedNodeMethodRepresentation(w io.Writer) {
// Perhaps surprisingly, the way to get the representation node pointer
// does not actually depend on what the representation strategy is.
// REVIEW: this appears to be standard even across kinds; can we extract it?
doTemplate(`
func (n {{ .Type | TypeSymbol }}) Representation() ipld.Node {
return (*_{{ .Type | TypeSymbol }}__Repr)(n)
}
`, w, g.AdjCfg, g)
}
// --- Node interface satisfaction --->
func (g structGenerator) EmitNodeType(w io.Writer) {
// No additional types needed. Methods all attach to the native type.
// We do, however, want some constants for our fields;
// they'll make iterators able to work faster. So let's emit those.
doTemplate(`
var (
{{- $type := .Type -}} {{- /* ranging modifies dot, unhelpfully */ -}}
{{- range $field := .Type.Fields }}
fieldName__{{ $type | TypeSymbol }}_{{ $field | FieldSymbolUpper }} = _String{"{{ $field.Name }}"}
{{- end }}
)
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeTypeAssertions(w io.Writer) {
doTemplate(`
var _ ipld.Node = ({{ .Type | TypeSymbol }})(&_{{ .Type | TypeSymbol }}{})
var _ schema.TypedNode = ({{ .Type | TypeSymbol }})(&_{{ .Type | TypeSymbol }}{})
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeMethodLookupString(w io.Writer) {
doTemplate(`
func (n {{ .Type | TypeSymbol }}) LookupString(key string) (ipld.Node, error) {
switch key {
{{- range $field := .Type.Fields }}
case "{{ $field.Name }}":
{{- if $field.IsOptional }}
if n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Absent {
return ipld.Undef, nil
}
{{- end}}
{{- if $field.IsNullable }}
if n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Null {
return ipld.Null, nil
}
{{- end}}
{{- if or $field.IsOptional $field.IsNullable }}
return n.{{ $field | FieldSymbolLower }}.v, nil
{{- else}}
return &n.{{ $field | FieldSymbolLower }}, nil
{{- end}}
{{- end}}
default:
return nil, schema.ErrNoSuchField{Type: nil /*TODO*/, FieldName: key}
}
}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeMethodLookup(w io.Writer) {
doTemplate(`
func (n {{ .Type | TypeSymbol }}) Lookup(key ipld.Node) (ipld.Node, error) {
ks, err := key.AsString()
if err != nil {
return nil, ipld.ErrInvalidKey{"got " + key.ReprKind().String() + ", need string"}
}
return n.LookupString(ks)
}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeMethodMapIterator(w io.Writer) {
// Note that the typed iterator will report absent fields.
// The representation iterator (if has one) however will skip those.
doTemplate(`
func (n {{ .Type | TypeSymbol }}) MapIterator() ipld.MapIterator {
return &_{{ .Type | TypeSymbol }}__MapItr{n, 0}
}
type _{{ .Type | TypeSymbol }}__MapItr struct {
n {{ .Type | TypeSymbol }}
idx int
}
func (itr *_{{ .Type | TypeSymbol }}__MapItr) Next() (k ipld.Node, v ipld.Node, _ error) {
if itr.idx >= {{ len .Type.Fields }} {
return nil, nil, ipld.ErrIteratorOverread{}
}
switch itr.idx {
{{- $type := .Type -}} {{- /* ranging modifies dot, unhelpfully */ -}}
{{- range $i, $field := .Type.Fields }}
case {{ $i }}:
k = &fieldName__{{ $type | TypeSymbol }}_{{ $field | FieldSymbolUpper }}
{{- if $field.IsOptional }}
if itr.n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Absent {
v = ipld.Undef
break
}
{{- end}}
{{- if $field.IsNullable }}
if itr.n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Null {
v = ipld.Null
break
}
{{- end}}
{{- if or $field.IsOptional $field.IsNullable }}
v = itr.n.{{ $field | FieldSymbolLower}}.v
{{- else}}
v = &itr.n.{{ $field | FieldSymbolLower}}
{{- end}}
{{- end}}
default:
panic("unreachable")
}
itr.idx++
return
}
func (itr *_{{ .Type | TypeSymbol }}__MapItr) Done() bool {
return itr.idx >= {{ len .Type.Fields }}
}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeMethodLength(w io.Writer) {
doTemplate(`
func ({{ .Type | TypeSymbol }}) Length() int {
return {{ len .Type.Fields }}
}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeMethodStyle(w io.Writer) {
// REVIEW: this appears to be standard even across kinds; can we extract it?
doTemplate(`
func ({{ .Type | TypeSymbol }}) Style() ipld.NodeStyle {
return _{{ .Type | TypeSymbol }}__Style{}
}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeStyleType(w io.Writer) {
// REVIEW: this appears to be standard even across kinds; can we extract it?
doTemplate(`
type _{{ .Type | TypeSymbol }}__Style struct{}
func (_{{ .Type | TypeSymbol }}__Style) NewBuilder() ipld.NodeBuilder {
var nb _{{ .Type | TypeSymbol }}__Builder
nb.Reset()
return &nb
}
`, w, g.AdjCfg, g)
}
// --- NodeBuilder and NodeAssembler --->
func (g structGenerator) EmitNodeBuilder(w io.Writer) {
doTemplate(`
type _{{ .Type | TypeSymbol }}__Builder struct {
_{{ .Type | TypeSymbol }}__Assembler
}
func (nb *_{{ .Type | TypeSymbol }}__Builder) Build() ipld.Node {
if nb.state != maState_finished {
panic("invalid state: assembler for {{ .PkgName }}.{{ .Type.Name }} must be 'finished' before Build can be called!")
}
return nb.w
}
func (nb *_{{ .Type | TypeSymbol }}__Builder) Reset() {
var w _{{ .Type | TypeSymbol }}
*nb = _{{ .Type | TypeSymbol }}__Builder{_{{ .Type | TypeSymbol }}__Assembler{w: &w, state: maState_initial}}
}
`, w, g.AdjCfg, g)
}
func (g structGenerator) EmitNodeAssembler(w io.Writer) {
doTemplate(`
type _{{ .Type | TypeSymbol }}__Assembler struct {
w *_{{ .Type | TypeSymbol }}
state maState
}
func (na *_{{ .Type | TypeSymbol }}__Assembler) BeginMap(sizeHint int) (ipld.MapAssembler, error) {
panic("todo structassembler beginmap")
}
func (_{{ .Type | TypeSymbol }}__Assembler) BeginList(sizeHint int) (ipld.ListAssembler, error) {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.BeginList(0)
}
func (_{{ .Type | TypeSymbol }}__Assembler) AssignNull() error {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignNull()
}
func (_{{ .Type | TypeSymbol }}__Assembler) AssignBool(bool) error {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignBool(false)
}
func (_{{ .Type | TypeSymbol }}__Assembler) AssignInt(int) error {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignInt(0)
}
func (_{{ .Type | TypeSymbol }}__Assembler) AssignFloat(float64) error {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignFloat(0)
}
func (_{{ .Type | TypeSymbol }}__Assembler) AssignString(v string) error {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignString("")
}
func (_{{ .Type | TypeSymbol }}__Assembler) AssignBytes([]byte) error {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignBytes(nil)
}
func (_{{ .Type | TypeSymbol }}__Assembler) AssignLink(ipld.Link) error {
return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignLink(nil)
}
func (na *_{{ .Type | TypeSymbol }}__Assembler) AssignNode(v ipld.Node) error {
panic("todo structassembler assignNode")
}
func (_{{ .Type | TypeSymbol }}__Assembler) Style() ipld.NodeStyle {
return _{{ .Type | TypeSymbol }}__Style{}
}
`, w, g.AdjCfg, g)
}
package gengo
import (
"github.com/ipld/go-ipld-prime/schema"
"github.com/ipld/go-ipld-prime/schema/gen/go/mixins"
)
var _ TypeGenerator = &stringReprStringGenerator{}
func NewStructReprMapGenerator(pkgName string, typ schema.TypeStruct, adjCfg *AdjunctCfg) TypeGenerator {
return structReprMapGenerator{
structGenerator{
adjCfg,
mixins.MapTraits{
pkgName,
string(typ.Name()),
adjCfg.TypeSymbol(typ),
},
pkgName,
typ,
},
}
}
type structReprMapGenerator struct {
structGenerator
}
func (g structReprMapGenerator) GetRepresentationNodeGen() NodeGenerator {
return nil
}
......@@ -87,6 +87,9 @@ func EmitEntireType(tg TypeGenerator, w io.Writer) {
tg.EmitTypedNodeMethodRepresentation(w)
rng := tg.GetRepresentationNodeGen()
if rng == nil { // FIXME: hack to save me from stubbing tons right now, remove when done
return
}
EmitNode(rng, w)
}
......
......@@ -21,9 +21,26 @@ func TestSmoke(t *testing.T) {
pkgName := "whee"
adjCfg := &AdjunctCfg{}
f = openOrPanic("_test/minima.go")
EmitInternalEnums(pkgName, f)
tString := schema.SpawnString("String")
tStroct := schema.SpawnStruct("Stroct",
[]schema.StructField{
schema.SpawnStructField("f1", tString, false, false),
schema.SpawnStructField("f2", tString, true, false),
schema.SpawnStructField("f3", tString, true, true),
schema.SpawnStructField("f4", tString, false, true),
},
schema.StructRepresentation_Map{},
)
f = openOrPanic("_test/tString.go")
EmitFileHeader(pkgName, f)
EmitEntireType(NewStringReprStringGenerator(pkgName, tString, adjCfg), f)
f = openOrPanic("_test/tStroct.go")
EmitFileHeader(pkgName, f)
EmitEntireType(NewStructReprMapGenerator(pkgName, tStroct, adjCfg), f)
}
......@@ -5,6 +5,8 @@ import (
"text/template"
wish "github.com/warpfork/go-wish"
"github.com/ipld/go-ipld-prime/schema"
)
func doTemplate(tmplstr string, w io.Writer, adjCfg *AdjunctCfg, data interface{}) {
......@@ -13,6 +15,16 @@ func doTemplate(tmplstr string, w io.Writer, adjCfg *AdjunctCfg, data interface{
"TypeSymbol": adjCfg.TypeSymbol,
"FieldSymbolLower": adjCfg.FieldSymbolLower,
"FieldSymbolUpper": adjCfg.FieldSymbolUpper,
"FieldTypeOrMaybe": func(f schema.StructField) string {
// Returns the symbol used for embedding the field's type, or, the MaybeT of that type.
// Shorthand for:
// `{{if or $field.IsOptional $field.IsNullable }}Maybe{{else}}_{{end}}{{ $field.Type | TypeSymbol }}`
// REVIEW: still not sure if this is gonna be worth it. Thought it would appear in more places; actually, is only one.
if f.IsOptional() || f.IsNullable() {
return "Maybe" + adjCfg.TypeSymbol(f.Type())
}
return "_" + adjCfg.TypeSymbol(f.Type())
},
}).
Parse(wish.Dedent(tmplstr)))
if err := tmpl.Execute(w, data); err != nil {
......
......@@ -3,7 +3,7 @@ package schema
type Maybe uint8
const (
Maybe_Value = Maybe(0)
Maybe_Absent = Maybe(0)
Maybe_Null = Maybe(1)
Maybe_Absent = Maybe(2)
Maybe_Value = Maybe(2)
)
......@@ -135,6 +135,12 @@ func (f StructField) IsOptional() bool { return f.optional }
// or either, or neither.
func (f StructField) IsNullable() bool { return f.nullable }
// IsMaybe returns true if the field value is allowed to be either null or absent.
//
// This is a simple "or" of the two properties,
// but this method is a shorthand that turns out useful often.
func (f StructField) IsMaybe() bool { return f.nullable || f.optional }
func (t TypeStruct) RepresentationStrategy() StructRepresentation {
return t.representation
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment