package main import ( "fmt" "path/filepath" "sort" "strconv" "strings" ) // eventIR is one or . Copies point at their source IR and // share its container. type eventIR struct { Name string // Go base name, e.g. MapRequest Code int // core: wire code; extension: relative code; XGE: evtype XGE bool Copy *eventIR C *container Node *Node KindExpr string // "" when the event has no Kind (core XGE envelope) } const eventLifetime = "Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from." func eventCode(n *Node) (int, error) { code, err := strconv.Atoi(n.Attr["number"]) if err != nil || code < 0 || code > 255 { return 0, n.fail("invalid event number %q", n.Attr["number"]) } return code, nil } func kindExpr(m *Module, e *eventIR) (string, error) { switch { case m.ExtName == "" && e.XGE: // The core XGE envelope is not an event type: DecodeEvent resolves // every XGE packet to its extension's kind or KindUnknown. return "", nil case m.ExtName == "": if e.Code < 2 || e.Code > 34 { return "", e.Node.fail("core event code %d outside 2..34", e.Code) } return fmt.Sprintf("xturbo.EventKind(%d)", e.Code), nil case e.XGE: return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | 0x80 | %d", m.ExtName, e.Code), nil } if e.Code > 127 { return "", e.Node.fail("extension event number %d above 127", e.Code) } if e.Code == 0 { // "| 0" is a no-op that staticcheck rejects (SA4016). return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s) << 8", m.ExtName), nil } return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | %d", m.ExtName, e.Code), nil } // xgeReserved are the implicit XGE header getters. const xgeReserved = "Raw Extension Length EventType Tail" func resolveEvents(m *Module) ([]*eventIR, error) { var events []*eventIR byXName := map[string]*eventIR{} for _, n := range m.Root.Children { if n.Kind != "event" && n.Kind != "eventcopy" { continue } code, err := eventCode(n) if err != nil { return nil, err } e := &eventIR{Name: goName(n.Attr["name"]), Code: code, Node: n} if n.Kind == "eventcopy" { src := byXName[n.Attr["ref"]] if src == nil { return nil, n.fail("eventcopy of unknown event %s", n.Attr["ref"]) } if src.Copy != nil { src = src.Copy } e.Copy, e.C, e.XGE = src, src.C, src.XGE } else { e.XGE = n.Attr["xge"] == "true" lay, reserved := layoutEvent, "Raw" switch { case e.XGE: lay, reserved = layoutXGE, xgeReserved if code > 127 { return nil, n.fail("xge evtype %d >= 128", code) } case n.Attr["no-sequence-number"] == "true": lay = layoutEventNoSeq } c, err := buildContainer(m, e.Name+"Event", "e", n.Children, lay, reserved) if err != nil { return nil, err } if c.Dynamic >= 0 || c.Fixed > 32 { return nil, n.fail("event members must lie within the first 32 bytes") } for _, mem := range c.Members { if mem.Kind == "list" && mem.ListKind == "iter" { return nil, n.fail("variable-size struct list in event") } } e.C = c } if e.KindExpr, err = kindExpr(m, e); err != nil { return nil, err } names := []string{e.Name + "Event", e.Name + "Of"} if e.KindExpr != "" { names = append(names, "Kind"+e.Name) } if m.ExtName == "" && !e.XGE { names = append(names, e.Name+"EventData") } for _, s := range names { if err := m.claim(s, n); err != nil { return nil, err } } byXName[n.Attr["name"]] = e events = append(events, e) } return events, nil } // dataField renders one EventData struct field and its Put statement. func dataField(c *container, mem member) (field, put string, err error) { name := mem.Method if name == "Put" { name += "_" } src := "d." + name at := fmt.Sprintf("tmp[%d:]", mem.Static) switch { case mem.Kind == "field" && isScalar(mem.Type): return name + " " + mem.GoType, putScalar(mem.Type, src, at), nil case mem.Kind == "field": return name + " " + mem.GoType, fmt.Sprintf("%s.Put(%s);", src, at), nil case mem.Kind == "list" && mem.Count.Kind == "value": count := mem.Size / mem.Type.Size elem := mem.Elem var b strings.Builder fmt.Fprintf(&b, "for i := range %s {", src) target := fmt.Sprintf("tmp[%d+i*%d:]", mem.Static, mem.Type.Size) if isScalar(mem.Type) { b.WriteString(putScalar(mem.Type, src+"[i]", target)) } else { fmt.Fprintf(&b, "%s[i].Put(%s);", src, target) } b.WriteString("};") return fmt.Sprintf("%s [%d]%s", name, count, elem), b.String(), nil } return "", "", c.Module.Root.fail("unsupported event member %s for EventData", mem.XName) } func eventDecl(m *Module, e *eventIR, lists map[string]string) (string, error) { var b strings.Builder if e.KindExpr != "" { fmt.Fprintf(&b, "// Kind%s identifies %s events in xturbo.Event.Kind.\nconst Kind%s = %s;\n", e.Name, e.Name, e.Name, e.KindExpr) } view := e.Name + "Event" if e.Copy != nil { src := e.Copy.Name fmt.Fprintf(&b, "// %s shares %s's layout (eventcopy).\ntype %s = %sEvent;\n", view, src, view, src) fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind.\nfunc %sOf(ev *xturbo.Event) %s { return %sOf(ev) };\n", e.Name, e.Name, e.Name, view, src) if m.ExtName == "" && !e.XGE { fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent with %s's fields.\ntype %sEventData %sEventData;\n", e.Name, e.Name, src, e.Name, src) fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst.\nfunc (d *%sEventData) Put(dst *[32]byte) { (*%sEventData)(d).Put(dst); dst[0] = %d };\n", e.Code, e.Name, src, e.Code) } return b.String(), nil } c := e.C if e.XGE { fmt.Fprintf(&b, "// %s is a view over a %s generic event: the copied Event.Raw plus the borrowed Event.Tail.\n// Tail is valid only until the Handler callback returns; copy what you keep.\n", view, e.Name) fmt.Fprintf(&b, "type %s struct { raw *[32]byte; tail []byte };\n", view) fmt.Fprintf(&b, "// %sOf views ev as %s; false when ev is not a generic event or Tail does not match its length.\n", e.Name, e.Name) fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) (%s, bool) { if ev.Raw[0]&0x7f != 35 || len(ev.Tail) != 4*int(binary.LittleEndian.Uint32(ev.Raw[4:])) { return %s{}, false }; return %s{raw: &ev.Raw, tail: ev.Tail}, true };\n", e.Name, view, view, view) fmt.Fprintf(&b, "func (e %s) Extension() uint8 { return e.raw[1] };\n", view) fmt.Fprintf(&b, "func (e %s) Length() uint32 { return binary.LittleEndian.Uint32(e.raw[4:]) };\n", view) fmt.Fprintf(&b, "func (e %s) EventType() uint16 { return binary.LittleEndian.Uint16(e.raw[8:]) };\n", view) fmt.Fprintf(&b, "func (e %s) Tail() []byte { return e.tail };\n", view) } else { fmt.Fprintf(&b, "// %s is a view over a %s event's 32 bytes. %s\n", view, e.Name, eventLifetime) fmt.Fprintf(&b, "type %s struct { raw *[32]byte };\n", view) fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind: a wrong kind yields garbage, never a panic.\n", e.Name, e.Name) fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) %s { return %s{raw: &ev.Raw} };\n", e.Name, view, view) } fmt.Fprintf(&b, "func (e %s) Raw() *[32]byte { return e.raw };\n", view) if err := c.getters(&b, view, lists); err != nil { return "", err } if m.ExtName != "" || e.XGE { return b.String(), nil } var fields, puts []string for _, mem := range c.Members { if mem.Kind == "pad" { continue } f, p, err := dataField(c, mem) if err != nil { return "", err } fields = append(fields, f) puts = append(puts, p) } fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent.\ntype %sEventData struct {\n%s\n};\n", e.Name, e.Name, e.Name, strings.Join(fields, "\n")) // Encode into a local array and store it last: borrowed fields (union // views such as ClientMessageData) may alias dst, e.g. d.Put(&ev.Raw). fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst. dst may alias\n// memory that d's borrowed fields view, such as the Event.Raw they were read from.\nfunc (d *%sEventData) Put(dst *[32]byte) { var tmp [32]byte; tmp[0] = %d; %s; *dst = tmp };\n", e.Code, e.Name, e.Code, strings.Join(puts, "")) return b.String(), nil } // errorIR is one or . type errorIR struct { Const, Name string Code int } func errorConst(xname string) string { return "Bad" + goName(strings.TrimPrefix(xname, "Bad")) } func resolveErrors(m *Module) ([]errorIR, error) { var errs []errorIR seen := map[int]bool{} for _, n := range m.Root.Children { if n.Kind != "error" && n.Kind != "errorcopy" { continue } code, err := strconv.Atoi(n.Attr["number"]) if err != nil || code < 0 || code > 255 || seen[code] { return nil, n.fail("invalid or duplicate error number %q", n.Attr["number"]) } seen[code] = true e := errorIR{Const: errorConst(n.Attr["name"]), Code: code} e.Name = e.Const if err := m.claim(e.Const, n); err != nil { return nil, err } errs = append(errs, e) } return errs, nil } // errorCount is one past the highest error number. func errorCount(m *Module) int { count := 0 for _, n := range m.Root.Children { if n.Kind == "error" || n.Kind == "errorcopy" { code, _ := strconv.Atoi(n.Attr["number"]) count = max(count, code+1) } } return count } // eventCount is one past the highest non-XGE event number. func eventCount(m *Module) (int, error) { count := 0 for _, n := range m.Root.Children { if (n.Kind == "event" || n.Kind == "eventcopy") && n.Attr["xge"] != "true" { code, err := eventCode(n) if err != nil { return 0, err } count = max(count, code+1) } } return count, nil } func errorsDecl(m *Module, errs []errorIR) string { var b strings.Builder what := "wire error codes" if m.ExtName != "" { what = "error codes relative to the extension's FirstError (XError.Rel)" } fmt.Fprintf(&b, "// Error codes: %s.\nconst (\n", what) for _, e := range errs { fmt.Fprintf(&b, "%s uint8 = %d\n", e.Const, e.Code) } b.WriteString(");\n") names := make([]string, errorCount(m)) for _, e := range errs { names[e.Code] = e.Name } b.WriteString("var errorNames = [...]string{") for _, n := range names { fmt.Fprintf(&b, "%q,", n) } b.WriteString("};\n") fmt.Fprintf(&b, "// ErrorName returns the name of an error code (%s), or \"\" when unknown.\n", what) b.WriteString("func ErrorName(code uint8) string { if int(code) < len(errorNames) { return errorNames[code] }; return \"\" };\n") return b.String() } func emitEvents(modules []*Module) error { for _, m := range modules { events, err := resolveEvents(m) if err != nil { return err } lists := map[string]string{} var decls []string for _, e := range events { d, err := eventDecl(m, e, lists) if err != nil { return err } decls = append(decls, d) } var names []string for name := range lists { // Reuse a list view already declared by replies_generated.go. if old := m.Names[name]; old != nil && old == m.Root { continue } if err := m.claim(name, m.Root); err != nil { return err } names = append(names, name) } sort.Strings(names) for _, name := range names { decls = append(decls, lists[name]) } errs, err := resolveErrors(m) if err != nil { return err } if len(errs) > 0 { data := struct { Pkg string Decls []string }{Pkg: m.Pkg, Decls: []string{errorsDecl(m, errs)}} if err := writeTemplate(filepath.Join(m.Pkg, "errors_generated.go"), "errors", data); err != nil { return err } } if len(decls) == 0 { continue } imports := importsFor(m, strings.Join(decls, "\n")) + "\nimport \"bnema.dev/xturbo\"" decls = append([]string{imports}, decls...) data := struct { Pkg string Decls []string }{Pkg: m.Pkg, Decls: decls} if err := writeTemplate(filepath.Join(m.Pkg, "events_generated.go"), "events", data); err != nil { return err } if err := emitEventFuzz(m, events); err != nil { return err } } return nil } // emitEventFuzz writes FuzzEventViews: every view getter runs on arbitrary // bytes, and for core events EventData built from the getters survives a // Put/view round trip. Seeds are the package's event goldens. func emitEventFuzz(m *Module, events []*eventIR) error { var b strings.Builder b.WriteString("func FuzzEventViews(f *testing.F) {\nf.Add(make([]byte, 32));\n") goldens, _ := filepath.Glob(filepath.Join("testdata/golden", m.Pkg, "*.event.hex")) sort.Strings(goldens) for _, g := range goldens { fmt.Fprintf(&b, "if raw, err := xtest.LoadHex(%q); err != nil { f.Fatal(err) } else { f.Add(raw) };\n", "../"+filepath.ToSlash(g)) } b.WriteString("f.Fuzz(func(t *testing.T, raw []byte) {\nif len(raw) < 32 { return };\nvar ev xturbo.Event;\ncopy(ev.Raw[:], raw);\nev.Tail = raw[32:];\n") for _, e := range events { if e.Copy != nil { fmt.Fprintf(&b, "_ = %sOf(&ev);\n", e.Name) continue } v := "v" + e.Name if e.XGE { fmt.Fprintf(&b, "if %s, ok := %sOf(&ev); ok { _ = %s.Extension(); _ = %s.Length(); _ = %s.EventType(); _ = len(%s.Tail());", v, e.Name, v, v, v, v) } else { fmt.Fprintf(&b, "{ %s := %sOf(&ev);", v, e.Name) } touch(&b, e.C, v, 0, nil, `t.Fatal("iterator error")`) if m.ExtName == "" && !e.XGE { var fields, checks []string for _, mem := range e.C.Members { if mem.Kind == "pad" { continue } name := mem.Method if name == "Put" { name += "_" } switch { case mem.Kind == "list" && mem.ListKind == "bytes": fields = append(fields, fmt.Sprintf("%s: [%d]%s(%s.%s())", name, mem.Size/mem.Type.Size, mem.Elem, v, mem.Method)) checks = append(checks, fmt.Sprintf("if !bytes.Equal(w.%s(), %s.%s()) { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method)) case mem.Kind == "field" && isScalar(mem.Type): fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method)) checks = append(checks, fmt.Sprintf("if w.%s() != %s.%s() { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method)) case mem.Kind == "field": fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method)) checks = append(checks, fmt.Sprintf("if !bytes.Equal(out[%d:%d], ev.Raw[%d:%d]) { t.Fatal(%q) };", mem.Static, mem.Static+mem.Size, mem.Static, mem.Static+mem.Size, e.Name+"."+mem.Method)) default: return e.Node.fail("event member %s has no EventData round trip", mem.XName) } } fmt.Fprintf(&b, "d := %sEventData{%s}; var out [32]byte; d.Put(&out);", e.Name, strings.Join(fields, ", ")) fmt.Fprintf(&b, "if out[0] != %d { t.Fatal(\"%s code\") };", e.Code, e.Name) b.WriteString("back := xturbo.Event{Raw: out};") fmt.Fprintf(&b, "w := %sOf(&back); %s", e.Name, strings.Join(checks, "")) // In place: dst aliases the Raw that d's borrowed fields view. fmt.Fprintf(&b, "in := xturbo.Event{Raw: ev.Raw}; vi := %sOf(&in); di := %sEventData{%s}; di.Put(&in.Raw); if in.Raw != out { t.Fatal(%q) };", e.Name, e.Name, strings.ReplaceAll(strings.Join(fields, ", "), v+".", "vi."), e.Name+" in-place Put") } b.WriteString("};\n") } b.WriteString("})\n}\n") body := b.String() imports := "import \"testing\"\nimport \"bnema.dev/xturbo\"" if len(goldens) > 0 { imports += "\nimport \"bnema.dev/xturbo/internal/xtest\"" } if strings.Contains(body, "bytes.") { imports += "\nimport \"bytes\"" } data := struct { Pkg string Decls []string }{Pkg: m.Pkg, Decls: []string{imports, body}} return writeTemplate(filepath.Join(m.Pkg, "events_fuzz_test.go"), "events", data) }