detached d08e097

#1 feat(xtgen): generator for xcb-proto bindings

state
merged · d08e097 · fast-forward
branch
feat/xtgen → main
opened
bnema ·
updated
CommitSubjectAuthorDateSignatureStatus
d08e097feat(xtgen): decode replies as zero-copy views with list iteratorsbriceGPG Verified
017e663feat(xtgen): encode typed mask-ordered request value listsbriceGPG Verified
582be88feat(xtgen): encode request lists and derive exclusive lengthsbriceGPG Verified
8e6fe5cfeat(xtgen): encode fixed requests with captured wire goldensbriceGPG Verified
85a51ddfeat(xtgen): resolve wire types and emit typed layoutsbriceGPG Verified
2c9c8eafeat(xtgen): parse pinned XML into a strict source-located ASTbriceGPG Verified

Checks 4

102 files · +16959 -1
4/4
A bigreq/replies_fuzz_test.go +21 -0
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "testing"
5+
6+func FuzzParseEnableReply(f *testing.F) {
7+ f.Add(make([]byte, 32))
8+ f.Fuzz(func(t *testing.T, raw []byte) {
9+ r, ok := ParseEnableReply(raw)
10+ if !ok {
11+ if len(raw) >= 32 {
12+ t.Fatal("rejected a complete fixed-size reply")
13+ }
14+ return
15+ }
16+ if len(r.Raw()) != len(raw) {
17+ t.Fatal("Raw must return the parsed bytes")
18+ }
19+ _ = r.MaximumRequestLength()
20+ })
21+}
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "testing"
5+
6+func FuzzParseEnableReply(f *testing.F) {
7+ f.Add(make([]byte, 32))
8+ f.Fuzz(func(t *testing.T, raw []byte) {
9+ r, ok := ParseEnableReply(raw)
10+ if !ok {
11+ if len(raw) >= 32 {
12+ t.Fatal("rejected a complete fixed-size reply")
13+ }
14+ return
15+ }
16+ if len(r.Raw()) != len(raw) {
17+ t.Fatal("Raw must return the parsed bytes")
18+ }
19+ _ = r.MaximumRequestLength()
20+ })
21+}
A bigreq/replies_generated.go +21 -0
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "encoding/binary"
5+
6+// EnableReply is a zero-copy view over a Enable reply; offsets count from reply byte 0.
7+// It borrows the reply bytes: valid only until the Handler callback returns, because the read ring is reused. Copy what you keep.
8+type EnableReply struct{ raw []byte }
9+
10+// ParseEnableReply validates raw (the whole reply, 32+4*length bytes) once; getters do not re-check.
11+func ParseEnableReply(raw []byte) (EnableReply, bool) {
12+ if len(raw) < 32 {
13+ return EnableReply{}, false
14+ }
15+ r := EnableReply{raw: raw}
16+ return r, true
17+}
18+func (r EnableReply) Raw() []byte { return r.raw }
19+func (r EnableReply) MaximumRequestLength() uint32 {
20+ return uint32(binary.LittleEndian.Uint32(r.raw[8:]))
21+}
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "encoding/binary"
5+
6+// EnableReply is a zero-copy view over a Enable reply; offsets count from reply byte 0.
7+// It borrows the reply bytes: valid only until the Handler callback returns, because the read ring is reused. Copy what you keep.
8+type EnableReply struct{ raw []byte }
9+
10+// ParseEnableReply validates raw (the whole reply, 32+4*length bytes) once; getters do not re-check.
11+func ParseEnableReply(raw []byte) (EnableReply, bool) {
12+ if len(raw) < 32 {
13+ return EnableReply{}, false
14+ }
15+ r := EnableReply{raw: raw}
16+ return r, true
17+}
18+func (r EnableReply) Raw() []byte { return r.raw }
19+func (r EnableReply) MaximumRequestLength() uint32 {
20+ return uint32(binary.LittleEndian.Uint32(r.raw[8:]))
21+}
A bigreq/requests_generated.go +21 -0
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "bnema.dev/xturbo"
5+
6+func Enable(b *xturbo.Buf) xturbo.Seq {
7+ if b.Ext == nil {
8+ b.Fail(xturbo.ErrExtMissing)
9+ return 0
10+ }
11+ major := b.Ext.Major(xturbo.ExtBigRequests)
12+ if major == 0 {
13+ b.Fail(xturbo.ErrExtMissing)
14+ return 0
15+ }
16+ p, seq := b.Request(major, 0, 0)
17+ if p == nil {
18+ return 0
19+ }
20+ return seq
21+}
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "bnema.dev/xturbo"
5+
6+func Enable(b *xturbo.Buf) xturbo.Seq {
7+ if b.Ext == nil {
8+ b.Fail(xturbo.ErrExtMissing)
9+ return 0
10+ }
11+ major := b.Ext.Major(xturbo.ExtBigRequests)
12+ if major == 0 {
13+ b.Fail(xturbo.ErrExtMissing)
14+ return 0
15+ }
16+ p, seq := b.Request(major, 0, 0)
17+ if p == nil {
18+ return 0
19+ }
20+ return seq
21+}
A bigreq/sizes_test.go +11 -0
@@ -0,0 +1,11 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "testing"
5+
6+func TestWireSizes(t *testing.T) {
7+ if got := 32; got != 32 {
8+ t.Errorf("reply Enable fixed size = %d", got)
9+ }
10+
11+}
@@ -0,0 +1,11 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
3+
4+import "testing"
5+
6+func TestWireSizes(t *testing.T) {
7+ if got := 32; got != 32 {
8+ t.Errorf("reply Enable fixed size = %d", got)
9+ }
10+
11+}
A bigreq/types_generated.go +2 -0
@@ -0,0 +1,2 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
@@ -0,0 +1,2 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package bigreq
A buf.go +92 -0
@@ -0,0 +1,92 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+)
7+
8+// Seq identifies a request; zero is invalid.
9+type Seq uint64
10+
11+var (
12+ ErrRequestTooLarge = errors.New("xturbo: request too large")
13+ ErrExtMissing = errors.New("xturbo: extension missing")
14+ ErrBadLength = errors.New("xturbo: bad length")
15+ ErrSeqOverflow = errors.New("xturbo: sequence overflow")
16+)
17+
18+// Buf is the caller-owned request encoder. The transport will own flushing and
19+// acknowledgment in Phase 3. It is not safe for concurrent use.
20+type Buf struct {
21+ Ext *ExtTable
22+ b []byte
23+ flushed int
24+ next, acked Seq
25+ maxReq uint32
26+ err error
27+}
28+
29+func NewBuf(size int) *Buf {
30+ if size <= 0 {
31+ size = 64 << 10
32+ }
33+ return &Buf{b: make([]byte, 0, size), next: 1, maxReq: 262140}
34+}
35+func (b *Buf) Fail(err error) {
36+ if b.err == nil {
37+ b.err = err
38+ }
39+}
40+func (b *Buf) Err() error { return b.err }
41+func (b *Buf) Reset() { b.b = b.b[:0]; b.flushed = 0 }
42+func (b *Buf) Bytes() []byte { return b.b[b.flushed:] }
43+func (b *Buf) Len() int { return len(b.b) - b.flushed }
44+func (b *Buf) Next() Seq { return b.next }
45+
46+// Request reserves a zeroed, four-byte-aligned body after the wire header.
47+// BIG-REQUESTS enablement is transport-owned; maxReq starts at the core limit.
48+func (b *Buf) Request(major, data uint8, body int) ([]byte, Seq) {
49+ if b.err != nil {
50+ return nil, 0
51+ }
52+ if body < 0 || body&3 != 0 {
53+ b.Fail(ErrBadLength)
54+ return nil, 0
55+ }
56+ if uint64(body)+4 > uint64(b.maxReq) {
57+ b.Fail(ErrRequestTooLarge)
58+ return nil, 0
59+ }
60+ if b.next-b.acked >= 0xffff {
61+ b.Fail(ErrSeqOverflow)
62+ return nil, 0
63+ }
64+ header := 4
65+ if (body+4)/4 > 0xffff {
66+ header = 8
67+ }
68+ total := body + header
69+ start := len(b.b)
70+ if total > cap(b.b)-start {
71+ capacity := cap(b.b) * 2
72+ if capacity < start+total {
73+ capacity = start + total
74+ }
75+ grown := make([]byte, start, capacity)
76+ copy(grown, b.b)
77+ b.b = grown
78+ }
79+ b.b = b.b[:start+total]
80+ p := b.b[start:]
81+ clear(p)
82+ p[0] = major
83+ p[1] = data
84+ if header == 4 {
85+ binary.LittleEndian.PutUint16(p[2:], uint16(total/4))
86+ } else {
87+ binary.LittleEndian.PutUint32(p[4:], uint32(total/4))
88+ }
89+ seq := b.next
90+ b.next++
91+ return p[header:], seq
92+}
@@ -0,0 +1,92 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+)
7+
8+// Seq identifies a request; zero is invalid.
9+type Seq uint64
10+
11+var (
12+ ErrRequestTooLarge = errors.New("xturbo: request too large")
13+ ErrExtMissing = errors.New("xturbo: extension missing")
14+ ErrBadLength = errors.New("xturbo: bad length")
15+ ErrSeqOverflow = errors.New("xturbo: sequence overflow")
16+)
17+
18+// Buf is the caller-owned request encoder. The transport will own flushing and
19+// acknowledgment in Phase 3. It is not safe for concurrent use.
20+type Buf struct {
21+ Ext *ExtTable
22+ b []byte
23+ flushed int
24+ next, acked Seq
25+ maxReq uint32
26+ err error
27+}
28+
29+func NewBuf(size int) *Buf {
30+ if size <= 0 {
31+ size = 64 << 10
32+ }
33+ return &Buf{b: make([]byte, 0, size), next: 1, maxReq: 262140}
34+}
35+func (b *Buf) Fail(err error) {
36+ if b.err == nil {
37+ b.err = err
38+ }
39+}
40+func (b *Buf) Err() error { return b.err }
41+func (b *Buf) Reset() { b.b = b.b[:0]; b.flushed = 0 }
42+func (b *Buf) Bytes() []byte { return b.b[b.flushed:] }
43+func (b *Buf) Len() int { return len(b.b) - b.flushed }
44+func (b *Buf) Next() Seq { return b.next }
45+
46+// Request reserves a zeroed, four-byte-aligned body after the wire header.
47+// BIG-REQUESTS enablement is transport-owned; maxReq starts at the core limit.
48+func (b *Buf) Request(major, data uint8, body int) ([]byte, Seq) {
49+ if b.err != nil {
50+ return nil, 0
51+ }
52+ if body < 0 || body&3 != 0 {
53+ b.Fail(ErrBadLength)
54+ return nil, 0
55+ }
56+ if uint64(body)+4 > uint64(b.maxReq) {
57+ b.Fail(ErrRequestTooLarge)
58+ return nil, 0
59+ }
60+ if b.next-b.acked >= 0xffff {
61+ b.Fail(ErrSeqOverflow)
62+ return nil, 0
63+ }
64+ header := 4
65+ if (body+4)/4 > 0xffff {
66+ header = 8
67+ }
68+ total := body + header
69+ start := len(b.b)
70+ if total > cap(b.b)-start {
71+ capacity := cap(b.b) * 2
72+ if capacity < start+total {
73+ capacity = start + total
74+ }
75+ grown := make([]byte, start, capacity)
76+ copy(grown, b.b)
77+ b.b = grown
78+ }
79+ b.b = b.b[:start+total]
80+ p := b.b[start:]
81+ clear(p)
82+ p[0] = major
83+ p[1] = data
84+ if header == 4 {
85+ binary.LittleEndian.PutUint16(p[2:], uint16(total/4))
86+ } else {
87+ binary.LittleEndian.PutUint32(p[4:], uint32(total/4))
88+ }
89+ seq := b.next
90+ b.next++
91+ return p[header:], seq
92+}
A buf_test.go +69 -0
@@ -0,0 +1,69 @@
1+package xturbo
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "errors"
7+ "testing"
8+)
9+
10+func TestBufRequest(t *testing.T) {
11+ b := NewBuf(4)
12+ p, seq := b.Request(8, 2, 4)
13+ if seq != 1 || len(p) != 4 || !bytes.Equal(b.Bytes(), []byte{8, 2, 2, 0, 0, 0, 0, 0}) {
14+ t.Fatalf("%x %d", b.Bytes(), seq)
15+ }
16+ p[0] = 255
17+ b.Reset()
18+ p, seq = b.Request(14, 0, 4)
19+ if seq != 2 || p[0] != 0 || b.Next() != 3 || b.Len() != 8 {
20+ t.Fatal("reset sequence/zeroing")
21+ }
22+ first := errors.New("first")
23+ b.Fail(first)
24+ b.Fail(ErrBadLength)
25+ p, seq = b.Request(1, 0, 0)
26+ if p != nil || seq != 0 || b.Err() != first {
27+ t.Fatal("sticky error")
28+ }
29+}
30+func TestBufLimits(t *testing.T) {
31+ for _, body := range []int{-1, 1, 262140} {
32+ b := NewBuf(0)
33+ p, s := b.Request(1, 0, body)
34+ if p != nil || s != 0 || b.Err() == nil || b.Len() != 0 {
35+ t.Fatal(body)
36+ }
37+ }
38+ b := NewBuf(0)
39+ b.next = 0xffff
40+ if p, s := b.Request(1, 0, 0); p != nil || s != 0 || b.Err() != ErrSeqOverflow {
41+ t.Fatal("overflow")
42+ }
43+ b = NewBuf(0)
44+ b.maxReq = 1 << 20
45+ p, s := b.Request(1, 0, 262140)
46+ if s != 1 || len(p) != 262140 || binary.LittleEndian.Uint16(b.Bytes()[2:]) != 0 || binary.LittleEndian.Uint32(b.Bytes()[4:]) != 65537 {
47+ t.Fatal("BIG-REQUESTS header")
48+ }
49+}
50+func TestAllocBufRequest(t *testing.T) {
51+ for _, body := range []int{4, 65536} {
52+ b := NewBuf(128 << 10)
53+ if n := testing.AllocsPerRun(1000, func() { b.Reset(); b.Request(1, 0, body) }); n != 0 {
54+ t.Fatal(n)
55+ }
56+ if b.Err() != nil {
57+ t.Fatal(b.Err())
58+ }
59+ }
60+}
61+func BenchmarkBufRequest(b *testing.B) {
62+ buf := NewBuf(0)
63+ b.ReportAllocs()
64+ for b.Loop() {
65+ buf.Reset()
66+ buf.acked = buf.next
67+ buf.Request(1, 0, 4)
68+ }
69+}
@@ -0,0 +1,69 @@
1+package xturbo
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "errors"
7+ "testing"
8+)
9+
10+func TestBufRequest(t *testing.T) {
11+ b := NewBuf(4)
12+ p, seq := b.Request(8, 2, 4)
13+ if seq != 1 || len(p) != 4 || !bytes.Equal(b.Bytes(), []byte{8, 2, 2, 0, 0, 0, 0, 0}) {
14+ t.Fatalf("%x %d", b.Bytes(), seq)
15+ }
16+ p[0] = 255
17+ b.Reset()
18+ p, seq = b.Request(14, 0, 4)
19+ if seq != 2 || p[0] != 0 || b.Next() != 3 || b.Len() != 8 {
20+ t.Fatal("reset sequence/zeroing")
21+ }
22+ first := errors.New("first")
23+ b.Fail(first)
24+ b.Fail(ErrBadLength)
25+ p, seq = b.Request(1, 0, 0)
26+ if p != nil || seq != 0 || b.Err() != first {
27+ t.Fatal("sticky error")
28+ }
29+}
30+func TestBufLimits(t *testing.T) {
31+ for _, body := range []int{-1, 1, 262140} {
32+ b := NewBuf(0)
33+ p, s := b.Request(1, 0, body)
34+ if p != nil || s != 0 || b.Err() == nil || b.Len() != 0 {
35+ t.Fatal(body)
36+ }
37+ }
38+ b := NewBuf(0)
39+ b.next = 0xffff
40+ if p, s := b.Request(1, 0, 0); p != nil || s != 0 || b.Err() != ErrSeqOverflow {
41+ t.Fatal("overflow")
42+ }
43+ b = NewBuf(0)
44+ b.maxReq = 1 << 20
45+ p, s := b.Request(1, 0, 262140)
46+ if s != 1 || len(p) != 262140 || binary.LittleEndian.Uint16(b.Bytes()[2:]) != 0 || binary.LittleEndian.Uint32(b.Bytes()[4:]) != 65537 {
47+ t.Fatal("BIG-REQUESTS header")
48+ }
49+}
50+func TestAllocBufRequest(t *testing.T) {
51+ for _, body := range []int{4, 65536} {
52+ b := NewBuf(128 << 10)
53+ if n := testing.AllocsPerRun(1000, func() { b.Reset(); b.Request(1, 0, body) }); n != 0 {
54+ t.Fatal(n)
55+ }
56+ if b.Err() != nil {
57+ t.Fatal(b.Err())
58+ }
59+ }
60+}
61+func BenchmarkBufRequest(b *testing.B) {
62+ buf := NewBuf(0)
63+ b.ReportAllocs()
64+ for b.Loop() {
65+ buf.Reset()
66+ buf.acked = buf.next
67+ buf.Request(1, 0, 4)
68+ }
69+}
A cmd/xtgen/emit.go +252 -0
@@ -0,0 +1,252 @@
1+package main
2+
3+import (
4+ "bytes"
5+ "embed"
6+ "fmt"
7+ "go/format"
8+ "os"
9+ "path/filepath"
10+ "sort"
11+ "strings"
12+ "text/template"
13+)
14+
15+//go:embed templates/*.tmpl
16+var templates embed.FS
17+
18+func writeTemplate(path, name string, data any) error {
19+ t, err := template.ParseFS(templates, "templates/"+name+".tmpl")
20+ if err != nil {
21+ return err
22+ }
23+ var b bytes.Buffer
24+ if err = t.Execute(&b, data); err != nil {
25+ return err
26+ }
27+ code, err := format.Source(b.Bytes())
28+ if err != nil {
29+ return fmt.Errorf("format %s: %w\n%s", path, err, b.String())
30+ }
31+ if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
32+ return err
33+ }
34+ return os.WriteFile(path, code, 0644)
35+}
36+func putScalar(t *Type, src, dst string) string {
37+ if t.Base == "bool" {
38+ if t.Size == 1 {
39+ return fmt.Sprintf("if %s { %s[0]=1 } else { %s[0]=0 };", src, dst, dst)
40+ }
41+ return fmt.Sprintf("if %s { binary.LittleEndian.PutUint32(%s,1) } else { binary.LittleEndian.PutUint32(%s,0) };", src, dst, dst)
42+ }
43+ if t.Size == 1 {
44+ return fmt.Sprintf("%s[0]=byte(%s);", dst, src)
45+ }
46+ return fmt.Sprintf("binary.LittleEndian.PutUint%d(%s,uint%d(%s));", 8*t.Size, dst, 8*t.Size, src)
47+}
48+func getScalar(t *Type, goType, src string) string {
49+ value := src + "[0]"
50+ if t.Size > 1 {
51+ value = fmt.Sprintf("binary.LittleEndian.Uint%d(%s)", 8*t.Size, src)
52+ }
53+ if t.Base == "bool" {
54+ return "(" + value + "!=0)"
55+ }
56+ return goType + "(" + value + ")"
57+}
58+func isScalar(t *Type) bool { return t.Base != "" }
59+func typeDecl(t *Type) (string, error) {
60+ if isScalar(t) {
61+ return fmt.Sprintf("type %s %s", t.GoType, t.Base), nil
62+ }
63+ var b strings.Builder
64+ if t.Kind == "union" {
65+ fmt.Fprintf(&b, "type %s struct { raw []byte }; const %sSize=%d;\n", t.GoType, t.GoType, t.Size)
66+ fmt.Fprintf(&b, "func Get%s(p []byte) %s { return %s{raw:p[:%d]} };\n", t.GoType, t.GoType, t.GoType, t.Size)
67+ fmt.Fprintf(&b, "func (v %s) Put(p []byte) { copy(p[:%d],v.raw) };\n", t.GoType, t.Size)
68+ for _, f := range t.Fields {
69+ if f.Kind == "pad" {
70+ continue
71+ }
72+ if f.Kind == "list" {
73+ fmt.Fprintf(&b, "func (v %s) %s() (out %s) { for i:=range out { out[i]=%s }; return };\n", t.GoType, f.Name, f.GoType, getScalar(f.Type, qualified(t.Module, f.Type.Module, f.Type.GoType), fmt.Sprintf("v.raw[i*%d:]", f.Type.Size)))
74+ } else {
75+ fmt.Fprintf(&b, "func (v %s) %s() %s { return %s };\n", t.GoType, f.Name, f.GoType, getScalar(f.Type, f.GoType, "v.raw"))
76+ }
77+ }
78+ return b.String(), nil
79+ }
80+ fmt.Fprintf(&b, "type %s struct {\n", t.GoType)
81+ for _, f := range t.Fields {
82+ if f.Kind != "pad" {
83+ name := f.Name
84+ if name == "Size" || name == "Put" {
85+ name += "_"
86+ }
87+ fmt.Fprintf(&b, "%s %s\n", name, f.GoType)
88+ }
89+ }
90+ b.WriteString("}\n")
91+ fmt.Fprintf(&b, "const %sSize=%d;\n", t.GoType, t.Size)
92+ if t.Variable {
93+ fmt.Fprintf(&b, "func (v %s) Size() int { off:=0;", t.GoType)
94+ for _, f := range t.Fields {
95+ name := f.Name
96+ if name == "Size" || name == "Put" {
97+ name += "_"
98+ }
99+ if f.Align > 0 {
100+ fmt.Fprintf(&b, "off=(off+%d)&^%d;", f.Align-1, f.Align-1)
101+ } else if f.Kind == "list" && f.Count == 0 {
102+ if f.Type.Variable {
103+ fmt.Fprintf(&b, "for i:=range v.%s { off+=v.%s[i].Size() };", name, name)
104+ } else {
105+ fmt.Fprintf(&b, "off+=len(v.%s)*%d;", name, f.Type.Size)
106+ }
107+ } else {
108+ fmt.Fprintf(&b, "off+=%d;", f.Size)
109+ }
110+ }
111+ b.WriteString("return off };\n")
112+ }
113+ fmt.Fprintf(&b, "func (v %s) Put(p []byte) { ", t.GoType)
114+ if t.Variable {
115+ b.WriteString("off:=0;")
116+ } else {
117+ fmt.Fprintf(&b, "clear(p[:%d]);", t.Size)
118+ }
119+ for _, f := range t.Fields {
120+ name := f.Name
121+ if name == "Size" || name == "Put" {
122+ name += "_"
123+ }
124+ off := fmt.Sprint(f.Off)
125+ if t.Variable {
126+ off = "off"
127+ }
128+ if f.Kind == "pad" {
129+ if t.Variable {
130+ if f.Align > 0 {
131+ fmt.Fprintf(&b, "{ next:=(off+%d)&^%d; clear(p[off:next]); off=next };", f.Align-1, f.Align-1)
132+ } else {
133+ fmt.Fprintf(&b, "clear(p[off:off+%d]); off+=%d;", f.Size, f.Size)
134+ }
135+ }
136+ continue
137+ }
138+ src := "v." + name
139+ dst := "p[" + off + ":]"
140+ if f.Kind == "list" {
141+ fmt.Fprintf(&b, "for i:=range %s {", src)
142+ item := src + "[i]"
143+ target := fmt.Sprintf("p[%s+i*%d:]", off, f.Type.Size)
144+ if f.Type.Variable {
145+ target = "p[off:]"
146+ }
147+ if isScalar(f.Type) {
148+ b.WriteString(putScalar(f.Type, item, target))
149+ } else {
150+ fmt.Fprintf(&b, "%s.Put(%s);", item, target)
151+ }
152+ if f.Type.Variable {
153+ fmt.Fprintf(&b, "off+=%s.Size();", item)
154+ }
155+ b.WriteString("};")
156+ if t.Variable && !f.Type.Variable {
157+ fmt.Fprintf(&b, "off+=len(%s)*%d;", src, f.Type.Size)
158+ }
159+ } else {
160+ if isScalar(f.Type) {
161+ b.WriteString(putScalar(f.Type, src, dst))
162+ } else {
163+ fmt.Fprintf(&b, "%s.Put(%s);", src, dst)
164+ }
165+ if t.Variable {
166+ fmt.Fprintf(&b, "off+=%d;", f.Size)
167+ }
168+ }
169+ }
170+ b.WriteString("}\n")
171+ if !t.Variable {
172+ fmt.Fprintf(&b, "func Get%s(p []byte) (v %s) { _=p[%d];", t.GoType, t.GoType, t.Size-1)
173+ for _, f := range t.Fields {
174+ if f.Kind == "pad" {
175+ continue
176+ }
177+ dst := "v." + f.Name
178+ src := fmt.Sprintf("p[%d:]", f.Off)
179+ if f.Kind == "list" {
180+ fmt.Fprintf(&b, "for i:=range %s { %s[i]=%s };", dst, dst, getScalar(f.Type, qualified(t.Module, f.Type.Module, f.Type.GoType), fmt.Sprintf("p[%d+i*%d:]", f.Off, f.Type.Size)))
181+ } else if isScalar(f.Type) {
182+ fmt.Fprintf(&b, "%s=%s;", dst, getScalar(f.Type, f.GoType, src))
183+ } else {
184+ fmt.Fprintf(&b, "%s=%s(%s);", dst, qualified(t.Module, f.Type.Module, "Get"+f.Type.GoType), src)
185+ }
186+ }
187+ b.WriteString("return };\n")
188+ }
189+ return b.String(), nil
190+}
191+func importsFor(m *Module, decls string) string {
192+ paths := map[string]string{"binary.": "encoding/binary", "bits.": "math/bits"}
193+ for _, dep := range m.Imports {
194+ paths[dep.Pkg+"."] = "bnema.dev/xturbo/" + dep.Pkg
195+ }
196+ var imports []string
197+ for prefix, path := range paths {
198+ if strings.Contains(decls, prefix) {
199+ imports = append(imports, fmt.Sprintf("import %q", path))
200+ }
201+ }
202+ sort.Strings(imports)
203+ return strings.Join(imports, "\n")
204+}
205+func emitTypes(modules []*Module) error {
206+ for _, m := range modules {
207+ for _, t := range m.Types {
208+ decl, err := typeDecl(t)
209+ if err != nil {
210+ return err
211+ }
212+ m.Decls = append(m.Decls, decl)
213+ }
214+ for _, e := range m.Enums {
215+ var b strings.Builder
216+ if !e.Existing {
217+ fmt.Fprintf(&b, "type %s %s;\n", e.Name, e.Base)
218+ }
219+ for _, item := range e.Items {
220+ fmt.Fprintf(&b, "const %s %s = %d;\n", item.Name, e.Name, item.Value)
221+ }
222+ m.Decls = append(m.Decls, b.String())
223+ }
224+ imp := importsFor(m, strings.Join(m.Decls, "\n"))
225+ m.Decls = append([]string{imp}, m.Decls...)
226+ if err := writeTemplate(filepath.Join(m.Pkg, "types_generated.go"), "types", m); err != nil {
227+ return err
228+ }
229+ for _, t := range m.Types {
230+ if t.Kind == "struct" || t.Kind == "union" {
231+ m.SizeChecks = append(m.SizeChecks, fmt.Sprintf("if %sSize != %d { t.Errorf(%q, %sSize) }", t.GoType, t.Size, t.GoType+" fixed prefix = %d", t.GoType))
232+ }
233+ }
234+ if err := packetSizeChecks(m); err != nil {
235+ return err
236+ }
237+ if err := writeTemplate(filepath.Join(m.Pkg, "sizes_test.go"), "sizes", m); err != nil {
238+ return err
239+ }
240+ }
241+ var extensions []*Module
242+ for _, m := range modules {
243+ if m.ExtName != "" {
244+ extensions = append(extensions, m)
245+ }
246+ }
247+ return writeTemplate("ext_generated.go", "ext", extensions)
248+}
249+
250+func sizeCheck(name string, got, want int) string {
251+ return fmt.Sprintf("if got:= %d; got != %d { t.Errorf(%q,got) }", got, want, name+" fixed size = %d")
252+}
@@ -0,0 +1,252 @@
1+package main
2+
3+import (
4+ "bytes"
5+ "embed"
6+ "fmt"
7+ "go/format"
8+ "os"
9+ "path/filepath"
10+ "sort"
11+ "strings"
12+ "text/template"
13+)
14+
15+//go:embed templates/*.tmpl
16+var templates embed.FS
17+
18+func writeTemplate(path, name string, data any) error {
19+ t, err := template.ParseFS(templates, "templates/"+name+".tmpl")
20+ if err != nil {
21+ return err
22+ }
23+ var b bytes.Buffer
24+ if err = t.Execute(&b, data); err != nil {
25+ return err
26+ }
27+ code, err := format.Source(b.Bytes())
28+ if err != nil {
29+ return fmt.Errorf("format %s: %w\n%s", path, err, b.String())
30+ }
31+ if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
32+ return err
33+ }
34+ return os.WriteFile(path, code, 0644)
35+}
36+func putScalar(t *Type, src, dst string) string {
37+ if t.Base == "bool" {
38+ if t.Size == 1 {
39+ return fmt.Sprintf("if %s { %s[0]=1 } else { %s[0]=0 };", src, dst, dst)
40+ }
41+ return fmt.Sprintf("if %s { binary.LittleEndian.PutUint32(%s,1) } else { binary.LittleEndian.PutUint32(%s,0) };", src, dst, dst)
42+ }
43+ if t.Size == 1 {
44+ return fmt.Sprintf("%s[0]=byte(%s);", dst, src)
45+ }
46+ return fmt.Sprintf("binary.LittleEndian.PutUint%d(%s,uint%d(%s));", 8*t.Size, dst, 8*t.Size, src)
47+}
48+func getScalar(t *Type, goType, src string) string {
49+ value := src + "[0]"
50+ if t.Size > 1 {
51+ value = fmt.Sprintf("binary.LittleEndian.Uint%d(%s)", 8*t.Size, src)
52+ }
53+ if t.Base == "bool" {
54+ return "(" + value + "!=0)"
55+ }
56+ return goType + "(" + value + ")"
57+}
58+func isScalar(t *Type) bool { return t.Base != "" }
59+func typeDecl(t *Type) (string, error) {
60+ if isScalar(t) {
61+ return fmt.Sprintf("type %s %s", t.GoType, t.Base), nil
62+ }
63+ var b strings.Builder
64+ if t.Kind == "union" {
65+ fmt.Fprintf(&b, "type %s struct { raw []byte }; const %sSize=%d;\n", t.GoType, t.GoType, t.Size)
66+ fmt.Fprintf(&b, "func Get%s(p []byte) %s { return %s{raw:p[:%d]} };\n", t.GoType, t.GoType, t.GoType, t.Size)
67+ fmt.Fprintf(&b, "func (v %s) Put(p []byte) { copy(p[:%d],v.raw) };\n", t.GoType, t.Size)
68+ for _, f := range t.Fields {
69+ if f.Kind == "pad" {
70+ continue
71+ }
72+ if f.Kind == "list" {
73+ fmt.Fprintf(&b, "func (v %s) %s() (out %s) { for i:=range out { out[i]=%s }; return };\n", t.GoType, f.Name, f.GoType, getScalar(f.Type, qualified(t.Module, f.Type.Module, f.Type.GoType), fmt.Sprintf("v.raw[i*%d:]", f.Type.Size)))
74+ } else {
75+ fmt.Fprintf(&b, "func (v %s) %s() %s { return %s };\n", t.GoType, f.Name, f.GoType, getScalar(f.Type, f.GoType, "v.raw"))
76+ }
77+ }
78+ return b.String(), nil
79+ }
80+ fmt.Fprintf(&b, "type %s struct {\n", t.GoType)
81+ for _, f := range t.Fields {
82+ if f.Kind != "pad" {
83+ name := f.Name
84+ if name == "Size" || name == "Put" {
85+ name += "_"
86+ }
87+ fmt.Fprintf(&b, "%s %s\n", name, f.GoType)
88+ }
89+ }
90+ b.WriteString("}\n")
91+ fmt.Fprintf(&b, "const %sSize=%d;\n", t.GoType, t.Size)
92+ if t.Variable {
93+ fmt.Fprintf(&b, "func (v %s) Size() int { off:=0;", t.GoType)
94+ for _, f := range t.Fields {
95+ name := f.Name
96+ if name == "Size" || name == "Put" {
97+ name += "_"
98+ }
99+ if f.Align > 0 {
100+ fmt.Fprintf(&b, "off=(off+%d)&^%d;", f.Align-1, f.Align-1)
101+ } else if f.Kind == "list" && f.Count == 0 {
102+ if f.Type.Variable {
103+ fmt.Fprintf(&b, "for i:=range v.%s { off+=v.%s[i].Size() };", name, name)
104+ } else {
105+ fmt.Fprintf(&b, "off+=len(v.%s)*%d;", name, f.Type.Size)
106+ }
107+ } else {
108+ fmt.Fprintf(&b, "off+=%d;", f.Size)
109+ }
110+ }
111+ b.WriteString("return off };\n")
112+ }
113+ fmt.Fprintf(&b, "func (v %s) Put(p []byte) { ", t.GoType)
114+ if t.Variable {
115+ b.WriteString("off:=0;")
116+ } else {
117+ fmt.Fprintf(&b, "clear(p[:%d]);", t.Size)
118+ }
119+ for _, f := range t.Fields {
120+ name := f.Name
121+ if name == "Size" || name == "Put" {
122+ name += "_"
123+ }
124+ off := fmt.Sprint(f.Off)
125+ if t.Variable {
126+ off = "off"
127+ }
128+ if f.Kind == "pad" {
129+ if t.Variable {
130+ if f.Align > 0 {
131+ fmt.Fprintf(&b, "{ next:=(off+%d)&^%d; clear(p[off:next]); off=next };", f.Align-1, f.Align-1)
132+ } else {
133+ fmt.Fprintf(&b, "clear(p[off:off+%d]); off+=%d;", f.Size, f.Size)
134+ }
135+ }
136+ continue
137+ }
138+ src := "v." + name
139+ dst := "p[" + off + ":]"
140+ if f.Kind == "list" {
141+ fmt.Fprintf(&b, "for i:=range %s {", src)
142+ item := src + "[i]"
143+ target := fmt.Sprintf("p[%s+i*%d:]", off, f.Type.Size)
144+ if f.Type.Variable {
145+ target = "p[off:]"
146+ }
147+ if isScalar(f.Type) {
148+ b.WriteString(putScalar(f.Type, item, target))
149+ } else {
150+ fmt.Fprintf(&b, "%s.Put(%s);", item, target)
151+ }
152+ if f.Type.Variable {
153+ fmt.Fprintf(&b, "off+=%s.Size();", item)
154+ }
155+ b.WriteString("};")
156+ if t.Variable && !f.Type.Variable {
157+ fmt.Fprintf(&b, "off+=len(%s)*%d;", src, f.Type.Size)
158+ }
159+ } else {
160+ if isScalar(f.Type) {
161+ b.WriteString(putScalar(f.Type, src, dst))
162+ } else {
163+ fmt.Fprintf(&b, "%s.Put(%s);", src, dst)
164+ }
165+ if t.Variable {
166+ fmt.Fprintf(&b, "off+=%d;", f.Size)
167+ }
168+ }
169+ }
170+ b.WriteString("}\n")
171+ if !t.Variable {
172+ fmt.Fprintf(&b, "func Get%s(p []byte) (v %s) { _=p[%d];", t.GoType, t.GoType, t.Size-1)
173+ for _, f := range t.Fields {
174+ if f.Kind == "pad" {
175+ continue
176+ }
177+ dst := "v." + f.Name
178+ src := fmt.Sprintf("p[%d:]", f.Off)
179+ if f.Kind == "list" {
180+ fmt.Fprintf(&b, "for i:=range %s { %s[i]=%s };", dst, dst, getScalar(f.Type, qualified(t.Module, f.Type.Module, f.Type.GoType), fmt.Sprintf("p[%d+i*%d:]", f.Off, f.Type.Size)))
181+ } else if isScalar(f.Type) {
182+ fmt.Fprintf(&b, "%s=%s;", dst, getScalar(f.Type, f.GoType, src))
183+ } else {
184+ fmt.Fprintf(&b, "%s=%s(%s);", dst, qualified(t.Module, f.Type.Module, "Get"+f.Type.GoType), src)
185+ }
186+ }
187+ b.WriteString("return };\n")
188+ }
189+ return b.String(), nil
190+}
191+func importsFor(m *Module, decls string) string {
192+ paths := map[string]string{"binary.": "encoding/binary", "bits.": "math/bits"}
193+ for _, dep := range m.Imports {
194+ paths[dep.Pkg+"."] = "bnema.dev/xturbo/" + dep.Pkg
195+ }
196+ var imports []string
197+ for prefix, path := range paths {
198+ if strings.Contains(decls, prefix) {
199+ imports = append(imports, fmt.Sprintf("import %q", path))
200+ }
201+ }
202+ sort.Strings(imports)
203+ return strings.Join(imports, "\n")
204+}
205+func emitTypes(modules []*Module) error {
206+ for _, m := range modules {
207+ for _, t := range m.Types {
208+ decl, err := typeDecl(t)
209+ if err != nil {
210+ return err
211+ }
212+ m.Decls = append(m.Decls, decl)
213+ }
214+ for _, e := range m.Enums {
215+ var b strings.Builder
216+ if !e.Existing {
217+ fmt.Fprintf(&b, "type %s %s;\n", e.Name, e.Base)
218+ }
219+ for _, item := range e.Items {
220+ fmt.Fprintf(&b, "const %s %s = %d;\n", item.Name, e.Name, item.Value)
221+ }
222+ m.Decls = append(m.Decls, b.String())
223+ }
224+ imp := importsFor(m, strings.Join(m.Decls, "\n"))
225+ m.Decls = append([]string{imp}, m.Decls...)
226+ if err := writeTemplate(filepath.Join(m.Pkg, "types_generated.go"), "types", m); err != nil {
227+ return err
228+ }
229+ for _, t := range m.Types {
230+ if t.Kind == "struct" || t.Kind == "union" {
231+ m.SizeChecks = append(m.SizeChecks, fmt.Sprintf("if %sSize != %d { t.Errorf(%q, %sSize) }", t.GoType, t.Size, t.GoType+" fixed prefix = %d", t.GoType))
232+ }
233+ }
234+ if err := packetSizeChecks(m); err != nil {
235+ return err
236+ }
237+ if err := writeTemplate(filepath.Join(m.Pkg, "sizes_test.go"), "sizes", m); err != nil {
238+ return err
239+ }
240+ }
241+ var extensions []*Module
242+ for _, m := range modules {
243+ if m.ExtName != "" {
244+ extensions = append(extensions, m)
245+ }
246+ }
247+ return writeTemplate("ext_generated.go", "ext", extensions)
248+}
249+
250+func sizeCheck(name string, got, want int) string {
251+ return fmt.Sprintf("if got:= %d; got != %d { t.Errorf(%q,got) }", got, want, name+" fixed size = %d")
252+}
A cmd/xtgen/expr.go +43 -0
@@ -0,0 +1,43 @@
1+package main
2+
3+import "fmt"
4+
5+// renderExpr centralizes expression lowering; callers supply parameter, owned
6+// field, or getter references. No expression decisions are left to templates.
7+func renderExpr(n *Node, ref func(string) string, m *Module) (string, error) {
8+ if n == nil {
9+ return "", nil
10+ }
11+ switch n.Kind {
12+ case "value":
13+ return n.Text, nil
14+ case "fieldref":
15+ return ref(n.Text), nil
16+ case "enumref":
17+ e, owner := findEnum(m, n.Attr["ref"])
18+ if e == nil {
19+ return "", n.fail("unknown enum %s", n.Attr["ref"])
20+ }
21+ return "int(" + qualified(m, owner, e.Name+goName(n.Text)) + ")", nil
22+ case "op", "unop", "popcount":
23+ var parts []string
24+ for _, c := range n.Children {
25+ s, err := renderExpr(c, ref, m)
26+ if err != nil {
27+ return "", err
28+ }
29+ parts = append(parts, s)
30+ }
31+ if n.Kind == "popcount" {
32+ return "bits.OnesCount(uint(" + parts[0] + "))", nil
33+ }
34+ if n.Kind == "unop" {
35+ return "(^" + parts[0] + ")", nil
36+ }
37+ if len(parts) != 2 {
38+ return "", n.fail("invalid binary expression")
39+ }
40+ return "(" + parts[0] + " " + n.Attr["op"] + " " + parts[1] + ")", nil
41+ }
42+ return "", fmt.Errorf("%s:%d: unsupported expression %s", n.File, n.Line, n.Kind)
43+}
@@ -0,0 +1,43 @@
1+package main
2+
3+import "fmt"
4+
5+// renderExpr centralizes expression lowering; callers supply parameter, owned
6+// field, or getter references. No expression decisions are left to templates.
7+func renderExpr(n *Node, ref func(string) string, m *Module) (string, error) {
8+ if n == nil {
9+ return "", nil
10+ }
11+ switch n.Kind {
12+ case "value":
13+ return n.Text, nil
14+ case "fieldref":
15+ return ref(n.Text), nil
16+ case "enumref":
17+ e, owner := findEnum(m, n.Attr["ref"])
18+ if e == nil {
19+ return "", n.fail("unknown enum %s", n.Attr["ref"])
20+ }
21+ return "int(" + qualified(m, owner, e.Name+goName(n.Text)) + ")", nil
22+ case "op", "unop", "popcount":
23+ var parts []string
24+ for _, c := range n.Children {
25+ s, err := renderExpr(c, ref, m)
26+ if err != nil {
27+ return "", err
28+ }
29+ parts = append(parts, s)
30+ }
31+ if n.Kind == "popcount" {
32+ return "bits.OnesCount(uint(" + parts[0] + "))", nil
33+ }
34+ if n.Kind == "unop" {
35+ return "(^" + parts[0] + ")", nil
36+ }
37+ if len(parts) != 2 {
38+ return "", n.fail("invalid binary expression")
39+ }
40+ return "(" + parts[0] + " " + n.Attr["op"] + " " + parts[1] + ")", nil
41+ }
42+ return "", fmt.Errorf("%s:%d: unsupported expression %s", n.File, n.Line, n.Kind)
43+}
A cmd/xtgen/ir.go +39 -0
@@ -0,0 +1,39 @@
1+package main
2+
3+// Type is a resolved wire type. Size is the fixed prefix, not Go's memory size.
4+type Type struct {
5+ Name, GoType, Base, Kind string
6+ Size int
7+ Variable bool
8+ Node *Node
9+ Module *Module
10+ Fields []Field
11+ Resolving, Resolved bool
12+}
13+type Field struct {
14+ Name, Param, GoType, Kind string
15+ Off, Size, Count, Align int
16+ Type *Type
17+ Node *Node
18+ Expr *Node
19+}
20+type Enum struct {
21+ Name, Base string
22+ Existing bool
23+ Items []EnumItem
24+ Node *Node
25+}
26+type EnumItem struct {
27+ Name string
28+ Value int64
29+}
30+type Module struct {
31+ Pkg, Header, XName, ExtName string
32+ Root *Node
33+ Imports []*Module
34+ Types []*Type
35+ Enums []Enum
36+ Symbols map[string]*Type
37+ Names map[string]*Node
38+ Decls, SizeChecks []string
39+}
@@ -0,0 +1,39 @@
1+package main
2+
3+// Type is a resolved wire type. Size is the fixed prefix, not Go's memory size.
4+type Type struct {
5+ Name, GoType, Base, Kind string
6+ Size int
7+ Variable bool
8+ Node *Node
9+ Module *Module
10+ Fields []Field
11+ Resolving, Resolved bool
12+}
13+type Field struct {
14+ Name, Param, GoType, Kind string
15+ Off, Size, Count, Align int
16+ Type *Type
17+ Node *Node
18+ Expr *Node
19+}
20+type Enum struct {
21+ Name, Base string
22+ Existing bool
23+ Items []EnumItem
24+ Node *Node
25+}
26+type EnumItem struct {
27+ Name string
28+ Value int64
29+}
30+type Module struct {
31+ Pkg, Header, XName, ExtName string
32+ Root *Node
33+ Imports []*Module
34+ Types []*Type
35+ Enums []Enum
36+ Symbols map[string]*Type
37+ Names map[string]*Node
38+ Decls, SizeChecks []string
39+}
A cmd/xtgen/layout.go +103 -0
@@ -0,0 +1,103 @@
1+package main
2+
3+import "strconv"
4+
5+// packetFixed computes the minimum complete packet size. The first XML byte
6+// occupies byte 1; the implicit sequence/length header follows that byte.
7+func packetFixed(m *Module, n *Node, reply bool) (int, error) {
8+ off := 1
9+ if n.Attr["no-sequence-number"] == "true" {
10+ off = 1
11+ }
12+ for _, f := range n.Children {
13+ if f.Kind == "doc" {
14+ continue
15+ }
16+ size := 0
17+ switch f.Kind {
18+ case "pad":
19+ if a := f.Attr["align"]; a != "" {
20+ align, _ := strconv.Atoi(a)
21+ size = (-off) & (align - 1)
22+ } else {
23+ size, _ = strconv.Atoi(f.Attr["bytes"])
24+ }
25+ case "field", "exprfield":
26+ typ, err := lookup(m, f.Attr["type"])
27+ if err != nil {
28+ return 0, err
29+ }
30+ size = typ.Size
31+ case "list":
32+ if len(f.Children) == 1 && f.Children[0].Kind == "value" {
33+ count, _ := strconv.Atoi(f.Children[0].Text)
34+ typ, err := lookup(m, f.Attr["type"])
35+ if err != nil {
36+ return 0, err
37+ }
38+ size = count * typ.Size
39+ } else {
40+ goto done
41+ }
42+ default:
43+ return 0, f.fail("unsupported packet member %s", f.Kind)
44+ }
45+ off += size
46+ if off == 2 && n.Attr["no-sequence-number"] != "true" {
47+ if reply {
48+ off = 8
49+ } else {
50+ off = 4
51+ }
52+ }
53+ }
54+done:
55+ if off < 32 {
56+ off = 32
57+ }
58+ return off, nil
59+}
60+func packetSizeChecks(m *Module) error {
61+ for _, n := range m.Root.Children {
62+ if n.Kind == "event" || n.Kind == "eventcopy" {
63+ size := 32
64+ if n.Kind == "event" {
65+ var err error
66+ size, err = packetFixed(m, n, false)
67+ if err != nil {
68+ return err
69+ }
70+ }
71+ if size != 32 {
72+ return n.fail("event fixed size %d, expected 32", size)
73+ }
74+ m.SizeChecks = append(m.SizeChecks, sizeCheck("event "+n.Attr["name"], size, 32))
75+ }
76+ if n.Kind == "request" {
77+ for _, r := range n.Children {
78+ if r.Kind != "reply" {
79+ continue
80+ }
81+ size, err := packetFixed(m, r, true)
82+ if err != nil {
83+ return err
84+ }
85+ expected := 32
86+ if m.Header == "xproto" {
87+ switch n.Attr["name"] {
88+ case "QueryKeymap":
89+ expected = 40
90+ case "GetKeyboardControl":
91+ expected = 52
92+ case "GetWindowAttributes":
93+ expected = 44
94+ case "QueryFont", "ListFontsWithInfo":
95+ expected = 60
96+ }
97+ }
98+ m.SizeChecks = append(m.SizeChecks, sizeCheck("reply "+n.Attr["name"], size, expected))
99+ }
100+ }
101+ }
102+ return nil
103+}
@@ -0,0 +1,103 @@
1+package main
2+
3+import "strconv"
4+
5+// packetFixed computes the minimum complete packet size. The first XML byte
6+// occupies byte 1; the implicit sequence/length header follows that byte.
7+func packetFixed(m *Module, n *Node, reply bool) (int, error) {
8+ off := 1
9+ if n.Attr["no-sequence-number"] == "true" {
10+ off = 1
11+ }
12+ for _, f := range n.Children {
13+ if f.Kind == "doc" {
14+ continue
15+ }
16+ size := 0
17+ switch f.Kind {
18+ case "pad":
19+ if a := f.Attr["align"]; a != "" {
20+ align, _ := strconv.Atoi(a)
21+ size = (-off) & (align - 1)
22+ } else {
23+ size, _ = strconv.Atoi(f.Attr["bytes"])
24+ }
25+ case "field", "exprfield":
26+ typ, err := lookup(m, f.Attr["type"])
27+ if err != nil {
28+ return 0, err
29+ }
30+ size = typ.Size
31+ case "list":
32+ if len(f.Children) == 1 && f.Children[0].Kind == "value" {
33+ count, _ := strconv.Atoi(f.Children[0].Text)
34+ typ, err := lookup(m, f.Attr["type"])
35+ if err != nil {
36+ return 0, err
37+ }
38+ size = count * typ.Size
39+ } else {
40+ goto done
41+ }
42+ default:
43+ return 0, f.fail("unsupported packet member %s", f.Kind)
44+ }
45+ off += size
46+ if off == 2 && n.Attr["no-sequence-number"] != "true" {
47+ if reply {
48+ off = 8
49+ } else {
50+ off = 4
51+ }
52+ }
53+ }
54+done:
55+ if off < 32 {
56+ off = 32
57+ }
58+ return off, nil
59+}
60+func packetSizeChecks(m *Module) error {
61+ for _, n := range m.Root.Children {
62+ if n.Kind == "event" || n.Kind == "eventcopy" {
63+ size := 32
64+ if n.Kind == "event" {
65+ var err error
66+ size, err = packetFixed(m, n, false)
67+ if err != nil {
68+ return err
69+ }
70+ }
71+ if size != 32 {
72+ return n.fail("event fixed size %d, expected 32", size)
73+ }
74+ m.SizeChecks = append(m.SizeChecks, sizeCheck("event "+n.Attr["name"], size, 32))
75+ }
76+ if n.Kind == "request" {
77+ for _, r := range n.Children {
78+ if r.Kind != "reply" {
79+ continue
80+ }
81+ size, err := packetFixed(m, r, true)
82+ if err != nil {
83+ return err
84+ }
85+ expected := 32
86+ if m.Header == "xproto" {
87+ switch n.Attr["name"] {
88+ case "QueryKeymap":
89+ expected = 40
90+ case "GetKeyboardControl":
91+ expected = 52
92+ case "GetWindowAttributes":
93+ expected = 44
94+ case "QueryFont", "ListFontsWithInfo":
95+ expected = 60
96+ }
97+ }
98+ m.SizeChecks = append(m.SizeChecks, sizeCheck("reply "+n.Attr["name"], size, expected))
99+ }
100+ }
101+ }
102+ return nil
103+}
M cmd/xtgen/main.go +47 -1
@@ -1,4 +1,50 @@
11 // Command xtgen generates bindings from the pinned xcb-proto XML.
22 package main
33
4-func main() {}
4+import (
5+ "flag"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+)
10+
11+func main() {
12+ all := flag.Bool("all", false, "process all vendored modules")
13+ flag.Parse()
14+ paths := flag.Args()
15+ if *all {
16+ var err error
17+ paths, err = filepath.Glob("spec/xml/*.xml")
18+ if err != nil {
19+ fmt.Fprintln(os.Stderr, err)
20+ os.Exit(1)
21+ }
22+ }
23+ if len(paths) == 0 {
24+ fmt.Fprintln(os.Stderr, "xtgen: supply XML paths or -all")
25+ os.Exit(1)
26+ }
27+ if *all {
28+ modules, err := loadModules()
29+ if err == nil {
30+ err = emitTypes(modules)
31+ }
32+ if err == nil {
33+ err = emitRequests(modules)
34+ }
35+ if err == nil {
36+ err = emitReplies(modules)
37+ }
38+ if err != nil {
39+ fmt.Fprintln(os.Stderr, err)
40+ os.Exit(1)
41+ }
42+ return
43+ }
44+ for _, path := range paths {
45+ if _, err := parseFile(path); err != nil {
46+ fmt.Fprintln(os.Stderr, err)
47+ os.Exit(1)
48+ }
49+ }
50+}
@@ -1,4 +1,50 @@
1 // Command xtgen generates bindings from the pinned xcb-proto XML.1 // Command xtgen generates bindings from the pinned xcb-proto XML.
2 package main2 package main
3 3
4-func main() {}4+import (
5+ "flag"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+)
10+
11+func main() {
12+ all := flag.Bool("all", false, "process all vendored modules")
13+ flag.Parse()
14+ paths := flag.Args()
15+ if *all {
16+ var err error
17+ paths, err = filepath.Glob("spec/xml/*.xml")
18+ if err != nil {
19+ fmt.Fprintln(os.Stderr, err)
20+ os.Exit(1)
21+ }
22+ }
23+ if len(paths) == 0 {
24+ fmt.Fprintln(os.Stderr, "xtgen: supply XML paths or -all")
25+ os.Exit(1)
26+ }
27+ if *all {
28+ modules, err := loadModules()
29+ if err == nil {
30+ err = emitTypes(modules)
31+ }
32+ if err == nil {
33+ err = emitRequests(modules)
34+ }
35+ if err == nil {
36+ err = emitReplies(modules)
37+ }
38+ if err != nil {
39+ fmt.Fprintln(os.Stderr, err)
40+ os.Exit(1)
41+ }
42+ return
43+ }
44+ for _, path := range paths {
45+ if _, err := parseFile(path); err != nil {
46+ fmt.Fprintln(os.Stderr, err)
47+ os.Exit(1)
48+ }
49+ }
50+}
A cmd/xtgen/naming.go +43 -0
@@ -0,0 +1,43 @@
1+package main
2+
3+import (
4+ "go/token"
5+ "strings"
6+ "unicode"
7+)
8+
9+func goName(s string) string {
10+ var out strings.Builder
11+ for _, part := range strings.Split(s, "_") {
12+ if part == "" {
13+ continue
14+ }
15+ if part == strings.ToUpper(part) {
16+ part = strings.ToLower(part)
17+ }
18+ r := []rune(part)
19+ r[0] = unicode.ToUpper(r[0])
20+ out.WriteString(string(r))
21+ }
22+ name := out.String()
23+ if name != "" && name[0] >= '0' && name[0] <= '9' {
24+ name = "X" + name
25+ }
26+ if token.Lookup(name).IsKeyword() {
27+ name += "_"
28+ }
29+ return name
30+}
31+func paramName(s string) string {
32+ name := goName(s)
33+ if name == "" {
34+ return name
35+ }
36+ r := []rune(name)
37+ r[0] = unicode.ToLower(r[0])
38+ name = string(r)
39+ if token.Lookup(name).IsKeyword() {
40+ name += "_"
41+ }
42+ return name
43+}
@@ -0,0 +1,43 @@
1+package main
2+
3+import (
4+ "go/token"
5+ "strings"
6+ "unicode"
7+)
8+
9+func goName(s string) string {
10+ var out strings.Builder
11+ for _, part := range strings.Split(s, "_") {
12+ if part == "" {
13+ continue
14+ }
15+ if part == strings.ToUpper(part) {
16+ part = strings.ToLower(part)
17+ }
18+ r := []rune(part)
19+ r[0] = unicode.ToUpper(r[0])
20+ out.WriteString(string(r))
21+ }
22+ name := out.String()
23+ if name != "" && name[0] >= '0' && name[0] <= '9' {
24+ name = "X" + name
25+ }
26+ if token.Lookup(name).IsKeyword() {
27+ name += "_"
28+ }
29+ return name
30+}
31+func paramName(s string) string {
32+ name := goName(s)
33+ if name == "" {
34+ return name
35+ }
36+ r := []rune(name)
37+ r[0] = unicode.ToLower(r[0])
38+ name = string(r)
39+ if token.Lookup(name).IsKeyword() {
40+ name += "_"
41+ }
42+ return name
43+}
A cmd/xtgen/parse.go +292 -0
@@ -0,0 +1,292 @@
1+package main
2+
3+import (
4+ "encoding/xml"
5+ "fmt"
6+ "io"
7+ "os"
8+ "strconv"
9+ "strings"
10+)
11+
12+// Node retains XML order and source locations, including expressions. Resolution
13+// turns this syntax tree into the flat, typed emission IR; parsing makes no Go
14+// naming or layout choices.
15+type Node struct {
16+ Kind string
17+ Attr map[string]string
18+ Text string
19+ Children []*Node
20+ File string
21+ Line int
22+}
23+
24+func (n *Node) fail(format string, args ...any) error {
25+ return fmt.Errorf("%s:%d: %s", n.File, n.Line, fmt.Sprintf(format, args...))
26+}
27+
28+// Attribute and child whitelists are per element, not global: a known attribute
29+// or element used in the wrong place is also an error. Documentation is ignored.
30+var attributes = map[string]string{
31+ "xcb": "header extension-name extension-xname extension-multiword major-version minor-version",
32+ "import": "", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", "type": "",
33+ "enum": "name", "item": "name", "value": "", "bit": "", "struct": "name", "union": "name",
34+ "request": "name opcode combine-adjacent", "reply": "", "event": "name number no-sequence-number xge",
35+ "eventcopy": "name number ref", "error": "name number type", "errorcopy": "name number ref",
36+ "field": "name type enum altenum mask", "pad": "bytes align", "list": "name type enum",
37+ "exprfield": "name type", "switch": "name", "bitcase": "", "fieldref": "", "enumref": "ref",
38+ "op": "op", "unop": "op", "popcount": "", "doc": "",
39+}
40+var children = map[string]string{
41+ "xcb": "import typedef xidtype xidunion enum struct union request event eventcopy error errorcopy doc",
42+ "xidunion": "type", "enum": "item doc", "item": "value bit doc",
43+ "struct": "field pad list doc", "union": "field pad list doc",
44+ "request": "field pad list exprfield switch reply doc", "reply": "field pad list exprfield doc",
45+ "event": "field pad list exprfield doc", "error": "field pad doc",
46+ "list": "value fieldref op unop popcount enumref", "exprfield": "value fieldref op unop popcount enumref",
47+ "switch": "fieldref op unop popcount enumref bitcase", "bitcase": "enumref value field pad doc",
48+ "op": "value fieldref op unop popcount enumref", "unop": "value fieldref op unop popcount enumref",
49+ "popcount": "value fieldref op unop popcount enumref",
50+}
51+
52+func contains(words, word string) bool {
53+ for _, s := range strings.Fields(words) {
54+ if s == word {
55+ return true
56+ }
57+ }
58+ return false
59+}
60+
61+func parseFile(path string) (*Node, error) {
62+ f, err := os.Open(path)
63+ if err != nil {
64+ return nil, err
65+ }
66+ defer f.Close()
67+ return parseXML(path, f)
68+}
69+func parseXML(file string, r io.Reader) (*Node, error) {
70+ d := xml.NewDecoder(r)
71+ var root *Node
72+ var stack []*Node
73+ for {
74+ token, err := d.Token()
75+ if err == io.EOF {
76+ break
77+ }
78+ if err != nil {
79+ line, _ := d.InputPos()
80+ return nil, fmt.Errorf("%s:%d: %w", file, line, err)
81+ }
82+ switch t := token.(type) {
83+ case xml.StartElement:
84+ line, _ := d.InputPos()
85+ n := &Node{Kind: t.Name.Local, Attr: make(map[string]string), File: file, Line: line}
86+ allowed, ok := attributes[n.Kind]
87+ if !ok || t.Name.Space != "" {
88+ return nil, n.fail("unsupported element <%s>", t.Name.Local)
89+ }
90+ if len(stack) == 0 {
91+ if root != nil || n.Kind != "xcb" {
92+ return nil, n.fail("expected a single <xcb> root")
93+ }
94+ root = n
95+ } else {
96+ parent := stack[len(stack)-1]
97+ if !contains(children[parent.Kind], n.Kind) {
98+ return nil, n.fail("unsupported <%s> in <%s>", n.Kind, parent.Kind)
99+ }
100+ parent.Children = append(parent.Children, n)
101+ }
102+ if n.Kind == "doc" {
103+ if err := d.Skip(); err != nil {
104+ return nil, n.fail("invalid doc: %v", err)
105+ }
106+ continue
107+ }
108+ for _, a := range t.Attr {
109+ if a.Name.Space != "" || !contains(allowed, a.Name.Local) {
110+ return nil, n.fail("unknown attribute %q on <%s>", a.Name.Local, n.Kind)
111+ }
112+ if _, ok := n.Attr[a.Name.Local]; ok {
113+ return nil, n.fail("duplicate attribute %q", a.Name.Local)
114+ }
115+ n.Attr[a.Name.Local] = a.Value
116+ }
117+ stack = append(stack, n)
118+ case xml.EndElement:
119+ if len(stack) == 0 {
120+ return nil, fmt.Errorf("%s: unexpected closing element", file)
121+ }
122+ n := stack[len(stack)-1]
123+ n.Text = strings.TrimSpace(n.Text)
124+ if err := validateNode(n); err != nil {
125+ return nil, err
126+ }
127+ stack = stack[:len(stack)-1]
128+ case xml.CharData:
129+ if len(stack) > 0 {
130+ stack[len(stack)-1].Text += string(t)
131+ } else if strings.TrimSpace(string(t)) != "" {
132+ return nil, fmt.Errorf("%s: text outside root", file)
133+ }
134+ case xml.Directive:
135+ return nil, fmt.Errorf("%s: XML directives are unsupported", file)
136+ }
137+ }
138+ if root == nil {
139+ return nil, fmt.Errorf("%s:1: missing <xcb> root", file)
140+ }
141+ return root, nil
142+}
143+func require(n *Node, names string) error {
144+ for _, name := range strings.Fields(names) {
145+ if n.Attr[name] == "" {
146+ return n.fail("missing %s on <%s>", name, n.Kind)
147+ }
148+ }
149+ return nil
150+}
151+func number(n *Node, attr string, bits int) error {
152+ _, err := strconv.ParseUint(n.Attr[attr], 0, bits)
153+ if err != nil {
154+ return n.fail("invalid %s %q", attr, n.Attr[attr])
155+ }
156+ return nil
157+}
158+func exprCount(n *Node) int {
159+ count := 0
160+ for _, c := range n.Children {
161+ if c.Kind != "doc" {
162+ count++
163+ }
164+ }
165+ return count
166+}
167+func validateNode(n *Node) error {
168+ required := map[string]string{
169+ "xcb": "header", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name",
170+ "enum": "name", "item": "name", "struct": "name", "union": "name", "request": "name opcode",
171+ "event": "name number", "eventcopy": "name number ref", "error": "name number", "errorcopy": "name number ref",
172+ "field": "name type", "list": "name type", "exprfield": "name type", "switch": "name", "enumref": "ref", "op": "op", "unop": "op",
173+ }
174+ if err := require(n, required[n.Kind]); err != nil {
175+ return err
176+ }
177+ switch n.Kind {
178+ case "import", "type", "fieldref", "enumref":
179+ if n.Text == "" {
180+ return n.fail("empty <%s>", n.Kind)
181+ }
182+ case "value", "bit":
183+ if _, err := strconv.ParseInt(n.Text, 0, 64); err != nil {
184+ return n.fail("invalid <%s> integer %q", n.Kind, n.Text)
185+ }
186+ case "pad":
187+ _, b := n.Attr["bytes"]
188+ _, a := n.Attr["align"]
189+ if b == a {
190+ return n.fail("pad requires exactly one of bytes or align")
191+ }
192+ key := "bytes"
193+ if a {
194+ key = "align"
195+ }
196+ if err := number(n, key, 31); err != nil {
197+ return err
198+ }
199+ if a {
200+ v, _ := strconv.Atoi(n.Attr[key])
201+ if v == 0 || v&(v-1) != 0 {
202+ return n.fail("pad alignment must be a positive power of two")
203+ }
204+ }
205+ case "request":
206+ if err := number(n, "opcode", 8); err != nil {
207+ return err
208+ }
209+ case "event", "eventcopy", "error", "errorcopy":
210+ if err := number(n, "number", 8); err != nil {
211+ return err
212+ }
213+ if n.Attr["xge"] == "true" {
214+ v, _ := strconv.Atoi(n.Attr["number"])
215+ if v >= 128 {
216+ return n.fail("xge evtype must be < 128")
217+ }
218+ }
219+ case "op":
220+ if !contains("+ - * / & | << >>", n.Attr["op"]) {
221+ return n.fail("unsupported operator %q", n.Attr["op"])
222+ }
223+ if exprCount(n) != 2 {
224+ return n.fail("op requires two operands")
225+ }
226+ case "unop":
227+ if n.Attr["op"] != "~" {
228+ return n.fail("unsupported unary operator %q", n.Attr["op"])
229+ }
230+ if exprCount(n) != 1 {
231+ return n.fail("unop requires one operand")
232+ }
233+ case "popcount", "exprfield", "item":
234+ if exprCount(n) != 1 {
235+ return n.fail("<%s> requires one expression", n.Kind)
236+ }
237+ case "list":
238+ if exprCount(n) > 1 {
239+ return n.fail("list has multiple length expressions")
240+ }
241+ case "bitcase":
242+ fields := 0
243+ for _, c := range n.Children {
244+ if c.Kind == "field" {
245+ fields++
246+ if contains("CARD8 CARD16 INT8 INT16 BOOL char BYTE void", c.Attr["type"]) {
247+ return c.fail("value-list bitcase requires a 4-byte field")
248+ }
249+ }
250+ }
251+ if fields != 1 {
252+ return n.fail("value-list bitcase requires exactly one field")
253+ }
254+ }
255+ for _, key := range []string{"xge", "no-sequence-number", "combine-adjacent", "extension-multiword"} {
256+ if v, ok := n.Attr[key]; ok && v != "true" && v != "false" {
257+ return n.fail("invalid boolean %s=%q", key, v)
258+ }
259+ }
260+ // Forward references cannot be laid out in one pass. Reject at the length
261+ // expression's location rather than failing later while emitting Go.
262+ declared := map[string]bool{}
263+ for _, c := range n.Children {
264+ if c.Kind == "field" || c.Kind == "exprfield" {
265+ declared[c.Attr["name"]] = true
266+ }
267+ }
268+ seen := map[string]bool{}
269+ for _, c := range n.Children {
270+ if c.Kind == "list" {
271+ var visit func(*Node) error
272+ visit = func(e *Node) error {
273+ if e.Kind == "fieldref" && declared[e.Text] && !seen[e.Text] {
274+ return e.fail("list length references later field %q", e.Text)
275+ }
276+ for _, child := range e.Children {
277+ if err := visit(child); err != nil {
278+ return err
279+ }
280+ }
281+ return nil
282+ }
283+ if err := visit(c); err != nil {
284+ return err
285+ }
286+ }
287+ if c.Kind == "field" || c.Kind == "exprfield" {
288+ seen[c.Attr["name"]] = true
289+ }
290+ }
291+ return nil
292+}
@@ -0,0 +1,292 @@
1+package main
2+
3+import (
4+ "encoding/xml"
5+ "fmt"
6+ "io"
7+ "os"
8+ "strconv"
9+ "strings"
10+)
11+
12+// Node retains XML order and source locations, including expressions. Resolution
13+// turns this syntax tree into the flat, typed emission IR; parsing makes no Go
14+// naming or layout choices.
15+type Node struct {
16+ Kind string
17+ Attr map[string]string
18+ Text string
19+ Children []*Node
20+ File string
21+ Line int
22+}
23+
24+func (n *Node) fail(format string, args ...any) error {
25+ return fmt.Errorf("%s:%d: %s", n.File, n.Line, fmt.Sprintf(format, args...))
26+}
27+
28+// Attribute and child whitelists are per element, not global: a known attribute
29+// or element used in the wrong place is also an error. Documentation is ignored.
30+var attributes = map[string]string{
31+ "xcb": "header extension-name extension-xname extension-multiword major-version minor-version",
32+ "import": "", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", "type": "",
33+ "enum": "name", "item": "name", "value": "", "bit": "", "struct": "name", "union": "name",
34+ "request": "name opcode combine-adjacent", "reply": "", "event": "name number no-sequence-number xge",
35+ "eventcopy": "name number ref", "error": "name number type", "errorcopy": "name number ref",
36+ "field": "name type enum altenum mask", "pad": "bytes align", "list": "name type enum",
37+ "exprfield": "name type", "switch": "name", "bitcase": "", "fieldref": "", "enumref": "ref",
38+ "op": "op", "unop": "op", "popcount": "", "doc": "",
39+}
40+var children = map[string]string{
41+ "xcb": "import typedef xidtype xidunion enum struct union request event eventcopy error errorcopy doc",
42+ "xidunion": "type", "enum": "item doc", "item": "value bit doc",
43+ "struct": "field pad list doc", "union": "field pad list doc",
44+ "request": "field pad list exprfield switch reply doc", "reply": "field pad list exprfield doc",
45+ "event": "field pad list exprfield doc", "error": "field pad doc",
46+ "list": "value fieldref op unop popcount enumref", "exprfield": "value fieldref op unop popcount enumref",
47+ "switch": "fieldref op unop popcount enumref bitcase", "bitcase": "enumref value field pad doc",
48+ "op": "value fieldref op unop popcount enumref", "unop": "value fieldref op unop popcount enumref",
49+ "popcount": "value fieldref op unop popcount enumref",
50+}
51+
52+func contains(words, word string) bool {
53+ for _, s := range strings.Fields(words) {
54+ if s == word {
55+ return true
56+ }
57+ }
58+ return false
59+}
60+
61+func parseFile(path string) (*Node, error) {
62+ f, err := os.Open(path)
63+ if err != nil {
64+ return nil, err
65+ }
66+ defer f.Close()
67+ return parseXML(path, f)
68+}
69+func parseXML(file string, r io.Reader) (*Node, error) {
70+ d := xml.NewDecoder(r)
71+ var root *Node
72+ var stack []*Node
73+ for {
74+ token, err := d.Token()
75+ if err == io.EOF {
76+ break
77+ }
78+ if err != nil {
79+ line, _ := d.InputPos()
80+ return nil, fmt.Errorf("%s:%d: %w", file, line, err)
81+ }
82+ switch t := token.(type) {
83+ case xml.StartElement:
84+ line, _ := d.InputPos()
85+ n := &Node{Kind: t.Name.Local, Attr: make(map[string]string), File: file, Line: line}
86+ allowed, ok := attributes[n.Kind]
87+ if !ok || t.Name.Space != "" {
88+ return nil, n.fail("unsupported element <%s>", t.Name.Local)
89+ }
90+ if len(stack) == 0 {
91+ if root != nil || n.Kind != "xcb" {
92+ return nil, n.fail("expected a single <xcb> root")
93+ }
94+ root = n
95+ } else {
96+ parent := stack[len(stack)-1]
97+ if !contains(children[parent.Kind], n.Kind) {
98+ return nil, n.fail("unsupported <%s> in <%s>", n.Kind, parent.Kind)
99+ }
100+ parent.Children = append(parent.Children, n)
101+ }
102+ if n.Kind == "doc" {
103+ if err := d.Skip(); err != nil {
104+ return nil, n.fail("invalid doc: %v", err)
105+ }
106+ continue
107+ }
108+ for _, a := range t.Attr {
109+ if a.Name.Space != "" || !contains(allowed, a.Name.Local) {
110+ return nil, n.fail("unknown attribute %q on <%s>", a.Name.Local, n.Kind)
111+ }
112+ if _, ok := n.Attr[a.Name.Local]; ok {
113+ return nil, n.fail("duplicate attribute %q", a.Name.Local)
114+ }
115+ n.Attr[a.Name.Local] = a.Value
116+ }
117+ stack = append(stack, n)
118+ case xml.EndElement:
119+ if len(stack) == 0 {
120+ return nil, fmt.Errorf("%s: unexpected closing element", file)
121+ }
122+ n := stack[len(stack)-1]
123+ n.Text = strings.TrimSpace(n.Text)
124+ if err := validateNode(n); err != nil {
125+ return nil, err
126+ }
127+ stack = stack[:len(stack)-1]
128+ case xml.CharData:
129+ if len(stack) > 0 {
130+ stack[len(stack)-1].Text += string(t)
131+ } else if strings.TrimSpace(string(t)) != "" {
132+ return nil, fmt.Errorf("%s: text outside root", file)
133+ }
134+ case xml.Directive:
135+ return nil, fmt.Errorf("%s: XML directives are unsupported", file)
136+ }
137+ }
138+ if root == nil {
139+ return nil, fmt.Errorf("%s:1: missing <xcb> root", file)
140+ }
141+ return root, nil
142+}
143+func require(n *Node, names string) error {
144+ for _, name := range strings.Fields(names) {
145+ if n.Attr[name] == "" {
146+ return n.fail("missing %s on <%s>", name, n.Kind)
147+ }
148+ }
149+ return nil
150+}
151+func number(n *Node, attr string, bits int) error {
152+ _, err := strconv.ParseUint(n.Attr[attr], 0, bits)
153+ if err != nil {
154+ return n.fail("invalid %s %q", attr, n.Attr[attr])
155+ }
156+ return nil
157+}
158+func exprCount(n *Node) int {
159+ count := 0
160+ for _, c := range n.Children {
161+ if c.Kind != "doc" {
162+ count++
163+ }
164+ }
165+ return count
166+}
167+func validateNode(n *Node) error {
168+ required := map[string]string{
169+ "xcb": "header", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name",
170+ "enum": "name", "item": "name", "struct": "name", "union": "name", "request": "name opcode",
171+ "event": "name number", "eventcopy": "name number ref", "error": "name number", "errorcopy": "name number ref",
172+ "field": "name type", "list": "name type", "exprfield": "name type", "switch": "name", "enumref": "ref", "op": "op", "unop": "op",
173+ }
174+ if err := require(n, required[n.Kind]); err != nil {
175+ return err
176+ }
177+ switch n.Kind {
178+ case "import", "type", "fieldref", "enumref":
179+ if n.Text == "" {
180+ return n.fail("empty <%s>", n.Kind)
181+ }
182+ case "value", "bit":
183+ if _, err := strconv.ParseInt(n.Text, 0, 64); err != nil {
184+ return n.fail("invalid <%s> integer %q", n.Kind, n.Text)
185+ }
186+ case "pad":
187+ _, b := n.Attr["bytes"]
188+ _, a := n.Attr["align"]
189+ if b == a {
190+ return n.fail("pad requires exactly one of bytes or align")
191+ }
192+ key := "bytes"
193+ if a {
194+ key = "align"
195+ }
196+ if err := number(n, key, 31); err != nil {
197+ return err
198+ }
199+ if a {
200+ v, _ := strconv.Atoi(n.Attr[key])
201+ if v == 0 || v&(v-1) != 0 {
202+ return n.fail("pad alignment must be a positive power of two")
203+ }
204+ }
205+ case "request":
206+ if err := number(n, "opcode", 8); err != nil {
207+ return err
208+ }
209+ case "event", "eventcopy", "error", "errorcopy":
210+ if err := number(n, "number", 8); err != nil {
211+ return err
212+ }
213+ if n.Attr["xge"] == "true" {
214+ v, _ := strconv.Atoi(n.Attr["number"])
215+ if v >= 128 {
216+ return n.fail("xge evtype must be < 128")
217+ }
218+ }
219+ case "op":
220+ if !contains("+ - * / & | << >>", n.Attr["op"]) {
221+ return n.fail("unsupported operator %q", n.Attr["op"])
222+ }
223+ if exprCount(n) != 2 {
224+ return n.fail("op requires two operands")
225+ }
226+ case "unop":
227+ if n.Attr["op"] != "~" {
228+ return n.fail("unsupported unary operator %q", n.Attr["op"])
229+ }
230+ if exprCount(n) != 1 {
231+ return n.fail("unop requires one operand")
232+ }
233+ case "popcount", "exprfield", "item":
234+ if exprCount(n) != 1 {
235+ return n.fail("<%s> requires one expression", n.Kind)
236+ }
237+ case "list":
238+ if exprCount(n) > 1 {
239+ return n.fail("list has multiple length expressions")
240+ }
241+ case "bitcase":
242+ fields := 0
243+ for _, c := range n.Children {
244+ if c.Kind == "field" {
245+ fields++
246+ if contains("CARD8 CARD16 INT8 INT16 BOOL char BYTE void", c.Attr["type"]) {
247+ return c.fail("value-list bitcase requires a 4-byte field")
248+ }
249+ }
250+ }
251+ if fields != 1 {
252+ return n.fail("value-list bitcase requires exactly one field")
253+ }
254+ }
255+ for _, key := range []string{"xge", "no-sequence-number", "combine-adjacent", "extension-multiword"} {
256+ if v, ok := n.Attr[key]; ok && v != "true" && v != "false" {
257+ return n.fail("invalid boolean %s=%q", key, v)
258+ }
259+ }
260+ // Forward references cannot be laid out in one pass. Reject at the length
261+ // expression's location rather than failing later while emitting Go.
262+ declared := map[string]bool{}
263+ for _, c := range n.Children {
264+ if c.Kind == "field" || c.Kind == "exprfield" {
265+ declared[c.Attr["name"]] = true
266+ }
267+ }
268+ seen := map[string]bool{}
269+ for _, c := range n.Children {
270+ if c.Kind == "list" {
271+ var visit func(*Node) error
272+ visit = func(e *Node) error {
273+ if e.Kind == "fieldref" && declared[e.Text] && !seen[e.Text] {
274+ return e.fail("list length references later field %q", e.Text)
275+ }
276+ for _, child := range e.Children {
277+ if err := visit(child); err != nil {
278+ return err
279+ }
280+ }
281+ return nil
282+ }
283+ if err := visit(c); err != nil {
284+ return err
285+ }
286+ }
287+ if c.Kind == "field" || c.Kind == "exprfield" {
288+ seen[c.Attr["name"]] = true
289+ }
290+ }
291+ return nil
292+}
A cmd/xtgen/parse_test.go +74 -0
@@ -0,0 +1,74 @@
1+package main
2+
3+import (
4+ "path/filepath"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestParseVendored(t *testing.T) {
10+ files, err := filepath.Glob("../../spec/xml/*.xml")
11+ if err != nil {
12+ t.Fatal(err)
13+ }
14+ if len(files) != 9 {
15+ t.Fatalf("got %d XML files, want 9", len(files))
16+ }
17+ for _, file := range files {
18+ t.Run(filepath.Base(file), func(t *testing.T) {
19+ n, err := parseFile(file)
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ if n.Kind != "xcb" || len(n.Children) == 0 {
24+ t.Fatal("missing module AST")
25+ }
26+ })
27+ }
28+}
29+func TestParseRejects(t *testing.T) {
30+ cases := []struct{ name, body, want string }{
31+ {"unknown element", "<surprise/>", "unsupported element"},
32+ {"unknown attribute", `<xidtype name="WINDOW" surprise="yes"/>`, "unknown attribute"},
33+ {"misplaced known element", `<struct name="S"><reply/></struct>`, "unsupported <reply>"},
34+ {"sumof", `<request name="R" opcode="1"><list name="a" type="CARD8"><sumof ref="a"/></list></request>`, "sumof"},
35+ {"paramref", `<request name="R" opcode="1"><list name="a" type="CARD8"><paramref type="CARD8">a</paramref></list></request>`, "paramref"},
36+ {"case", `<request name="R" opcode="1"><switch name="s"><case/></switch></request>`, "case"},
37+ {"struct switch", `<struct name="S"><switch name="s"/></struct>`, "unsupported <switch>"},
38+ {"reply switch", `<request name="R" opcode="1"><reply><switch name="s"/></reply></request>`, "unsupported <switch>"},
39+ {"nested switch", `<request name="R" opcode="1"><switch name="s"><bitcase><switch name="s"/></bitcase></switch></request>`, "unsupported <switch>"},
40+ {"fd", `<request name="R" opcode="1"><fd name="fd"/></request>`, "fd"},
41+ {"eventstruct", `<eventstruct name="E"/>`, "eventstruct"},
42+ {"required alignment", `<struct name="S"><required_start_align align="4"/></struct>`, "required_start_align"},
43+ {"forward length", `<struct name="S"><list name="a" type="CARD8"><fieldref>n</fieldref></list><field name="n" type="CARD8"/></struct>`, "later field"},
44+ {"xge code", `<event name="E" number="128" xge="true"/>`, "evtype"},
45+ {"bad opcode", `<request name="R" opcode="256"/>`, "invalid opcode"},
46+ {"bad operator", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="%"><value>1</value><value>2</value></op></exprfield></request>`, "unsupported operator"},
47+ {"wrong operands", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="+"><value>1</value></op></exprfield></request>`, "two operands"},
48+ {"bad pad", `<struct name="S"><pad align="3"/></struct>`, "power of two"},
49+ {"namespace", `<xidtype xmlns="other" name="X"/>`, "unsupported element"},
50+ {"duplicate attr", `<xidtype name="X" name="Y"/>`, "duplicate attribute"},
51+ }
52+ for _, tc := range cases {
53+ t.Run(tc.name, func(t *testing.T) {
54+ _, err := parseXML("bad.xml", strings.NewReader("<xcb header=\"test\">\n"+tc.body+"\n</xcb>"))
55+ if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "bad.xml:2:") {
56+ t.Fatalf("got %v, want file:line and %q", err, tc.want)
57+ }
58+ })
59+ }
60+}
61+func TestParseAST(t *testing.T) {
62+ n, err := parseXML("ok.xml", strings.NewReader(`<xcb header="a"><doc><arbitrary/></doc><request name="R" opcode="1"><field type="CARD32" name="n"/><list type="CARD8" name="data"><op op="*"><fieldref>n</fieldref><value>4</value></op></list></request></xcb>`))
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ r := n.Children[1]
67+ if r.Attr["name"] != "R" || r.Children[1].Children[0].Children[0].Text != "n" {
68+ t.Fatal("AST lost order or expression")
69+ }
70+}
71+func FuzzParseXML(f *testing.F) {
72+ f.Add(`<xcb header="test"/>`)
73+ f.Fuzz(func(t *testing.T, raw string) { _, _ = parseXML("fuzz.xml", strings.NewReader(raw)) })
74+}
@@ -0,0 +1,74 @@
1+package main
2+
3+import (
4+ "path/filepath"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestParseVendored(t *testing.T) {
10+ files, err := filepath.Glob("../../spec/xml/*.xml")
11+ if err != nil {
12+ t.Fatal(err)
13+ }
14+ if len(files) != 9 {
15+ t.Fatalf("got %d XML files, want 9", len(files))
16+ }
17+ for _, file := range files {
18+ t.Run(filepath.Base(file), func(t *testing.T) {
19+ n, err := parseFile(file)
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ if n.Kind != "xcb" || len(n.Children) == 0 {
24+ t.Fatal("missing module AST")
25+ }
26+ })
27+ }
28+}
29+func TestParseRejects(t *testing.T) {
30+ cases := []struct{ name, body, want string }{
31+ {"unknown element", "<surprise/>", "unsupported element"},
32+ {"unknown attribute", `<xidtype name="WINDOW" surprise="yes"/>`, "unknown attribute"},
33+ {"misplaced known element", `<struct name="S"><reply/></struct>`, "unsupported <reply>"},
34+ {"sumof", `<request name="R" opcode="1"><list name="a" type="CARD8"><sumof ref="a"/></list></request>`, "sumof"},
35+ {"paramref", `<request name="R" opcode="1"><list name="a" type="CARD8"><paramref type="CARD8">a</paramref></list></request>`, "paramref"},
36+ {"case", `<request name="R" opcode="1"><switch name="s"><case/></switch></request>`, "case"},
37+ {"struct switch", `<struct name="S"><switch name="s"/></struct>`, "unsupported <switch>"},
38+ {"reply switch", `<request name="R" opcode="1"><reply><switch name="s"/></reply></request>`, "unsupported <switch>"},
39+ {"nested switch", `<request name="R" opcode="1"><switch name="s"><bitcase><switch name="s"/></bitcase></switch></request>`, "unsupported <switch>"},
40+ {"fd", `<request name="R" opcode="1"><fd name="fd"/></request>`, "fd"},
41+ {"eventstruct", `<eventstruct name="E"/>`, "eventstruct"},
42+ {"required alignment", `<struct name="S"><required_start_align align="4"/></struct>`, "required_start_align"},
43+ {"forward length", `<struct name="S"><list name="a" type="CARD8"><fieldref>n</fieldref></list><field name="n" type="CARD8"/></struct>`, "later field"},
44+ {"xge code", `<event name="E" number="128" xge="true"/>`, "evtype"},
45+ {"bad opcode", `<request name="R" opcode="256"/>`, "invalid opcode"},
46+ {"bad operator", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="%"><value>1</value><value>2</value></op></exprfield></request>`, "unsupported operator"},
47+ {"wrong operands", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="+"><value>1</value></op></exprfield></request>`, "two operands"},
48+ {"bad pad", `<struct name="S"><pad align="3"/></struct>`, "power of two"},
49+ {"namespace", `<xidtype xmlns="other" name="X"/>`, "unsupported element"},
50+ {"duplicate attr", `<xidtype name="X" name="Y"/>`, "duplicate attribute"},
51+ }
52+ for _, tc := range cases {
53+ t.Run(tc.name, func(t *testing.T) {
54+ _, err := parseXML("bad.xml", strings.NewReader("<xcb header=\"test\">\n"+tc.body+"\n</xcb>"))
55+ if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "bad.xml:2:") {
56+ t.Fatalf("got %v, want file:line and %q", err, tc.want)
57+ }
58+ })
59+ }
60+}
61+func TestParseAST(t *testing.T) {
62+ n, err := parseXML("ok.xml", strings.NewReader(`<xcb header="a"><doc><arbitrary/></doc><request name="R" opcode="1"><field type="CARD32" name="n"/><list type="CARD8" name="data"><op op="*"><fieldref>n</fieldref><value>4</value></op></list></request></xcb>`))
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ r := n.Children[1]
67+ if r.Attr["name"] != "R" || r.Children[1].Children[0].Children[0].Text != "n" {
68+ t.Fatal("AST lost order or expression")
69+ }
70+}
71+func FuzzParseXML(f *testing.F) {
72+ f.Add(`<xcb header="test"/>`)
73+ f.Fuzz(func(t *testing.T, raw string) { _, _ = parseXML("fuzz.xml", strings.NewReader(raw)) })
74+}
A cmd/xtgen/replies.go +554 -0
@@ -0,0 +1,554 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "sort"
7+ "strconv"
8+ "strings"
9+)
10+
11+// member is one wire member of a decoded container (reply or variable struct
12+// view). Static is the offset from the container's byte 0, or -1 when it
13+// follows a variable-length list and must be recomputed from earlier members.
14+type member struct {
15+ XName, Method, Kind, GoType string
16+ Elem string // annotated element Go type for list views
17+ Type *Type
18+ Size, Align, Static int
19+ Count *Node
20+ ListKind string // bytes, scalar, struct, iter
21+}
22+
23+// container is the lowered layout shared by replies and variable struct views.
24+type container struct {
25+ Name, Recv string
26+ Members []member
27+ Fixed int // minimum byte length (static prefix)
28+ Dynamic int // index of the first variable list, -1 when none
29+ Reply bool
30+ Module *Module
31+}
32+
33+const replyLifetime = "It borrows the reply bytes: valid only until the Handler callback returns, because the read ring is reused. Copy what you keep."
34+
35+func isByteElem(xml string) bool {
36+ return xml == "void" || xml == "BYTE" || xml == "CARD8" || xml == "char"
37+}
38+
39+func annotatedType(m *Module, n *Node, typ *Type) (string, error) {
40+ goType := qualified(m, typ.Module, typ.GoType)
41+ for _, attr := range []string{"enum", "mask"} {
42+ if name := n.Attr[attr]; name != "" {
43+ e, owner := findEnum(m, name)
44+ if e == nil {
45+ return "", n.fail("unknown %s %s", attr, name)
46+ }
47+ goType = qualified(m, owner, e.Name)
48+ }
49+ }
50+ return goType, nil
51+}
52+
53+// elemName names the per-package list view for an element type.
54+func elemName(goType string) string {
55+ if i := strings.LastIndex(goType, "."); i >= 0 {
56+ goType = goType[i+1:]
57+ }
58+ return strings.ToUpper(goType[:1]) + goType[1:]
59+}
60+
61+// buildContainer lowers XML members. Replies start at byte 1 (the first XML
62+// member occupies the header data byte) and continue at byte 8.
63+func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, reserved string) (*container, error) {
64+ c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m}
65+ off := 0
66+ if reply {
67+ off = 1
68+ }
69+ first := true
70+ methods := map[string]bool{}
71+ for _, n := range nodes {
72+ if n.Kind == "doc" {
73+ continue
74+ }
75+ mem := member{XName: n.Attr["name"], Kind: n.Kind, Static: off}
76+ if c.Dynamic >= 0 {
77+ mem.Static = -1
78+ }
79+ switch n.Kind {
80+ case "pad":
81+ if a := n.Attr["align"]; a != "" {
82+ mem.Align, _ = strconv.Atoi(a)
83+ if c.Dynamic < 0 {
84+ mem.Size = (-off) & (mem.Align - 1)
85+ }
86+ } else {
87+ mem.Size, _ = strconv.Atoi(n.Attr["bytes"])
88+ }
89+ case "field", "list":
90+ typ, err := lookup(m, n.Attr["type"])
91+ if err != nil {
92+ return nil, n.fail("%v", err)
93+ }
94+ mem.Type = typ
95+ mem.Method = goName(mem.XName)
96+ if contains(reserved, mem.Method) {
97+ mem.Method += "_"
98+ }
99+ if methods[mem.Method] {
100+ return nil, n.fail("duplicate getter %s", mem.Method)
101+ }
102+ methods[mem.Method] = true
103+ elem, err := annotatedType(m, n, typ)
104+ if err != nil {
105+ return nil, err
106+ }
107+ mem.Elem = elem
108+ if n.Kind == "field" {
109+ if typ.Variable {
110+ return nil, n.fail("variable-size field outside a list is unsupported")
111+ }
112+ mem.GoType = elem
113+ mem.Size = typ.Size
114+ break
115+ }
116+ if len(n.Children) != 1 {
117+ return nil, n.fail("reply-side list requires a length expression")
118+ }
119+ mem.Count = n.Children[0]
120+ switch {
121+ case isByteElem(n.Attr["type"]):
122+ mem.ListKind, mem.GoType = "bytes", "[]byte"
123+ case typ.Variable:
124+ if typ.Size == 0 {
125+ return nil, n.fail("variable struct %s has an empty fixed prefix", typ.Name)
126+ }
127+ mem.ListKind, mem.GoType = "iter", qualified(m, typ.Module, typ.GoType+"Iter")
128+ case isScalar(typ):
129+ mem.ListKind, mem.GoType = "scalar", elemName(elem)+"List"
130+ default:
131+ mem.ListKind, mem.GoType = "struct", elemName(elem)+"List"
132+ }
133+ constant := mem.Count.Kind == "value" && !typ.Variable
134+ if constant {
135+ count, _ := strconv.Atoi(mem.Count.Text)
136+ mem.Size = count * typ.Size
137+ } else if c.Dynamic < 0 {
138+ c.Dynamic = len(c.Members)
139+ }
140+ default:
141+ return nil, n.fail("unsupported %s member %s", map[bool]string{true: "reply", false: "struct"}[reply], n.Kind)
142+ }
143+ if reply && first {
144+ if mem.Size != 1 {
145+ return nil, n.fail("first reply member must occupy the header data byte")
146+ }
147+ off = 8
148+ first = false
149+ c.Members = append(c.Members, mem)
150+ continue
151+ }
152+ first = false
153+ if c.Dynamic < 0 {
154+ off += mem.Size
155+ }
156+ c.Members = append(c.Members, mem)
157+ }
158+ if c.Dynamic >= 0 {
159+ c.Fixed = c.Members[c.Dynamic].Static
160+ } else {
161+ c.Fixed = off
162+ }
163+ return c, nil
164+}
165+
166+// ref renders a field reference as a getter call on the receiver. Replies may
167+// refer to the implicit header length word.
168+func (c *container) ref(s string) string {
169+ for _, mem := range c.Members {
170+ if mem.Kind == "field" && mem.XName == s {
171+ return "int(" + c.Recv + "." + mem.Method + "())"
172+ }
173+ }
174+ if c.Reply && s == "length" {
175+ return "int(binary.LittleEndian.Uint32(" + c.Recv + ".raw[4:]))"
176+ }
177+ return "<unresolved " + s + ">"
178+}
179+
180+func (c *container) count(mem member) (string, error) {
181+ s, err := renderExpr(mem.Count, c.ref, c.Module)
182+ if err != nil {
183+ return "", err
184+ }
185+ if strings.Contains(s, "<unresolved") {
186+ return "", mem.Count.fail("list length references an unknown field")
187+ }
188+ return s, nil
189+}
190+
191+// advance emits code moving off past member i. With fail != "" every step is
192+// bounds-checked against raw.
193+func (c *container) advance(b *strings.Builder, i int, fail string) error {
194+ mem := c.Members[i]
195+ raw := c.Recv + ".raw"
196+ switch mem.Kind {
197+ case "pad":
198+ if mem.Align > 0 {
199+ fmt.Fprintf(b, "off=(off+%d)&^%d;", mem.Align-1, mem.Align-1)
200+ } else {
201+ fmt.Fprintf(b, "off+=%d;", mem.Size)
202+ }
203+ case "field":
204+ fmt.Fprintf(b, "off+=%d;", mem.Size)
205+ case "list":
206+ n, err := c.count(mem)
207+ if err != nil {
208+ return err
209+ }
210+ if mem.ListKind == "iter" {
211+ fmt.Fprintf(b, "{ n:=%s;", n)
212+ if fail != "" {
213+ fmt.Fprintf(b, "if off>len(%s) || n<0 || n>len(%s) { %s };", raw, raw, fail)
214+ }
215+ fmt.Fprintf(b, "it:=%s(%s[off:],n); for it.Next() { off+=it.Value().Size() };", qualified(c.Module, mem.Type.Module, "New"+mem.Type.GoType+"Iter"), raw)
216+ if fail != "" {
217+ fmt.Fprintf(b, "if it.Err() { %s };", fail)
218+ }
219+ b.WriteString("};")
220+ return nil
221+ }
222+ fmt.Fprintf(b, "{ n:=%s;", n)
223+ if fail != "" {
224+ fmt.Fprintf(b, "if n<0 || n>len(%s) { %s };", raw, fail)
225+ }
226+ fmt.Fprintf(b, "off+=n*%d };", mem.Type.Size)
227+ if fail != "" {
228+ fmt.Fprintf(b, "if off>len(%s) { %s };", raw, fail)
229+ }
230+ }
231+ return nil
232+}
233+
234+// offset emits statements leaving member i's offset in off.
235+func (c *container) offset(b *strings.Builder, i int) error {
236+ if c.Members[i].Static >= 0 {
237+ fmt.Fprintf(b, "off:=%d;", c.Members[i].Static)
238+ return nil
239+ }
240+ fmt.Fprintf(b, "off:=%d;", c.Fixed)
241+ for j := c.Dynamic; j < i; j++ {
242+ if err := c.advance(b, j, ""); err != nil {
243+ return err
244+ }
245+ }
246+ return nil
247+}
248+
249+// validate emits the full-length walk used by Parse and Iter.Next.
250+func (c *container) validate(b *strings.Builder, fail string) error {
251+ if c.Dynamic < 0 {
252+ return nil
253+ }
254+ fmt.Fprintf(b, "off:=%d;", c.Fixed)
255+ for j := c.Dynamic; j < len(c.Members); j++ {
256+ if err := c.advance(b, j, fail); err != nil {
257+ return err
258+ }
259+ }
260+ fmt.Fprintf(b, "if off>len(%s.raw) { %s };", c.Recv, fail)
261+ return nil
262+}
263+
264+// getters emits one method per field and list.
265+func (c *container) getters(b *strings.Builder, typ string, lists map[string]string) error {
266+ r := c.Recv
267+ for i, mem := range c.Members {
268+ if mem.Kind == "pad" {
269+ continue
270+ }
271+ fmt.Fprintf(b, "func (%s %s) %s() %s {", r, typ, mem.Method, mem.GoType)
272+ static := mem.Static >= 0
273+ at := fmt.Sprintf("%s.raw[%d:]", r, mem.Static)
274+ if !static {
275+ if err := c.offset(b, i); err != nil {
276+ return err
277+ }
278+ at = r + ".raw[off:]"
279+ }
280+ if mem.Kind == "field" {
281+ if isScalar(mem.Type) {
282+ fmt.Fprintf(b, "return %s };\n", getScalar(mem.Type, mem.GoType, at))
283+ } else {
284+ fmt.Fprintf(b, "return %s(%s) };\n", qualified(c.Module, mem.Type.Module, "Get"+mem.Type.GoType), at)
285+ }
286+ continue
287+ }
288+ if static {
289+ fmt.Fprintf(b, "off:=%d;", mem.Static)
290+ }
291+ n, err := c.count(mem)
292+ if err != nil {
293+ return err
294+ }
295+ switch mem.ListKind {
296+ case "iter":
297+ fmt.Fprintf(b, "return %s(%s.raw[off:],%s) };\n", qualified(c.Module, mem.Type.Module, "New"+mem.Type.GoType+"Iter"), r, n)
298+ case "bytes":
299+ fmt.Fprintf(b, "end:=off+%s; return %s.raw[off:end:end] };\n", n, r)
300+ default:
301+ fmt.Fprintf(b, "end:=off+(%s)*%d; return %s{b:%s.raw[off:end:end]} };\n", n, mem.Type.Size, mem.GoType, r)
302+ lists[mem.GoType] = listDecl(c.Module, mem.GoType, mem.Elem, mem.Type)
303+ }
304+ }
305+ return nil
306+}
307+
308+func listDecl(m *Module, name, goType string, elem *Type) string {
309+ var b strings.Builder
310+ fmt.Fprintf(&b, "// %s is a borrowed view over a wire list of %s. %s\n", name, goType, "It shares the lifetime of the bytes it was taken from; copy what you keep.")
311+ fmt.Fprintf(&b, "type %s struct { b []byte };\n", name)
312+ fmt.Fprintf(&b, "func (l %s) Len() int { return len(l.b)/%d };\n", name, elem.Size)
313+ src := fmt.Sprintf("l.b[i*%d:]", elem.Size)
314+ if isScalar(elem) {
315+ fmt.Fprintf(&b, "func (l %s) At(i int) %s { return %s };\n", name, goType, getScalar(elem, goType, src))
316+ } else {
317+ fmt.Fprintf(&b, "func (l %s) At(i int) %s { return %s(%s) };\n", name, goType, qualified(m, elem.Module, "Get"+elem.GoType), src)
318+ }
319+ return b.String()
320+}
321+
322+type replyIR struct {
323+ Req string
324+ C *container
325+}
326+
327+func resolveReplies(m *Module) ([]replyIR, []*container, error) {
328+ var replies []replyIR
329+ for _, n := range m.Root.Children {
330+ if n.Kind != "request" {
331+ continue
332+ }
333+ for _, r := range n.Children {
334+ if r.Kind != "reply" {
335+ continue
336+ }
337+ name := goName(n.Attr["name"])
338+ for _, s := range []string{name + "Reply", "Parse" + name + "Reply"} {
339+ if err := m.claim(s, r); err != nil {
340+ return nil, nil, err
341+ }
342+ }
343+ c, err := buildContainer(m, name+"Reply", "r", r.Children, true, "Raw")
344+ if err != nil {
345+ return nil, nil, err
346+ }
347+ replies = append(replies, replyIR{Req: name, C: c})
348+ }
349+ }
350+ var views []*container
351+ for _, t := range m.Types {
352+ if t.Kind != "struct" || !t.Variable {
353+ continue
354+ }
355+ for _, s := range []string{t.GoType + "View", t.GoType + "Iter", "New" + t.GoType + "Iter"} {
356+ if err := m.claim(s, t.Node); err != nil {
357+ return nil, nil, err
358+ }
359+ }
360+ c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, false, "Raw Size")
361+ if err != nil {
362+ return nil, nil, err
363+ }
364+ views = append(views, c)
365+ }
366+ return replies, views, nil
367+}
368+
369+func replyDecl(r replyIR, lists map[string]string) (string, error) {
370+ c := r.C
371+ var b strings.Builder
372+ fmt.Fprintf(&b, "// %s is a zero-copy view over a %s reply; offsets count from reply byte 0.\n// %s\n", c.Name, r.Req, replyLifetime)
373+ fmt.Fprintf(&b, "type %s struct { raw []byte };\n", c.Name)
374+ fmt.Fprintf(&b, "// Parse%s validates raw (the whole reply, 32+4*length bytes) once; getters do not re-check.\n", c.Name)
375+ fmt.Fprintf(&b, "func Parse%s(raw []byte) (%s, bool) {", c.Name, c.Name)
376+ fixed := max(c.Fixed, 32)
377+ fmt.Fprintf(&b, "if len(raw)<%d { return %s{}, false };", fixed, c.Name)
378+ fmt.Fprintf(&b, "r:=%s{raw:raw};", c.Name)
379+ if err := c.validate(&b, "return "+c.Name+"{}, false"); err != nil {
380+ return "", err
381+ }
382+ b.WriteString("return r, true };\n")
383+ fmt.Fprintf(&b, "func (r %s) Raw() []byte { return r.raw };\n", c.Name)
384+ if err := c.getters(&b, c.Name, lists); err != nil {
385+ return "", err
386+ }
387+ return b.String(), nil
388+}
389+
390+func viewDecl(c *container, lists map[string]string) (string, error) {
391+ base := strings.TrimSuffix(c.Name, "View")
392+ iter := base + "Iter"
393+ var b strings.Builder
394+ fmt.Fprintf(&b, "// %s is a zero-copy view over one wire-encoded %s, yielded by %s.\n// It shares the lifetime of the bytes it was taken from (for replies: until the Handler callback returns). Copy what you keep.\n", c.Name, base, iter)
395+ fmt.Fprintf(&b, "type %s struct { raw []byte };\n", c.Name)
396+ fmt.Fprintf(&b, "func (v %s) Raw() []byte { return v.raw };\n", c.Name)
397+ fmt.Fprintf(&b, "// Size is the element's wire size, including trailing alignment.\nfunc (v %s) Size() int { return len(v.raw) };\n", c.Name)
398+ // size%s returns the element's wire size at the start of p, -1 when malformed.
399+ fmt.Fprintf(&b, "func size%s(p []byte) int { if len(p)<%d { return -1 }; v:=%s{raw:p}; _=v;", base, c.Fixed, c.Name)
400+ if c.Dynamic < 0 {
401+ fmt.Fprintf(&b, "return %d };\n", c.Fixed)
402+ } else {
403+ if err := c.validate(&b, "return -1"); err != nil {
404+ return "", err
405+ }
406+ b.WriteString("return off };\n")
407+ }
408+ if err := c.getters(&b, c.Name, lists); err != nil {
409+ return "", err
410+ }
411+ fmt.Fprintf(&b, "// %s walks n consecutive %s elements; Err reports a malformed length.\n// It shares the lifetime of the bytes it walks.\n", iter, base)
412+ fmt.Fprintf(&b, "type %s struct { b []byte; n int; v %s; err bool };\n", iter, c.Name)
413+ fmt.Fprintf(&b, "// New%s iterates n elements encoded at the start of b.\nfunc New%s(b []byte, n int) %s { return %s{b:b, n:n} };\n", iter, iter, iter, iter)
414+ fmt.Fprintf(&b, "func (it *%s) Next() bool { if it.n<=0 || it.err { return false }; s:=size%s(it.b); if s<0 { it.err=true; it.n=0; return false }; it.v=%s{raw:it.b[:s:s]}; it.b=it.b[s:]; it.n--; return true };\n", iter, base, c.Name)
415+ fmt.Fprintf(&b, "func (it *%s) Value() %s { return it.v };\n", iter, c.Name)
416+ fmt.Fprintf(&b, "func (it *%s) Err() bool { return it.err };\n", iter)
417+ return b.String(), nil
418+}
419+
420+// touch emits fuzz statements calling every getter and walking every list,
421+// recursing into variable struct views.
422+func touch(b *strings.Builder, c *container, expr string, depth int, all map[*Type]*container, failIter string) {
423+ for _, mem := range c.Members {
424+ switch {
425+ case mem.Kind == "field":
426+ fmt.Fprintf(b, "_ = %s.%s();", expr, mem.Method)
427+ case mem.Kind == "list" && mem.ListKind == "bytes":
428+ fmt.Fprintf(b, "_ = len(%s.%s());", expr, mem.Method)
429+ case mem.Kind == "list" && mem.ListKind == "iter":
430+ it := fmt.Sprintf("it%d", depth)
431+ fmt.Fprintf(b, "{ %s := %s.%s(); for %s.Next() {", it, expr, mem.Method, it)
432+ if child := all[mem.Type]; child != nil {
433+ touch(b, child, it+".Value()", depth+1, all, failIter)
434+ }
435+ fmt.Fprintf(b, "}; if %s.Err() { %s } };", it, failIter)
436+ case mem.Kind == "list":
437+ l := fmt.Sprintf("l%d", depth)
438+ fmt.Fprintf(b, "{ %s := %s.%s(); for i := 0; i < %s.Len(); i++ { _ = %s.At(i) } };", l, expr, mem.Method, l, l)
439+ }
440+ }
441+}
442+
443+func emitReplies(modules []*Module) error {
444+ type pending struct {
445+ m *Module
446+ replies []replyIR
447+ views []*container
448+ }
449+ var all []pending
450+ views := map[*Type]*container{}
451+ for _, m := range modules {
452+ replies, vs, err := resolveReplies(m)
453+ if err != nil {
454+ return err
455+ }
456+ for _, v := range vs {
457+ for _, t := range m.Types {
458+ if t.GoType+"View" == v.Name {
459+ views[t] = v
460+ }
461+ }
462+ }
463+ all = append(all, pending{m, replies, vs})
464+ }
465+ for _, p := range all {
466+ m := p.m
467+ lists := map[string]string{}
468+ var decls []string
469+ for _, r := range p.replies {
470+ d, err := replyDecl(r, lists)
471+ if err != nil {
472+ return err
473+ }
474+ decls = append(decls, d)
475+ }
476+ for _, v := range p.views {
477+ d, err := viewDecl(v, lists)
478+ if err != nil {
479+ return err
480+ }
481+ decls = append(decls, d)
482+ }
483+ var names []string
484+ for name := range lists {
485+ if err := m.claim(name, m.Root); err != nil {
486+ return err
487+ }
488+ names = append(names, name)
489+ }
490+ sort.Strings(names)
491+ for _, name := range names {
492+ decls = append(decls, lists[name])
493+ }
494+ if len(decls) == 0 {
495+ continue
496+ }
497+ decls = append([]string{importsFor(m, strings.Join(decls, "\n"))}, decls...)
498+ data := struct {
499+ Pkg string
500+ Decls []string
501+ }{Pkg: m.Pkg, Decls: decls}
502+ if err := writeTemplate(filepath.Join(m.Pkg, "replies_generated.go"), "replies", data); err != nil {
503+ return err
504+ }
505+ if err := emitReplyFuzz(m, p.replies, views); err != nil {
506+ return err
507+ }
508+ }
509+ return nil
510+}
511+
512+// emitReplyFuzz writes FuzzParse<Req>Reply for every reply. Seeds are the
513+// zero-filled fixed prefix plus any captured goldens <Req>*.reply.hex.
514+func emitReplyFuzz(m *Module, replies []replyIR, views map[*Type]*container) error {
515+ var decls []string
516+ usesGolden := false
517+ for _, r := range replies {
518+ var b strings.Builder
519+ c := r.C
520+ fmt.Fprintf(&b, "func FuzzParse%s(f *testing.F) {\n", c.Name)
521+ fmt.Fprintf(&b, "f.Add(make([]byte, %d));\n", (max(c.Fixed, 32)+3)&^3)
522+ goldens, _ := filepath.Glob(filepath.Join("testdata/golden", m.Pkg, r.Req+"*.reply.hex"))
523+ sort.Strings(goldens)
524+ for _, g := range goldens {
525+ base := filepath.Base(g)
526+ if strings.TrimSuffix(base, ".reply.hex") != r.Req && !startsUpper(strings.TrimPrefix(strings.TrimSuffix(base, ".reply.hex"), r.Req)) {
527+ continue
528+ }
529+ usesGolden = true
530+ fmt.Fprintf(&b, "if raw, err := xtest.LoadHex(%q); err != nil { f.Fatal(err) } else { f.Add(raw) };\n", "../"+filepath.ToSlash(g))
531+ }
532+ fmt.Fprintf(&b, "f.Fuzz(func(t *testing.T, raw []byte) {\nr, ok := Parse%s(raw);\nif !ok {", c.Name)
533+ if c.Dynamic < 0 {
534+ fmt.Fprintf(&b, "if len(raw) >= %d { t.Fatal(\"rejected a complete fixed-size reply\") };", max(c.Fixed, 32))
535+ }
536+ b.WriteString("return };\n")
537+ b.WriteString("if len(r.Raw()) != len(raw) { t.Fatal(\"Raw must return the parsed bytes\") };\n")
538+ touch(&b, c, "r", 0, views, `t.Fatal("iterator error after successful Parse")`)
539+ b.WriteString("\n})\n}\n")
540+ decls = append(decls, b.String())
541+ }
542+ imports := "import \"testing\""
543+ if usesGolden {
544+ imports += "\nimport \"bnema.dev/xturbo/internal/xtest\""
545+ }
546+ decls = append([]string{imports}, decls...)
547+ data := struct {
548+ Pkg string
549+ Decls []string
550+ }{Pkg: m.Pkg, Decls: decls}
551+ return writeTemplate(filepath.Join(m.Pkg, "replies_fuzz_test.go"), "replies", data)
552+}
553+
554+func startsUpper(s string) bool { return s != "" && s[0] >= 'A' && s[0] <= 'Z' }
@@ -0,0 +1,554 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "sort"
7+ "strconv"
8+ "strings"
9+)
10+
11+// member is one wire member of a decoded container (reply or variable struct
12+// view). Static is the offset from the container's byte 0, or -1 when it
13+// follows a variable-length list and must be recomputed from earlier members.
14+type member struct {
15+ XName, Method, Kind, GoType string
16+ Elem string // annotated element Go type for list views
17+ Type *Type
18+ Size, Align, Static int
19+ Count *Node
20+ ListKind string // bytes, scalar, struct, iter
21+}
22+
23+// container is the lowered layout shared by replies and variable struct views.
24+type container struct {
25+ Name, Recv string
26+ Members []member
27+ Fixed int // minimum byte length (static prefix)
28+ Dynamic int // index of the first variable list, -1 when none
29+ Reply bool
30+ Module *Module
31+}
32+
33+const replyLifetime = "It borrows the reply bytes: valid only until the Handler callback returns, because the read ring is reused. Copy what you keep."
34+
35+func isByteElem(xml string) bool {
36+ return xml == "void" || xml == "BYTE" || xml == "CARD8" || xml == "char"
37+}
38+
39+func annotatedType(m *Module, n *Node, typ *Type) (string, error) {
40+ goType := qualified(m, typ.Module, typ.GoType)
41+ for _, attr := range []string{"enum", "mask"} {
42+ if name := n.Attr[attr]; name != "" {
43+ e, owner := findEnum(m, name)
44+ if e == nil {
45+ return "", n.fail("unknown %s %s", attr, name)
46+ }
47+ goType = qualified(m, owner, e.Name)
48+ }
49+ }
50+ return goType, nil
51+}
52+
53+// elemName names the per-package list view for an element type.
54+func elemName(goType string) string {
55+ if i := strings.LastIndex(goType, "."); i >= 0 {
56+ goType = goType[i+1:]
57+ }
58+ return strings.ToUpper(goType[:1]) + goType[1:]
59+}
60+
61+// buildContainer lowers XML members. Replies start at byte 1 (the first XML
62+// member occupies the header data byte) and continue at byte 8.
63+func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, reserved string) (*container, error) {
64+ c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m}
65+ off := 0
66+ if reply {
67+ off = 1
68+ }
69+ first := true
70+ methods := map[string]bool{}
71+ for _, n := range nodes {
72+ if n.Kind == "doc" {
73+ continue
74+ }
75+ mem := member{XName: n.Attr["name"], Kind: n.Kind, Static: off}
76+ if c.Dynamic >= 0 {
77+ mem.Static = -1
78+ }
79+ switch n.Kind {
80+ case "pad":
81+ if a := n.Attr["align"]; a != "" {
82+ mem.Align, _ = strconv.Atoi(a)
83+ if c.Dynamic < 0 {
84+ mem.Size = (-off) & (mem.Align - 1)
85+ }
86+ } else {
87+ mem.Size, _ = strconv.Atoi(n.Attr["bytes"])
88+ }
89+ case "field", "list":
90+ typ, err := lookup(m, n.Attr["type"])
91+ if err != nil {
92+ return nil, n.fail("%v", err)
93+ }
94+ mem.Type = typ
95+ mem.Method = goName(mem.XName)
96+ if contains(reserved, mem.Method) {
97+ mem.Method += "_"
98+ }
99+ if methods[mem.Method] {
100+ return nil, n.fail("duplicate getter %s", mem.Method)
101+ }
102+ methods[mem.Method] = true
103+ elem, err := annotatedType(m, n, typ)
104+ if err != nil {
105+ return nil, err
106+ }
107+ mem.Elem = elem
108+ if n.Kind == "field" {
109+ if typ.Variable {
110+ return nil, n.fail("variable-size field outside a list is unsupported")
111+ }
112+ mem.GoType = elem
113+ mem.Size = typ.Size
114+ break
115+ }
116+ if len(n.Children) != 1 {
117+ return nil, n.fail("reply-side list requires a length expression")
118+ }
119+ mem.Count = n.Children[0]
120+ switch {
121+ case isByteElem(n.Attr["type"]):
122+ mem.ListKind, mem.GoType = "bytes", "[]byte"
123+ case typ.Variable:
124+ if typ.Size == 0 {
125+ return nil, n.fail("variable struct %s has an empty fixed prefix", typ.Name)
126+ }
127+ mem.ListKind, mem.GoType = "iter", qualified(m, typ.Module, typ.GoType+"Iter")
128+ case isScalar(typ):
129+ mem.ListKind, mem.GoType = "scalar", elemName(elem)+"List"
130+ default:
131+ mem.ListKind, mem.GoType = "struct", elemName(elem)+"List"
132+ }
133+ constant := mem.Count.Kind == "value" && !typ.Variable
134+ if constant {
135+ count, _ := strconv.Atoi(mem.Count.Text)
136+ mem.Size = count * typ.Size
137+ } else if c.Dynamic < 0 {
138+ c.Dynamic = len(c.Members)
139+ }
140+ default:
141+ return nil, n.fail("unsupported %s member %s", map[bool]string{true: "reply", false: "struct"}[reply], n.Kind)
142+ }
143+ if reply && first {
144+ if mem.Size != 1 {
145+ return nil, n.fail("first reply member must occupy the header data byte")
146+ }
147+ off = 8
148+ first = false
149+ c.Members = append(c.Members, mem)
150+ continue
151+ }
152+ first = false
153+ if c.Dynamic < 0 {
154+ off += mem.Size
155+ }
156+ c.Members = append(c.Members, mem)
157+ }
158+ if c.Dynamic >= 0 {
159+ c.Fixed = c.Members[c.Dynamic].Static
160+ } else {
161+ c.Fixed = off
162+ }
163+ return c, nil
164+}
165+
166+// ref renders a field reference as a getter call on the receiver. Replies may
167+// refer to the implicit header length word.
168+func (c *container) ref(s string) string {
169+ for _, mem := range c.Members {
170+ if mem.Kind == "field" && mem.XName == s {
171+ return "int(" + c.Recv + "." + mem.Method + "())"
172+ }
173+ }
174+ if c.Reply && s == "length" {
175+ return "int(binary.LittleEndian.Uint32(" + c.Recv + ".raw[4:]))"
176+ }
177+ return "<unresolved " + s + ">"
178+}
179+
180+func (c *container) count(mem member) (string, error) {
181+ s, err := renderExpr(mem.Count, c.ref, c.Module)
182+ if err != nil {
183+ return "", err
184+ }
185+ if strings.Contains(s, "<unresolved") {
186+ return "", mem.Count.fail("list length references an unknown field")
187+ }
188+ return s, nil
189+}
190+
191+// advance emits code moving off past member i. With fail != "" every step is
192+// bounds-checked against raw.
193+func (c *container) advance(b *strings.Builder, i int, fail string) error {
194+ mem := c.Members[i]
195+ raw := c.Recv + ".raw"
196+ switch mem.Kind {
197+ case "pad":
198+ if mem.Align > 0 {
199+ fmt.Fprintf(b, "off=(off+%d)&^%d;", mem.Align-1, mem.Align-1)
200+ } else {
201+ fmt.Fprintf(b, "off+=%d;", mem.Size)
202+ }
203+ case "field":
204+ fmt.Fprintf(b, "off+=%d;", mem.Size)
205+ case "list":
206+ n, err := c.count(mem)
207+ if err != nil {
208+ return err
209+ }
210+ if mem.ListKind == "iter" {
211+ fmt.Fprintf(b, "{ n:=%s;", n)
212+ if fail != "" {
213+ fmt.Fprintf(b, "if off>len(%s) || n<0 || n>len(%s) { %s };", raw, raw, fail)
214+ }
215+ fmt.Fprintf(b, "it:=%s(%s[off:],n); for it.Next() { off+=it.Value().Size() };", qualified(c.Module, mem.Type.Module, "New"+mem.Type.GoType+"Iter"), raw)
216+ if fail != "" {
217+ fmt.Fprintf(b, "if it.Err() { %s };", fail)
218+ }
219+ b.WriteString("};")
220+ return nil
221+ }
222+ fmt.Fprintf(b, "{ n:=%s;", n)
223+ if fail != "" {
224+ fmt.Fprintf(b, "if n<0 || n>len(%s) { %s };", raw, fail)
225+ }
226+ fmt.Fprintf(b, "off+=n*%d };", mem.Type.Size)
227+ if fail != "" {
228+ fmt.Fprintf(b, "if off>len(%s) { %s };", raw, fail)
229+ }
230+ }
231+ return nil
232+}
233+
234+// offset emits statements leaving member i's offset in off.
235+func (c *container) offset(b *strings.Builder, i int) error {
236+ if c.Members[i].Static >= 0 {
237+ fmt.Fprintf(b, "off:=%d;", c.Members[i].Static)
238+ return nil
239+ }
240+ fmt.Fprintf(b, "off:=%d;", c.Fixed)
241+ for j := c.Dynamic; j < i; j++ {
242+ if err := c.advance(b, j, ""); err != nil {
243+ return err
244+ }
245+ }
246+ return nil
247+}
248+
249+// validate emits the full-length walk used by Parse and Iter.Next.
250+func (c *container) validate(b *strings.Builder, fail string) error {
251+ if c.Dynamic < 0 {
252+ return nil
253+ }
254+ fmt.Fprintf(b, "off:=%d;", c.Fixed)
255+ for j := c.Dynamic; j < len(c.Members); j++ {
256+ if err := c.advance(b, j, fail); err != nil {
257+ return err
258+ }
259+ }
260+ fmt.Fprintf(b, "if off>len(%s.raw) { %s };", c.Recv, fail)
261+ return nil
262+}
263+
264+// getters emits one method per field and list.
265+func (c *container) getters(b *strings.Builder, typ string, lists map[string]string) error {
266+ r := c.Recv
267+ for i, mem := range c.Members {
268+ if mem.Kind == "pad" {
269+ continue
270+ }
271+ fmt.Fprintf(b, "func (%s %s) %s() %s {", r, typ, mem.Method, mem.GoType)
272+ static := mem.Static >= 0
273+ at := fmt.Sprintf("%s.raw[%d:]", r, mem.Static)
274+ if !static {
275+ if err := c.offset(b, i); err != nil {
276+ return err
277+ }
278+ at = r + ".raw[off:]"
279+ }
280+ if mem.Kind == "field" {
281+ if isScalar(mem.Type) {
282+ fmt.Fprintf(b, "return %s };\n", getScalar(mem.Type, mem.GoType, at))
283+ } else {
284+ fmt.Fprintf(b, "return %s(%s) };\n", qualified(c.Module, mem.Type.Module, "Get"+mem.Type.GoType), at)
285+ }
286+ continue
287+ }
288+ if static {
289+ fmt.Fprintf(b, "off:=%d;", mem.Static)
290+ }
291+ n, err := c.count(mem)
292+ if err != nil {
293+ return err
294+ }
295+ switch mem.ListKind {
296+ case "iter":
297+ fmt.Fprintf(b, "return %s(%s.raw[off:],%s) };\n", qualified(c.Module, mem.Type.Module, "New"+mem.Type.GoType+"Iter"), r, n)
298+ case "bytes":
299+ fmt.Fprintf(b, "end:=off+%s; return %s.raw[off:end:end] };\n", n, r)
300+ default:
301+ fmt.Fprintf(b, "end:=off+(%s)*%d; return %s{b:%s.raw[off:end:end]} };\n", n, mem.Type.Size, mem.GoType, r)
302+ lists[mem.GoType] = listDecl(c.Module, mem.GoType, mem.Elem, mem.Type)
303+ }
304+ }
305+ return nil
306+}
307+
308+func listDecl(m *Module, name, goType string, elem *Type) string {
309+ var b strings.Builder
310+ fmt.Fprintf(&b, "// %s is a borrowed view over a wire list of %s. %s\n", name, goType, "It shares the lifetime of the bytes it was taken from; copy what you keep.")
311+ fmt.Fprintf(&b, "type %s struct { b []byte };\n", name)
312+ fmt.Fprintf(&b, "func (l %s) Len() int { return len(l.b)/%d };\n", name, elem.Size)
313+ src := fmt.Sprintf("l.b[i*%d:]", elem.Size)
314+ if isScalar(elem) {
315+ fmt.Fprintf(&b, "func (l %s) At(i int) %s { return %s };\n", name, goType, getScalar(elem, goType, src))
316+ } else {
317+ fmt.Fprintf(&b, "func (l %s) At(i int) %s { return %s(%s) };\n", name, goType, qualified(m, elem.Module, "Get"+elem.GoType), src)
318+ }
319+ return b.String()
320+}
321+
322+type replyIR struct {
323+ Req string
324+ C *container
325+}
326+
327+func resolveReplies(m *Module) ([]replyIR, []*container, error) {
328+ var replies []replyIR
329+ for _, n := range m.Root.Children {
330+ if n.Kind != "request" {
331+ continue
332+ }
333+ for _, r := range n.Children {
334+ if r.Kind != "reply" {
335+ continue
336+ }
337+ name := goName(n.Attr["name"])
338+ for _, s := range []string{name + "Reply", "Parse" + name + "Reply"} {
339+ if err := m.claim(s, r); err != nil {
340+ return nil, nil, err
341+ }
342+ }
343+ c, err := buildContainer(m, name+"Reply", "r", r.Children, true, "Raw")
344+ if err != nil {
345+ return nil, nil, err
346+ }
347+ replies = append(replies, replyIR{Req: name, C: c})
348+ }
349+ }
350+ var views []*container
351+ for _, t := range m.Types {
352+ if t.Kind != "struct" || !t.Variable {
353+ continue
354+ }
355+ for _, s := range []string{t.GoType + "View", t.GoType + "Iter", "New" + t.GoType + "Iter"} {
356+ if err := m.claim(s, t.Node); err != nil {
357+ return nil, nil, err
358+ }
359+ }
360+ c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, false, "Raw Size")
361+ if err != nil {
362+ return nil, nil, err
363+ }
364+ views = append(views, c)
365+ }
366+ return replies, views, nil
367+}
368+
369+func replyDecl(r replyIR, lists map[string]string) (string, error) {
370+ c := r.C
371+ var b strings.Builder
372+ fmt.Fprintf(&b, "// %s is a zero-copy view over a %s reply; offsets count from reply byte 0.\n// %s\n", c.Name, r.Req, replyLifetime)
373+ fmt.Fprintf(&b, "type %s struct { raw []byte };\n", c.Name)
374+ fmt.Fprintf(&b, "// Parse%s validates raw (the whole reply, 32+4*length bytes) once; getters do not re-check.\n", c.Name)
375+ fmt.Fprintf(&b, "func Parse%s(raw []byte) (%s, bool) {", c.Name, c.Name)
376+ fixed := max(c.Fixed, 32)
377+ fmt.Fprintf(&b, "if len(raw)<%d { return %s{}, false };", fixed, c.Name)
378+ fmt.Fprintf(&b, "r:=%s{raw:raw};", c.Name)
379+ if err := c.validate(&b, "return "+c.Name+"{}, false"); err != nil {
380+ return "", err
381+ }
382+ b.WriteString("return r, true };\n")
383+ fmt.Fprintf(&b, "func (r %s) Raw() []byte { return r.raw };\n", c.Name)
384+ if err := c.getters(&b, c.Name, lists); err != nil {
385+ return "", err
386+ }
387+ return b.String(), nil
388+}
389+
390+func viewDecl(c *container, lists map[string]string) (string, error) {
391+ base := strings.TrimSuffix(c.Name, "View")
392+ iter := base + "Iter"
393+ var b strings.Builder
394+ fmt.Fprintf(&b, "// %s is a zero-copy view over one wire-encoded %s, yielded by %s.\n// It shares the lifetime of the bytes it was taken from (for replies: until the Handler callback returns). Copy what you keep.\n", c.Name, base, iter)
395+ fmt.Fprintf(&b, "type %s struct { raw []byte };\n", c.Name)
396+ fmt.Fprintf(&b, "func (v %s) Raw() []byte { return v.raw };\n", c.Name)
397+ fmt.Fprintf(&b, "// Size is the element's wire size, including trailing alignment.\nfunc (v %s) Size() int { return len(v.raw) };\n", c.Name)
398+ // size%s returns the element's wire size at the start of p, -1 when malformed.
399+ fmt.Fprintf(&b, "func size%s(p []byte) int { if len(p)<%d { return -1 }; v:=%s{raw:p}; _=v;", base, c.Fixed, c.Name)
400+ if c.Dynamic < 0 {
401+ fmt.Fprintf(&b, "return %d };\n", c.Fixed)
402+ } else {
403+ if err := c.validate(&b, "return -1"); err != nil {
404+ return "", err
405+ }
406+ b.WriteString("return off };\n")
407+ }
408+ if err := c.getters(&b, c.Name, lists); err != nil {
409+ return "", err
410+ }
411+ fmt.Fprintf(&b, "// %s walks n consecutive %s elements; Err reports a malformed length.\n// It shares the lifetime of the bytes it walks.\n", iter, base)
412+ fmt.Fprintf(&b, "type %s struct { b []byte; n int; v %s; err bool };\n", iter, c.Name)
413+ fmt.Fprintf(&b, "// New%s iterates n elements encoded at the start of b.\nfunc New%s(b []byte, n int) %s { return %s{b:b, n:n} };\n", iter, iter, iter, iter)
414+ fmt.Fprintf(&b, "func (it *%s) Next() bool { if it.n<=0 || it.err { return false }; s:=size%s(it.b); if s<0 { it.err=true; it.n=0; return false }; it.v=%s{raw:it.b[:s:s]}; it.b=it.b[s:]; it.n--; return true };\n", iter, base, c.Name)
415+ fmt.Fprintf(&b, "func (it *%s) Value() %s { return it.v };\n", iter, c.Name)
416+ fmt.Fprintf(&b, "func (it *%s) Err() bool { return it.err };\n", iter)
417+ return b.String(), nil
418+}
419+
420+// touch emits fuzz statements calling every getter and walking every list,
421+// recursing into variable struct views.
422+func touch(b *strings.Builder, c *container, expr string, depth int, all map[*Type]*container, failIter string) {
423+ for _, mem := range c.Members {
424+ switch {
425+ case mem.Kind == "field":
426+ fmt.Fprintf(b, "_ = %s.%s();", expr, mem.Method)
427+ case mem.Kind == "list" && mem.ListKind == "bytes":
428+ fmt.Fprintf(b, "_ = len(%s.%s());", expr, mem.Method)
429+ case mem.Kind == "list" && mem.ListKind == "iter":
430+ it := fmt.Sprintf("it%d", depth)
431+ fmt.Fprintf(b, "{ %s := %s.%s(); for %s.Next() {", it, expr, mem.Method, it)
432+ if child := all[mem.Type]; child != nil {
433+ touch(b, child, it+".Value()", depth+1, all, failIter)
434+ }
435+ fmt.Fprintf(b, "}; if %s.Err() { %s } };", it, failIter)
436+ case mem.Kind == "list":
437+ l := fmt.Sprintf("l%d", depth)
438+ fmt.Fprintf(b, "{ %s := %s.%s(); for i := 0; i < %s.Len(); i++ { _ = %s.At(i) } };", l, expr, mem.Method, l, l)
439+ }
440+ }
441+}
442+
443+func emitReplies(modules []*Module) error {
444+ type pending struct {
445+ m *Module
446+ replies []replyIR
447+ views []*container
448+ }
449+ var all []pending
450+ views := map[*Type]*container{}
451+ for _, m := range modules {
452+ replies, vs, err := resolveReplies(m)
453+ if err != nil {
454+ return err
455+ }
456+ for _, v := range vs {
457+ for _, t := range m.Types {
458+ if t.GoType+"View" == v.Name {
459+ views[t] = v
460+ }
461+ }
462+ }
463+ all = append(all, pending{m, replies, vs})
464+ }
465+ for _, p := range all {
466+ m := p.m
467+ lists := map[string]string{}
468+ var decls []string
469+ for _, r := range p.replies {
470+ d, err := replyDecl(r, lists)
471+ if err != nil {
472+ return err
473+ }
474+ decls = append(decls, d)
475+ }
476+ for _, v := range p.views {
477+ d, err := viewDecl(v, lists)
478+ if err != nil {
479+ return err
480+ }
481+ decls = append(decls, d)
482+ }
483+ var names []string
484+ for name := range lists {
485+ if err := m.claim(name, m.Root); err != nil {
486+ return err
487+ }
488+ names = append(names, name)
489+ }
490+ sort.Strings(names)
491+ for _, name := range names {
492+ decls = append(decls, lists[name])
493+ }
494+ if len(decls) == 0 {
495+ continue
496+ }
497+ decls = append([]string{importsFor(m, strings.Join(decls, "\n"))}, decls...)
498+ data := struct {
499+ Pkg string
500+ Decls []string
501+ }{Pkg: m.Pkg, Decls: decls}
502+ if err := writeTemplate(filepath.Join(m.Pkg, "replies_generated.go"), "replies", data); err != nil {
503+ return err
504+ }
505+ if err := emitReplyFuzz(m, p.replies, views); err != nil {
506+ return err
507+ }
508+ }
509+ return nil
510+}
511+
512+// emitReplyFuzz writes FuzzParse<Req>Reply for every reply. Seeds are the
513+// zero-filled fixed prefix plus any captured goldens <Req>*.reply.hex.
514+func emitReplyFuzz(m *Module, replies []replyIR, views map[*Type]*container) error {
515+ var decls []string
516+ usesGolden := false
517+ for _, r := range replies {
518+ var b strings.Builder
519+ c := r.C
520+ fmt.Fprintf(&b, "func FuzzParse%s(f *testing.F) {\n", c.Name)
521+ fmt.Fprintf(&b, "f.Add(make([]byte, %d));\n", (max(c.Fixed, 32)+3)&^3)
522+ goldens, _ := filepath.Glob(filepath.Join("testdata/golden", m.Pkg, r.Req+"*.reply.hex"))
523+ sort.Strings(goldens)
524+ for _, g := range goldens {
525+ base := filepath.Base(g)
526+ if strings.TrimSuffix(base, ".reply.hex") != r.Req && !startsUpper(strings.TrimPrefix(strings.TrimSuffix(base, ".reply.hex"), r.Req)) {
527+ continue
528+ }
529+ usesGolden = true
530+ fmt.Fprintf(&b, "if raw, err := xtest.LoadHex(%q); err != nil { f.Fatal(err) } else { f.Add(raw) };\n", "../"+filepath.ToSlash(g))
531+ }
532+ fmt.Fprintf(&b, "f.Fuzz(func(t *testing.T, raw []byte) {\nr, ok := Parse%s(raw);\nif !ok {", c.Name)
533+ if c.Dynamic < 0 {
534+ fmt.Fprintf(&b, "if len(raw) >= %d { t.Fatal(\"rejected a complete fixed-size reply\") };", max(c.Fixed, 32))
535+ }
536+ b.WriteString("return };\n")
537+ b.WriteString("if len(r.Raw()) != len(raw) { t.Fatal(\"Raw must return the parsed bytes\") };\n")
538+ touch(&b, c, "r", 0, views, `t.Fatal("iterator error after successful Parse")`)
539+ b.WriteString("\n})\n}\n")
540+ decls = append(decls, b.String())
541+ }
542+ imports := "import \"testing\""
543+ if usesGolden {
544+ imports += "\nimport \"bnema.dev/xturbo/internal/xtest\""
545+ }
546+ decls = append([]string{imports}, decls...)
547+ data := struct {
548+ Pkg string
549+ Decls []string
550+ }{Pkg: m.Pkg, Decls: decls}
551+ return writeTemplate(filepath.Join(m.Pkg, "replies_fuzz_test.go"), "replies", data)
552+}
553+
554+func startsUpper(s string) bool { return s != "" && s[0] >= 'A' && s[0] <= 'Z' }
A cmd/xtgen/replies_test.go +70 -0
@@ -0,0 +1,70 @@
1+package main
2+
3+import "testing"
4+
5+// TestReplyLayouts checks reply containers against the independent packet-size
6+// walker and pins list-view classification for representative replies.
7+func TestReplyLayouts(t *testing.T) {
8+ kinds := map[string]map[string]string{
9+ "xproto.GetPropertyReply": {"Value": "bytes"},
10+ "xproto.QueryTreeReply": {"Children": "scalar"},
11+ "xproto.ListExtensionsReply": {"Names": "iter"},
12+ "xproto.QueryFontReply": {"Properties": "struct", "CharInfos": "struct"},
13+ "xproto.ListHostsReply": {"Hosts": "iter"},
14+ "render.QueryPictFormatsReply": {"Formats": "struct", "Screens": "iter", "Subpixels": "scalar"},
15+ "xfixes.GetCursorImageAndNameReply": {"CursorImage": "scalar", "Name": "bytes"},
16+ }
17+ for _, m := range testModules(t) {
18+ replies, _, err := resolveReplies(m)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ for _, r := range replies {
23+ var node *Node
24+ for _, n := range m.Root.Children {
25+ if n.Kind == "request" && goName(n.Attr["name"]) == r.Req {
26+ for _, c := range n.Children {
27+ if c.Kind == "reply" {
28+ node = c
29+ }
30+ }
31+ }
32+ }
33+ want, err := packetFixed(m, node, true)
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ if got := max(r.C.Fixed, 32); got != want {
38+ t.Errorf("%s.%s fixed %d, packet walker %d", m.Pkg, r.C.Name, got, want)
39+ }
40+ for _, mem := range r.C.Members {
41+ if exp, ok := kinds[m.Pkg+"."+r.C.Name]; ok && mem.Kind == "list" {
42+ if exp[mem.Method] != mem.ListKind {
43+ t.Errorf("%s.%s.%s kind %s want %s", m.Pkg, r.C.Name, mem.Method, mem.ListKind, exp[mem.Method])
44+ }
45+ delete(exp, mem.Method)
46+ }
47+ }
48+ }
49+ }
50+ for name, rest := range kinds {
51+ if len(rest) != 0 {
52+ t.Errorf("%s: unchecked lists %v", name, rest)
53+ }
54+ }
55+}
56+
57+func TestReplyReservedGetter(t *testing.T) {
58+ m := &Module{Pkg: "x", Symbols: map[string]*Type{}, Names: map[string]*Node{}}
59+ nodes := []*Node{
60+ {Kind: "pad", Attr: map[string]string{"bytes": "1"}},
61+ {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}},
62+ }
63+ c, err := buildContainer(m, "XReply", "r", nodes, true, "Raw")
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ if c.Members[1].Method != "Raw_" || c.Members[1].Static != 8 || c.Fixed != 12 {
68+ t.Fatalf("%+v", c.Members[1])
69+ }
70+}
@@ -0,0 +1,70 @@
1+package main
2+
3+import "testing"
4+
5+// TestReplyLayouts checks reply containers against the independent packet-size
6+// walker and pins list-view classification for representative replies.
7+func TestReplyLayouts(t *testing.T) {
8+ kinds := map[string]map[string]string{
9+ "xproto.GetPropertyReply": {"Value": "bytes"},
10+ "xproto.QueryTreeReply": {"Children": "scalar"},
11+ "xproto.ListExtensionsReply": {"Names": "iter"},
12+ "xproto.QueryFontReply": {"Properties": "struct", "CharInfos": "struct"},
13+ "xproto.ListHostsReply": {"Hosts": "iter"},
14+ "render.QueryPictFormatsReply": {"Formats": "struct", "Screens": "iter", "Subpixels": "scalar"},
15+ "xfixes.GetCursorImageAndNameReply": {"CursorImage": "scalar", "Name": "bytes"},
16+ }
17+ for _, m := range testModules(t) {
18+ replies, _, err := resolveReplies(m)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ for _, r := range replies {
23+ var node *Node
24+ for _, n := range m.Root.Children {
25+ if n.Kind == "request" && goName(n.Attr["name"]) == r.Req {
26+ for _, c := range n.Children {
27+ if c.Kind == "reply" {
28+ node = c
29+ }
30+ }
31+ }
32+ }
33+ want, err := packetFixed(m, node, true)
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ if got := max(r.C.Fixed, 32); got != want {
38+ t.Errorf("%s.%s fixed %d, packet walker %d", m.Pkg, r.C.Name, got, want)
39+ }
40+ for _, mem := range r.C.Members {
41+ if exp, ok := kinds[m.Pkg+"."+r.C.Name]; ok && mem.Kind == "list" {
42+ if exp[mem.Method] != mem.ListKind {
43+ t.Errorf("%s.%s.%s kind %s want %s", m.Pkg, r.C.Name, mem.Method, mem.ListKind, exp[mem.Method])
44+ }
45+ delete(exp, mem.Method)
46+ }
47+ }
48+ }
49+ }
50+ for name, rest := range kinds {
51+ if len(rest) != 0 {
52+ t.Errorf("%s: unchecked lists %v", name, rest)
53+ }
54+ }
55+}
56+
57+func TestReplyReservedGetter(t *testing.T) {
58+ m := &Module{Pkg: "x", Symbols: map[string]*Type{}, Names: map[string]*Node{}}
59+ nodes := []*Node{
60+ {Kind: "pad", Attr: map[string]string{"bytes": "1"}},
61+ {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}},
62+ }
63+ c, err := buildContainer(m, "XReply", "r", nodes, true, "Raw")
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ if c.Members[1].Method != "Raw_" || c.Members[1].Static != 8 || c.Fixed != 12 {
68+ t.Fatalf("%+v", c.Members[1])
69+ }
70+}
A cmd/xtgen/requests.go +359 -0
@@ -0,0 +1,359 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "strconv"
7+ "strings"
8+)
9+
10+type Request struct {
11+ Name string
12+ Opcode uint8
13+ DataByte *Field
14+ Fixed int
15+ Params, Fields []Field
16+ Node *Node
17+ Derived map[string]string
18+ Lists []Field
19+ Values *ValueList
20+}
21+
22+// resolveRequests lowers scalar, list and mask-bitcase value-list requests.
23+func resolveRequests(m *Module) ([]Request, error) {
24+ var out []Request
25+ for _, n := range m.Root.Children {
26+ if n.Kind != "request" {
27+ continue
28+ }
29+ name := goName(n.Attr["name"])
30+ if err := m.claim(name, n); err != nil {
31+ return nil, err
32+ }
33+ opcode, _ := strconv.Atoi(n.Attr["opcode"])
34+ r := Request{Name: name, Opcode: uint8(opcode), Node: n, Derived: map[string]string{}}
35+ var err error
36+ r.Values, err = resolveValues(m, n, name)
37+ if err != nil {
38+ return nil, err
39+ }
40+ refs := map[string]int{}
41+ var countRefs func(*Node)
42+ countRefs = func(x *Node) {
43+ if x.Kind == "reply" || x.Kind == "doc" {
44+ return
45+ }
46+ if x.Kind == "fieldref" {
47+ refs[x.Text]++
48+ }
49+ for _, c := range x.Children {
50+ countRefs(c)
51+ }
52+ }
53+ countRefs(n)
54+ for _, x := range n.Children {
55+ if x.Kind == "list" && len(x.Children) == 1 && x.Children[0].Kind == "fieldref" && refs[x.Children[0].Text] == 1 {
56+ r.Derived[x.Children[0].Text] = paramName(x.Attr["name"])
57+ }
58+ }
59+ dynamic := false
60+ off := 4
61+ first := true
62+ for _, n := range n.Children {
63+ if n.Kind == "reply" || n.Kind == "doc" {
64+ continue
65+ }
66+ if n.Kind == "switch" {
67+ r.Params = append(r.Params, Field{Param: "v", GoType: "*" + name + "Values"})
68+ continue
69+ }
70+ f := Field{Name: goName(n.Attr["name"]), Param: paramName(n.Attr["name"]), Kind: n.Kind, Node: n, Off: off - 4}
71+ if n.Kind == "pad" {
72+ if n.Attr["align"] != "" {
73+ a, _ := strconv.Atoi(n.Attr["align"])
74+ f.Size = (-off) & (a - 1)
75+ } else {
76+ f.Size, _ = strconv.Atoi(n.Attr["bytes"])
77+ }
78+ } else {
79+ typ, err := lookup(m, n.Attr["type"])
80+ if err != nil {
81+ return nil, n.fail("%v", err)
82+ }
83+ if typ.Variable && n.Kind != "list" {
84+ return nil, n.fail("variable field requires list support")
85+ }
86+ f.Type = typ
87+ f.Size = typ.Size
88+ f.GoType = qualified(m, typ.Module, typ.GoType)
89+ if enum := n.Attr["enum"]; enum != "" {
90+ e, owner := findEnum(m, enum)
91+ if e == nil {
92+ return nil, n.fail("unknown enum %s", enum)
93+ }
94+ f.GoType = qualified(m, owner, e.Name)
95+ }
96+ if mask := n.Attr["mask"]; mask != "" {
97+ e, owner := findEnum(m, mask)
98+ if e == nil {
99+ return nil, n.fail("unknown mask %s", mask)
100+ }
101+ f.GoType = qualified(m, owner, e.Name)
102+ }
103+ if n.Kind == "list" {
104+ f.Size = 0
105+ f.GoType = "[]" + f.GoType
106+ if n.Attr["type"] == "char" {
107+ f.GoType = "string"
108+ }
109+ if n.Attr["type"] == "void" || n.Attr["type"] == "BYTE" || n.Attr["type"] == "CARD8" {
110+ f.GoType = "[]byte"
111+ }
112+ if len(n.Children) > 0 {
113+ f.Expr = n.Children[0]
114+ }
115+ if m.Header == "xproto" && r.Name == "SendEvent" {
116+ f.GoType = "*[32]byte"
117+ }
118+ r.Lists = append(r.Lists, f)
119+ r.Params = append(r.Params, f)
120+ } else if n.Kind == "field" && r.Derived[n.Attr["name"]] == "" {
121+ if r.Values != nil && n == r.Values.Mask.Node {
122+ f.Param = "v.Mask"
123+ } else {
124+ r.Params = append(r.Params, f)
125+ }
126+ }
127+ if n.Kind == "exprfield" {
128+ f.Expr = n.Children[0]
129+ }
130+ }
131+ // Core's first XML byte is header byte 1; extensions have no data byte.
132+ if first && m.ExtName == "" {
133+ if f.Size != 1 {
134+ return nil, n.fail("core request first member must occupy header data byte")
135+ }
136+ if f.Kind != "pad" {
137+ copy := f
138+ r.DataByte = &copy
139+ }
140+ first = false
141+ continue
142+ }
143+ first = false
144+ if f.Kind == "list" {
145+ dynamic = true
146+ }
147+ if dynamic {
148+ f.Off = -1
149+ }
150+ if f.Kind == "pad" && n.Attr["align"] != "" {
151+ f.Align, _ = strconv.Atoi(n.Attr["align"])
152+ }
153+ r.Fields = append(r.Fields, f)
154+ off += f.Size
155+ }
156+ r.Fixed = off - 4
157+ out = append(out, r)
158+ }
159+ return out, nil
160+}
161+func requestDecl(m *Module, r Request) (string, error) {
162+ var b strings.Builder
163+ if r.Values != nil {
164+ b.WriteString(valuesDecl(r.Name, r.Values))
165+ }
166+ fmt.Fprintf(&b, "func %s(b *xturbo.Buf", r.Name)
167+ for _, p := range r.Params {
168+ fmt.Fprintf(&b, ", %s %s", p.Param, p.GoType)
169+ }
170+ b.WriteString(") xturbo.Seq {\n")
171+ ref := func(s string) string {
172+ // xcb-proto permits an exprfield to refer to a trailing list's
173+ // implicit <list>_len (QueryTextExtents.string_len).
174+ declared := false
175+ for _, n := range r.Node.Children {
176+ if n.Kind == "field" && n.Attr["name"] == s {
177+ declared = true
178+ }
179+ }
180+ for _, list := range r.Lists {
181+ if !declared && s == list.Node.Attr["name"]+"_len" {
182+ return "len(" + list.Param + ")"
183+ }
184+ }
185+ if list := r.Derived[s]; list != "" {
186+ return "len(" + list + ")"
187+ }
188+ return "int(" + paramName(s) + ")"
189+ }
190+ // Derived lengths are cast only after checking their wire width.
191+ for _, f := range append(append([]Field{}, r.Fields...), r.Params...) {
192+ if f.Kind != "field" || r.Derived[f.Node.Attr["name"]] == "" {
193+ continue
194+ }
195+ if f.Type.Size < 8 {
196+ fmt.Fprintf(&b, "if uint64(%s)>%d { b.Fail(xturbo.ErrBadLength); return 0 };", ref(f.Node.Attr["name"]), uint64(1)<<(8*f.Type.Size)-1)
197+ }
198+ }
199+ if r.DataByte != nil {
200+ f := r.DataByte
201+ if list := r.Derived[f.Node.Attr["name"]]; list != "" {
202+ fmt.Fprintf(&b, "if len(%s)>255 { b.Fail(xturbo.ErrBadLength); return 0 };", list)
203+ }
204+ }
205+ for _, f := range r.Lists {
206+ if f.Expr != nil && !(f.Expr.Kind == "fieldref" && r.Derived[f.Expr.Text] != "") {
207+ ex, err := renderExpr(f.Expr, ref, m)
208+ if err != nil {
209+ return "", err
210+ }
211+ fmt.Fprintf(&b, "if len(%s)!=%s { b.Fail(xturbo.ErrBadLength); return 0 };", f.Param, ex)
212+ }
213+ }
214+ body := fmt.Sprint((r.Fixed + 3) &^ 3)
215+ if len(r.Lists) > 0 {
216+ b.WriteString("size:=0;")
217+ for _, f := range r.Fields {
218+ if f.Kind == "list" {
219+ if f.Type.Variable {
220+ fmt.Fprintf(&b, "for i:=range %s { size+=%s[i].Size() };", f.Param, f.Param)
221+ } else {
222+ fmt.Fprintf(&b, "size+=len(%s)*%d;", f.Param, f.Type.Size)
223+ }
224+ } else if f.Align > 0 {
225+ fmt.Fprintf(&b, "size=(size+%d)&^%d;", f.Align-1, f.Align-1)
226+ } else {
227+ fmt.Fprintf(&b, "size+=%d;", f.Size)
228+ }
229+ }
230+ b.WriteString("size=(size+3)&^3;")
231+ body = "size"
232+ }
233+ if r.Values != nil {
234+ body += "+4*bits.OnesCount32(uint32(v.Mask))"
235+ }
236+ major := fmt.Sprint(r.Opcode)
237+ data := "0"
238+ if m.ExtName != "" {
239+ fmt.Fprintf(&b, "if b.Ext==nil { b.Fail(xturbo.ErrExtMissing); return 0 }; major:=b.Ext.Major(xturbo.Ext%s); if major==0 { b.Fail(xturbo.ErrExtMissing); return 0 };\n", m.ExtName)
240+ major = "major"
241+ data = fmt.Sprint(r.Opcode)
242+ } else if r.DataByte != nil {
243+ f := r.DataByte
244+ if f.Type.Base == "bool" {
245+ value := f.Param
246+ if f.Kind == "exprfield" {
247+ ex, err := renderExpr(f.Expr, ref, m)
248+ if err != nil {
249+ return "", err
250+ }
251+ value = "(" + ex + " != 0)"
252+ }
253+ fmt.Fprintf(&b, "var data uint8; if %s { data=1 };", value)
254+ data = "data"
255+ } else {
256+ data = "uint8(" + ref(f.Node.Attr["name"]) + ")"
257+ }
258+ }
259+ fmt.Fprintf(&b, "p,seq:=b.Request(%s,%s,%s); if p==nil { return 0 };\n", major, data, body)
260+ if len(r.Lists) > 0 {
261+ b.WriteString("off:=0;")
262+ }
263+ for _, f := range r.Fields {
264+ if f.Kind == "pad" {
265+ if len(r.Lists) > 0 {
266+ if f.Align > 0 {
267+ fmt.Fprintf(&b, "off=(off+%d)&^%d;", f.Align-1, f.Align-1)
268+ } else {
269+ fmt.Fprintf(&b, "off+=%d;", f.Size)
270+ }
271+ }
272+ continue
273+ }
274+ if f.Kind == "list" {
275+ if f.GoType == "*[32]byte" {
276+ fmt.Fprintf(&b, "copy(p[off:],%s[:]);", f.Param)
277+ } else if f.GoType == "string" || f.GoType == "[]byte" {
278+ fmt.Fprintf(&b, "copy(p[off:],%s); off+=len(%s);", f.Param, f.Param)
279+ } else {
280+ fmt.Fprintf(&b, "for i:=range %s {", f.Param)
281+ if isScalar(f.Type) {
282+ b.WriteString(putScalar(f.Type, f.Param+"[i]", "p[off:]"))
283+ } else {
284+ fmt.Fprintf(&b, "%s[i].Put(p[off:]);", f.Param)
285+ }
286+ if f.Type.Variable {
287+ fmt.Fprintf(&b, "off+=%s[i].Size();", f.Param)
288+ } else {
289+ fmt.Fprintf(&b, "off+=%d;", f.Type.Size)
290+ }
291+ b.WriteString("};")
292+ }
293+ continue
294+ }
295+ src := f.Param
296+ if list := r.Derived[f.Node.Attr["name"]]; list != "" {
297+ src = "len(" + list + ")"
298+ }
299+ if f.Kind == "exprfield" {
300+ var err error
301+ src, err = renderExpr(f.Expr, ref, m)
302+ if err != nil {
303+ return "", err
304+ }
305+ }
306+ dst := fmt.Sprintf("p[%d:]", f.Off)
307+ if len(r.Lists) > 0 {
308+ dst = "p[off:]"
309+ }
310+ if isScalar(f.Type) {
311+ b.WriteString(putScalar(f.Type, src, dst))
312+ } else {
313+ fmt.Fprintf(&b, "%s.Put(%s);", src, dst)
314+ }
315+ if len(r.Lists) > 0 {
316+ fmt.Fprintf(&b, "off+=%d;", f.Size)
317+ }
318+ }
319+ if r.Values != nil {
320+ fmt.Fprintf(&b, "off:=%d;", r.Fixed)
321+ for _, f := range r.Values.Fields {
322+ fmt.Fprintf(&b, "if v.Mask & %s != 0 {", f.Bit)
323+ b.WriteString(putScalar(f.Field.Type, "v."+f.Field.Name, "p[off:]"))
324+ b.WriteString("off+=4 };")
325+ }
326+ b.WriteString("_ = off;")
327+ }
328+ b.WriteString("return seq }\n")
329+ return b.String(), nil
330+}
331+func emitRequests(modules []*Module) error {
332+ for _, m := range modules {
333+ requests, err := resolveRequests(m)
334+ if err != nil {
335+ return err
336+ }
337+ data := struct {
338+ Pkg string
339+ Decls []string
340+ }{Pkg: m.Pkg}
341+ for _, r := range requests {
342+ decl, err := requestDecl(m, r)
343+ if err != nil {
344+ return err
345+ }
346+ data.Decls = append(data.Decls, decl)
347+ }
348+ all := strings.Join(data.Decls, "\n")
349+ imp := importsFor(m, all)
350+ if len(requests) > 0 {
351+ imp += "\nimport \"bnema.dev/xturbo\""
352+ }
353+ data.Decls = append([]string{imp}, data.Decls...)
354+ if err := writeTemplate(filepath.Join(m.Pkg, "requests_generated.go"), "requests", data); err != nil {
355+ return err
356+ }
357+ }
358+ return nil
359+}
@@ -0,0 +1,359 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "strconv"
7+ "strings"
8+)
9+
10+type Request struct {
11+ Name string
12+ Opcode uint8
13+ DataByte *Field
14+ Fixed int
15+ Params, Fields []Field
16+ Node *Node
17+ Derived map[string]string
18+ Lists []Field
19+ Values *ValueList
20+}
21+
22+// resolveRequests lowers scalar, list and mask-bitcase value-list requests.
23+func resolveRequests(m *Module) ([]Request, error) {
24+ var out []Request
25+ for _, n := range m.Root.Children {
26+ if n.Kind != "request" {
27+ continue
28+ }
29+ name := goName(n.Attr["name"])
30+ if err := m.claim(name, n); err != nil {
31+ return nil, err
32+ }
33+ opcode, _ := strconv.Atoi(n.Attr["opcode"])
34+ r := Request{Name: name, Opcode: uint8(opcode), Node: n, Derived: map[string]string{}}
35+ var err error
36+ r.Values, err = resolveValues(m, n, name)
37+ if err != nil {
38+ return nil, err
39+ }
40+ refs := map[string]int{}
41+ var countRefs func(*Node)
42+ countRefs = func(x *Node) {
43+ if x.Kind == "reply" || x.Kind == "doc" {
44+ return
45+ }
46+ if x.Kind == "fieldref" {
47+ refs[x.Text]++
48+ }
49+ for _, c := range x.Children {
50+ countRefs(c)
51+ }
52+ }
53+ countRefs(n)
54+ for _, x := range n.Children {
55+ if x.Kind == "list" && len(x.Children) == 1 && x.Children[0].Kind == "fieldref" && refs[x.Children[0].Text] == 1 {
56+ r.Derived[x.Children[0].Text] = paramName(x.Attr["name"])
57+ }
58+ }
59+ dynamic := false
60+ off := 4
61+ first := true
62+ for _, n := range n.Children {
63+ if n.Kind == "reply" || n.Kind == "doc" {
64+ continue
65+ }
66+ if n.Kind == "switch" {
67+ r.Params = append(r.Params, Field{Param: "v", GoType: "*" + name + "Values"})
68+ continue
69+ }
70+ f := Field{Name: goName(n.Attr["name"]), Param: paramName(n.Attr["name"]), Kind: n.Kind, Node: n, Off: off - 4}
71+ if n.Kind == "pad" {
72+ if n.Attr["align"] != "" {
73+ a, _ := strconv.Atoi(n.Attr["align"])
74+ f.Size = (-off) & (a - 1)
75+ } else {
76+ f.Size, _ = strconv.Atoi(n.Attr["bytes"])
77+ }
78+ } else {
79+ typ, err := lookup(m, n.Attr["type"])
80+ if err != nil {
81+ return nil, n.fail("%v", err)
82+ }
83+ if typ.Variable && n.Kind != "list" {
84+ return nil, n.fail("variable field requires list support")
85+ }
86+ f.Type = typ
87+ f.Size = typ.Size
88+ f.GoType = qualified(m, typ.Module, typ.GoType)
89+ if enum := n.Attr["enum"]; enum != "" {
90+ e, owner := findEnum(m, enum)
91+ if e == nil {
92+ return nil, n.fail("unknown enum %s", enum)
93+ }
94+ f.GoType = qualified(m, owner, e.Name)
95+ }
96+ if mask := n.Attr["mask"]; mask != "" {
97+ e, owner := findEnum(m, mask)
98+ if e == nil {
99+ return nil, n.fail("unknown mask %s", mask)
100+ }
101+ f.GoType = qualified(m, owner, e.Name)
102+ }
103+ if n.Kind == "list" {
104+ f.Size = 0
105+ f.GoType = "[]" + f.GoType
106+ if n.Attr["type"] == "char" {
107+ f.GoType = "string"
108+ }
109+ if n.Attr["type"] == "void" || n.Attr["type"] == "BYTE" || n.Attr["type"] == "CARD8" {
110+ f.GoType = "[]byte"
111+ }
112+ if len(n.Children) > 0 {
113+ f.Expr = n.Children[0]
114+ }
115+ if m.Header == "xproto" && r.Name == "SendEvent" {
116+ f.GoType = "*[32]byte"
117+ }
118+ r.Lists = append(r.Lists, f)
119+ r.Params = append(r.Params, f)
120+ } else if n.Kind == "field" && r.Derived[n.Attr["name"]] == "" {
121+ if r.Values != nil && n == r.Values.Mask.Node {
122+ f.Param = "v.Mask"
123+ } else {
124+ r.Params = append(r.Params, f)
125+ }
126+ }
127+ if n.Kind == "exprfield" {
128+ f.Expr = n.Children[0]
129+ }
130+ }
131+ // Core's first XML byte is header byte 1; extensions have no data byte.
132+ if first && m.ExtName == "" {
133+ if f.Size != 1 {
134+ return nil, n.fail("core request first member must occupy header data byte")
135+ }
136+ if f.Kind != "pad" {
137+ copy := f
138+ r.DataByte = &copy
139+ }
140+ first = false
141+ continue
142+ }
143+ first = false
144+ if f.Kind == "list" {
145+ dynamic = true
146+ }
147+ if dynamic {
148+ f.Off = -1
149+ }
150+ if f.Kind == "pad" && n.Attr["align"] != "" {
151+ f.Align, _ = strconv.Atoi(n.Attr["align"])
152+ }
153+ r.Fields = append(r.Fields, f)
154+ off += f.Size
155+ }
156+ r.Fixed = off - 4
157+ out = append(out, r)
158+ }
159+ return out, nil
160+}
161+func requestDecl(m *Module, r Request) (string, error) {
162+ var b strings.Builder
163+ if r.Values != nil {
164+ b.WriteString(valuesDecl(r.Name, r.Values))
165+ }
166+ fmt.Fprintf(&b, "func %s(b *xturbo.Buf", r.Name)
167+ for _, p := range r.Params {
168+ fmt.Fprintf(&b, ", %s %s", p.Param, p.GoType)
169+ }
170+ b.WriteString(") xturbo.Seq {\n")
171+ ref := func(s string) string {
172+ // xcb-proto permits an exprfield to refer to a trailing list's
173+ // implicit <list>_len (QueryTextExtents.string_len).
174+ declared := false
175+ for _, n := range r.Node.Children {
176+ if n.Kind == "field" && n.Attr["name"] == s {
177+ declared = true
178+ }
179+ }
180+ for _, list := range r.Lists {
181+ if !declared && s == list.Node.Attr["name"]+"_len" {
182+ return "len(" + list.Param + ")"
183+ }
184+ }
185+ if list := r.Derived[s]; list != "" {
186+ return "len(" + list + ")"
187+ }
188+ return "int(" + paramName(s) + ")"
189+ }
190+ // Derived lengths are cast only after checking their wire width.
191+ for _, f := range append(append([]Field{}, r.Fields...), r.Params...) {
192+ if f.Kind != "field" || r.Derived[f.Node.Attr["name"]] == "" {
193+ continue
194+ }
195+ if f.Type.Size < 8 {
196+ fmt.Fprintf(&b, "if uint64(%s)>%d { b.Fail(xturbo.ErrBadLength); return 0 };", ref(f.Node.Attr["name"]), uint64(1)<<(8*f.Type.Size)-1)
197+ }
198+ }
199+ if r.DataByte != nil {
200+ f := r.DataByte
201+ if list := r.Derived[f.Node.Attr["name"]]; list != "" {
202+ fmt.Fprintf(&b, "if len(%s)>255 { b.Fail(xturbo.ErrBadLength); return 0 };", list)
203+ }
204+ }
205+ for _, f := range r.Lists {
206+ if f.Expr != nil && !(f.Expr.Kind == "fieldref" && r.Derived[f.Expr.Text] != "") {
207+ ex, err := renderExpr(f.Expr, ref, m)
208+ if err != nil {
209+ return "", err
210+ }
211+ fmt.Fprintf(&b, "if len(%s)!=%s { b.Fail(xturbo.ErrBadLength); return 0 };", f.Param, ex)
212+ }
213+ }
214+ body := fmt.Sprint((r.Fixed + 3) &^ 3)
215+ if len(r.Lists) > 0 {
216+ b.WriteString("size:=0;")
217+ for _, f := range r.Fields {
218+ if f.Kind == "list" {
219+ if f.Type.Variable {
220+ fmt.Fprintf(&b, "for i:=range %s { size+=%s[i].Size() };", f.Param, f.Param)
221+ } else {
222+ fmt.Fprintf(&b, "size+=len(%s)*%d;", f.Param, f.Type.Size)
223+ }
224+ } else if f.Align > 0 {
225+ fmt.Fprintf(&b, "size=(size+%d)&^%d;", f.Align-1, f.Align-1)
226+ } else {
227+ fmt.Fprintf(&b, "size+=%d;", f.Size)
228+ }
229+ }
230+ b.WriteString("size=(size+3)&^3;")
231+ body = "size"
232+ }
233+ if r.Values != nil {
234+ body += "+4*bits.OnesCount32(uint32(v.Mask))"
235+ }
236+ major := fmt.Sprint(r.Opcode)
237+ data := "0"
238+ if m.ExtName != "" {
239+ fmt.Fprintf(&b, "if b.Ext==nil { b.Fail(xturbo.ErrExtMissing); return 0 }; major:=b.Ext.Major(xturbo.Ext%s); if major==0 { b.Fail(xturbo.ErrExtMissing); return 0 };\n", m.ExtName)
240+ major = "major"
241+ data = fmt.Sprint(r.Opcode)
242+ } else if r.DataByte != nil {
243+ f := r.DataByte
244+ if f.Type.Base == "bool" {
245+ value := f.Param
246+ if f.Kind == "exprfield" {
247+ ex, err := renderExpr(f.Expr, ref, m)
248+ if err != nil {
249+ return "", err
250+ }
251+ value = "(" + ex + " != 0)"
252+ }
253+ fmt.Fprintf(&b, "var data uint8; if %s { data=1 };", value)
254+ data = "data"
255+ } else {
256+ data = "uint8(" + ref(f.Node.Attr["name"]) + ")"
257+ }
258+ }
259+ fmt.Fprintf(&b, "p,seq:=b.Request(%s,%s,%s); if p==nil { return 0 };\n", major, data, body)
260+ if len(r.Lists) > 0 {
261+ b.WriteString("off:=0;")
262+ }
263+ for _, f := range r.Fields {
264+ if f.Kind == "pad" {
265+ if len(r.Lists) > 0 {
266+ if f.Align > 0 {
267+ fmt.Fprintf(&b, "off=(off+%d)&^%d;", f.Align-1, f.Align-1)
268+ } else {
269+ fmt.Fprintf(&b, "off+=%d;", f.Size)
270+ }
271+ }
272+ continue
273+ }
274+ if f.Kind == "list" {
275+ if f.GoType == "*[32]byte" {
276+ fmt.Fprintf(&b, "copy(p[off:],%s[:]);", f.Param)
277+ } else if f.GoType == "string" || f.GoType == "[]byte" {
278+ fmt.Fprintf(&b, "copy(p[off:],%s); off+=len(%s);", f.Param, f.Param)
279+ } else {
280+ fmt.Fprintf(&b, "for i:=range %s {", f.Param)
281+ if isScalar(f.Type) {
282+ b.WriteString(putScalar(f.Type, f.Param+"[i]", "p[off:]"))
283+ } else {
284+ fmt.Fprintf(&b, "%s[i].Put(p[off:]);", f.Param)
285+ }
286+ if f.Type.Variable {
287+ fmt.Fprintf(&b, "off+=%s[i].Size();", f.Param)
288+ } else {
289+ fmt.Fprintf(&b, "off+=%d;", f.Type.Size)
290+ }
291+ b.WriteString("};")
292+ }
293+ continue
294+ }
295+ src := f.Param
296+ if list := r.Derived[f.Node.Attr["name"]]; list != "" {
297+ src = "len(" + list + ")"
298+ }
299+ if f.Kind == "exprfield" {
300+ var err error
301+ src, err = renderExpr(f.Expr, ref, m)
302+ if err != nil {
303+ return "", err
304+ }
305+ }
306+ dst := fmt.Sprintf("p[%d:]", f.Off)
307+ if len(r.Lists) > 0 {
308+ dst = "p[off:]"
309+ }
310+ if isScalar(f.Type) {
311+ b.WriteString(putScalar(f.Type, src, dst))
312+ } else {
313+ fmt.Fprintf(&b, "%s.Put(%s);", src, dst)
314+ }
315+ if len(r.Lists) > 0 {
316+ fmt.Fprintf(&b, "off+=%d;", f.Size)
317+ }
318+ }
319+ if r.Values != nil {
320+ fmt.Fprintf(&b, "off:=%d;", r.Fixed)
321+ for _, f := range r.Values.Fields {
322+ fmt.Fprintf(&b, "if v.Mask & %s != 0 {", f.Bit)
323+ b.WriteString(putScalar(f.Field.Type, "v."+f.Field.Name, "p[off:]"))
324+ b.WriteString("off+=4 };")
325+ }
326+ b.WriteString("_ = off;")
327+ }
328+ b.WriteString("return seq }\n")
329+ return b.String(), nil
330+}
331+func emitRequests(modules []*Module) error {
332+ for _, m := range modules {
333+ requests, err := resolveRequests(m)
334+ if err != nil {
335+ return err
336+ }
337+ data := struct {
338+ Pkg string
339+ Decls []string
340+ }{Pkg: m.Pkg}
341+ for _, r := range requests {
342+ decl, err := requestDecl(m, r)
343+ if err != nil {
344+ return err
345+ }
346+ data.Decls = append(data.Decls, decl)
347+ }
348+ all := strings.Join(data.Decls, "\n")
349+ imp := importsFor(m, all)
350+ if len(requests) > 0 {
351+ imp += "\nimport \"bnema.dev/xturbo\""
352+ }
353+ data.Decls = append([]string{imp}, data.Decls...)
354+ if err := writeTemplate(filepath.Join(m.Pkg, "requests_generated.go"), "requests", data); err != nil {
355+ return err
356+ }
357+ }
358+ return nil
359+}
A cmd/xtgen/resolve.go +356 -0
@@ -0,0 +1,356 @@
1+package main
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "strconv"
9+ "strings"
10+)
11+
12+var builtin = map[string]*Type{}
13+
14+func init() {
15+ for _, b := range []struct {
16+ xml, goType string
17+ size int
18+ }{
19+ {"CARD8", "uint8", 1}, {"CARD16", "uint16", 2}, {"CARD32", "uint32", 4}, {"CARD64", "uint64", 8},
20+ {"INT8", "int8", 1}, {"INT16", "int16", 2}, {"INT32", "int32", 4}, {"INT64", "int64", 8},
21+ {"BOOL", "bool", 1}, {"BOOL32", "bool", 4}, {"char", "byte", 1}, {"void", "byte", 1}, {"BYTE", "byte", 1},
22+ } {
23+ builtin[b.xml] = &Type{Name: b.xml, GoType: b.goType, Base: b.goType, Size: b.size, Kind: "scalar", Resolved: true}
24+ }
25+}
26+func loadModules() ([]*Module, error) {
27+ var manifest struct {
28+ Files []struct {
29+ Path string `json:"path"`
30+ } `json:"files"`
31+ }
32+ b, err := os.ReadFile("spec/manifest.json")
33+ if err != nil {
34+ return nil, err
35+ }
36+ if err = json.Unmarshal(b, &manifest); err != nil {
37+ return nil, err
38+ }
39+ var nodes []*Node
40+ for _, f := range manifest.Files {
41+ if filepath.Ext(f.Path) != ".xml" {
42+ continue
43+ }
44+ n, err := parseFile(f.Path)
45+ if err != nil {
46+ return nil, err
47+ }
48+ nodes = append(nodes, n)
49+ }
50+ return resolveModules(nodes)
51+}
52+func (m *Module) claim(name string, n *Node) error {
53+ if old := m.Names[name]; old != nil {
54+ return n.fail("Go name collision %s (previously %s:%d)", name, old.File, old.Line)
55+ }
56+ m.Names[name] = n
57+ return nil
58+}
59+func resolveModules(nodes []*Node) ([]*Module, error) {
60+ var modules []*Module
61+ byHeader := map[string]*Module{}
62+ for _, n := range nodes {
63+ m := &Module{Header: n.Attr["header"], Pkg: strings.ReplaceAll(n.Attr["header"], "_", ""), XName: n.Attr["extension-xname"], ExtName: n.Attr["extension-name"], Root: n, Symbols: map[string]*Type{}, Names: map[string]*Node{}}
64+ if byHeader[m.Header] != nil {
65+ return nil, n.fail("duplicate module %s", m.Header)
66+ }
67+ byHeader[m.Header] = m
68+ modules = append(modules, m)
69+ for _, c := range n.Children {
70+ switch c.Kind {
71+ case "xidtype", "xidunion", "typedef", "struct", "union":
72+ raw := c.Attr["name"]
73+ if c.Kind == "typedef" {
74+ raw = c.Attr["newname"]
75+ }
76+ name := goName(raw)
77+ if err := m.claim(name, c); err != nil {
78+ return nil, err
79+ }
80+ typ := &Type{Name: raw, GoType: name, Kind: c.Kind, Node: c, Module: m}
81+ m.Types = append(m.Types, typ)
82+ m.Symbols[raw] = typ
83+ }
84+ }
85+ }
86+ for _, m := range modules {
87+ for _, n := range m.Root.Children {
88+ if n.Kind == "import" {
89+ dep := byHeader[n.Text]
90+ if dep == nil {
91+ return nil, n.fail("unknown import %s", n.Text)
92+ }
93+ m.Imports = append(m.Imports, dep)
94+ }
95+ }
96+ }
97+ for _, m := range modules {
98+ for _, typ := range m.Types {
99+ if err := resolveType(typ); err != nil {
100+ return nil, err
101+ }
102+ }
103+ }
104+ for _, m := range modules {
105+ if err := resolveEnums(m); err != nil {
106+ return nil, err
107+ }
108+ }
109+ // Re-resolve enum annotations after widths are known, without changing wire sizes.
110+ for _, m := range modules {
111+ for _, typ := range m.Types {
112+ for i := range typ.Fields {
113+ f := &typ.Fields[i]
114+ if f.Kind != "field" {
115+ continue
116+ }
117+ if enum := f.Node.Attr["enum"]; enum != "" {
118+ if e, owner := findEnum(m, enum); e != nil {
119+ f.GoType = qualified(m, owner, e.Name)
120+ }
121+ }
122+ if mask := f.Node.Attr["mask"]; mask != "" {
123+ if e, owner := findEnum(m, mask); e != nil {
124+ f.GoType = qualified(m, owner, e.Name)
125+ }
126+ }
127+ }
128+ }
129+ }
130+ return modules, nil
131+}
132+func lookup(m *Module, name string) (*Type, error) {
133+ if t := builtin[name]; t != nil {
134+ return t, nil
135+ }
136+ if strings.Contains(name, ":") {
137+ parts := strings.SplitN(name, ":", 2)
138+ for _, dep := range m.Imports {
139+ if dep.Header == parts[0] {
140+ return lookup(dep, parts[1])
141+ }
142+ }
143+ return nil, m.Root.fail("unknown qualified type %s", name)
144+ }
145+ if t := m.Symbols[name]; t != nil {
146+ return t, nil
147+ }
148+ var found *Type
149+ for _, dep := range m.Imports {
150+ if t := dep.Symbols[name]; t != nil {
151+ if found != nil && found != t {
152+ return nil, m.Root.fail("ambiguous type %s", name)
153+ }
154+ found = t
155+ }
156+ }
157+ if found != nil {
158+ return found, nil
159+ }
160+ return nil, m.Root.fail("unknown type %s", name)
161+}
162+func qualified(m, owner *Module, name string) string {
163+ if owner == nil || owner == m {
164+ return name
165+ }
166+ return owner.Pkg + "." + name
167+}
168+func resolveType(t *Type) error {
169+ if t.Resolved {
170+ return nil
171+ }
172+ if t.Resolving {
173+ return t.Node.fail("recursive type %s", t.Name)
174+ }
175+ t.Resolving = true
176+ switch t.Kind {
177+ case "xidtype", "xidunion":
178+ t.Size = 4
179+ t.Base = "uint32"
180+ case "typedef":
181+ base, err := lookup(t.Module, t.Node.Attr["oldname"])
182+ if err != nil {
183+ return err
184+ }
185+ if err := resolveType(base); err != nil {
186+ return err
187+ }
188+ t.Size = base.Size
189+ t.Base = base.Base
190+ t.Variable = base.Variable
191+ default:
192+ off := 0
193+ for _, n := range t.Node.Children {
194+ if n.Kind == "doc" {
195+ continue
196+ }
197+ f := Field{Name: goName(n.Attr["name"]), Param: paramName(n.Attr["name"]), Kind: n.Kind, Node: n, Off: off}
198+ if t.Kind == "union" {
199+ f.Off = 0
200+ }
201+ switch n.Kind {
202+ case "pad":
203+ if n.Attr["align"] != "" {
204+ f.Align, _ = strconv.Atoi(n.Attr["align"])
205+ f.Size = (-off) & (f.Align - 1)
206+ } else {
207+ f.Size, _ = strconv.Atoi(n.Attr["bytes"])
208+ }
209+ case "field", "list":
210+ elem, err := lookup(t.Module, n.Attr["type"])
211+ if err != nil {
212+ return n.fail("%v", err)
213+ }
214+ if err := resolveType(elem); err != nil {
215+ return err
216+ }
217+ f.Type = elem
218+ f.GoType = qualified(t.Module, elem.Module, elem.GoType)
219+ f.Size = elem.Size
220+ if n.Kind == "list" {
221+ if len(n.Children) > 0 {
222+ f.Expr = n.Children[0]
223+ }
224+ if f.Expr != nil && f.Expr.Kind == "value" && !elem.Variable {
225+ count, err := strconv.Atoi(f.Expr.Text)
226+ if err != nil || count < 0 {
227+ return n.fail("invalid constant list size")
228+ }
229+ f.Count = count
230+ f.Size *= count
231+ f.GoType = fmt.Sprintf("[%d]%s", count, f.GoType)
232+ } else {
233+ t.Variable = true
234+ f.Size = 0
235+ f.GoType = "[]" + f.GoType
236+ if n.Attr["type"] == "char" {
237+ f.GoType = "string"
238+ }
239+ }
240+ }
241+ if elem.Variable {
242+ t.Variable = true
243+ }
244+ default:
245+ return n.fail("unsupported type member %s", n.Kind)
246+ }
247+ t.Fields = append(t.Fields, f)
248+ if t.Kind == "union" {
249+ if f.Size > off {
250+ off = f.Size
251+ }
252+ } else if !t.Variable {
253+ off += f.Size
254+ }
255+ }
256+ t.Size = off
257+ }
258+ t.Resolving = false
259+ t.Resolved = true
260+ return nil
261+}
262+func findEnum(m *Module, name string) (*Enum, *Module) {
263+ if strings.Contains(name, ":") {
264+ p := strings.SplitN(name, ":", 2)
265+ for _, dep := range m.Imports {
266+ if dep.Header == p[0] {
267+ return findEnum(dep, p[1])
268+ }
269+ }
270+ return nil, nil
271+ }
272+ for i := range m.Enums {
273+ if m.Enums[i].Node.Attr["name"] == name {
274+ return &m.Enums[i], m
275+ }
276+ }
277+ for _, dep := range m.Imports {
278+ for i := range dep.Enums {
279+ if dep.Enums[i].Node.Attr["name"] == name {
280+ return &dep.Enums[i], dep
281+ }
282+ }
283+ }
284+ return nil, nil
285+}
286+func resolveEnums(m *Module) error {
287+ for _, n := range m.Root.Children {
288+ if n.Kind != "enum" {
289+ continue
290+ }
291+ e := Enum{Name: goName(n.Attr["name"]), Base: "uint32", Node: n}
292+ if old := m.Names[e.Name]; old != nil {
293+ if !contains("xidtype xidunion typedef", old.Kind) {
294+ return n.fail("Go name collision %s", e.Name)
295+ }
296+ e.Existing = true
297+ } else if err := m.claim(e.Name, n); err != nil {
298+ return err
299+ }
300+ var max int64
301+ for _, item := range n.Children {
302+ if item.Kind == "doc" {
303+ continue
304+ }
305+ ex := item.Children[0]
306+ v, err := strconv.ParseInt(ex.Text, 0, 64)
307+ if err != nil {
308+ return ex.fail("invalid enum value")
309+ }
310+ if ex.Kind == "bit" {
311+ if v < 0 || v > 31 {
312+ return ex.fail("enum bit out of range")
313+ }
314+ v = 1 << uint(v)
315+ }
316+ if v < 0 || v > 0xffffffff {
317+ return ex.fail("enum value out of range")
318+ }
319+ if v > max {
320+ max = v
321+ }
322+ name := e.Name + goName(item.Attr["name"])
323+ if err := m.claim(name, item); err != nil {
324+ return err
325+ }
326+ e.Items = append(e.Items, EnumItem{name, v})
327+ }
328+ // Narrow only when every wire use has the same narrower width.
329+ width := 0
330+ mixed := false
331+ var scan func(*Node)
332+ scan = func(x *Node) {
333+ if x.Attr["enum"] == n.Attr["name"] || x.Attr["mask"] == n.Attr["name"] || x.Attr["altenum"] == n.Attr["name"] {
334+ typ, err := lookup(m, x.Attr["type"])
335+ if err == nil {
336+ if width != 0 && width != typ.Size {
337+ mixed = true
338+ }
339+ width = typ.Size
340+ }
341+ }
342+ for _, c := range x.Children {
343+ scan(c)
344+ }
345+ }
346+ scan(m.Root)
347+ if !mixed && width == 1 && max <= 255 {
348+ e.Base = "uint8"
349+ }
350+ if !mixed && width == 2 && max <= 65535 {
351+ e.Base = "uint16"
352+ }
353+ m.Enums = append(m.Enums, e)
354+ }
355+ return nil
356+}
@@ -0,0 +1,356 @@
1+package main
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "strconv"
9+ "strings"
10+)
11+
12+var builtin = map[string]*Type{}
13+
14+func init() {
15+ for _, b := range []struct {
16+ xml, goType string
17+ size int
18+ }{
19+ {"CARD8", "uint8", 1}, {"CARD16", "uint16", 2}, {"CARD32", "uint32", 4}, {"CARD64", "uint64", 8},
20+ {"INT8", "int8", 1}, {"INT16", "int16", 2}, {"INT32", "int32", 4}, {"INT64", "int64", 8},
21+ {"BOOL", "bool", 1}, {"BOOL32", "bool", 4}, {"char", "byte", 1}, {"void", "byte", 1}, {"BYTE", "byte", 1},
22+ } {
23+ builtin[b.xml] = &Type{Name: b.xml, GoType: b.goType, Base: b.goType, Size: b.size, Kind: "scalar", Resolved: true}
24+ }
25+}
26+func loadModules() ([]*Module, error) {
27+ var manifest struct {
28+ Files []struct {
29+ Path string `json:"path"`
30+ } `json:"files"`
31+ }
32+ b, err := os.ReadFile("spec/manifest.json")
33+ if err != nil {
34+ return nil, err
35+ }
36+ if err = json.Unmarshal(b, &manifest); err != nil {
37+ return nil, err
38+ }
39+ var nodes []*Node
40+ for _, f := range manifest.Files {
41+ if filepath.Ext(f.Path) != ".xml" {
42+ continue
43+ }
44+ n, err := parseFile(f.Path)
45+ if err != nil {
46+ return nil, err
47+ }
48+ nodes = append(nodes, n)
49+ }
50+ return resolveModules(nodes)
51+}
52+func (m *Module) claim(name string, n *Node) error {
53+ if old := m.Names[name]; old != nil {
54+ return n.fail("Go name collision %s (previously %s:%d)", name, old.File, old.Line)
55+ }
56+ m.Names[name] = n
57+ return nil
58+}
59+func resolveModules(nodes []*Node) ([]*Module, error) {
60+ var modules []*Module
61+ byHeader := map[string]*Module{}
62+ for _, n := range nodes {
63+ m := &Module{Header: n.Attr["header"], Pkg: strings.ReplaceAll(n.Attr["header"], "_", ""), XName: n.Attr["extension-xname"], ExtName: n.Attr["extension-name"], Root: n, Symbols: map[string]*Type{}, Names: map[string]*Node{}}
64+ if byHeader[m.Header] != nil {
65+ return nil, n.fail("duplicate module %s", m.Header)
66+ }
67+ byHeader[m.Header] = m
68+ modules = append(modules, m)
69+ for _, c := range n.Children {
70+ switch c.Kind {
71+ case "xidtype", "xidunion", "typedef", "struct", "union":
72+ raw := c.Attr["name"]
73+ if c.Kind == "typedef" {
74+ raw = c.Attr["newname"]
75+ }
76+ name := goName(raw)
77+ if err := m.claim(name, c); err != nil {
78+ return nil, err
79+ }
80+ typ := &Type{Name: raw, GoType: name, Kind: c.Kind, Node: c, Module: m}
81+ m.Types = append(m.Types, typ)
82+ m.Symbols[raw] = typ
83+ }
84+ }
85+ }
86+ for _, m := range modules {
87+ for _, n := range m.Root.Children {
88+ if n.Kind == "import" {
89+ dep := byHeader[n.Text]
90+ if dep == nil {
91+ return nil, n.fail("unknown import %s", n.Text)
92+ }
93+ m.Imports = append(m.Imports, dep)
94+ }
95+ }
96+ }
97+ for _, m := range modules {
98+ for _, typ := range m.Types {
99+ if err := resolveType(typ); err != nil {
100+ return nil, err
101+ }
102+ }
103+ }
104+ for _, m := range modules {
105+ if err := resolveEnums(m); err != nil {
106+ return nil, err
107+ }
108+ }
109+ // Re-resolve enum annotations after widths are known, without changing wire sizes.
110+ for _, m := range modules {
111+ for _, typ := range m.Types {
112+ for i := range typ.Fields {
113+ f := &typ.Fields[i]
114+ if f.Kind != "field" {
115+ continue
116+ }
117+ if enum := f.Node.Attr["enum"]; enum != "" {
118+ if e, owner := findEnum(m, enum); e != nil {
119+ f.GoType = qualified(m, owner, e.Name)
120+ }
121+ }
122+ if mask := f.Node.Attr["mask"]; mask != "" {
123+ if e, owner := findEnum(m, mask); e != nil {
124+ f.GoType = qualified(m, owner, e.Name)
125+ }
126+ }
127+ }
128+ }
129+ }
130+ return modules, nil
131+}
132+func lookup(m *Module, name string) (*Type, error) {
133+ if t := builtin[name]; t != nil {
134+ return t, nil
135+ }
136+ if strings.Contains(name, ":") {
137+ parts := strings.SplitN(name, ":", 2)
138+ for _, dep := range m.Imports {
139+ if dep.Header == parts[0] {
140+ return lookup(dep, parts[1])
141+ }
142+ }
143+ return nil, m.Root.fail("unknown qualified type %s", name)
144+ }
145+ if t := m.Symbols[name]; t != nil {
146+ return t, nil
147+ }
148+ var found *Type
149+ for _, dep := range m.Imports {
150+ if t := dep.Symbols[name]; t != nil {
151+ if found != nil && found != t {
152+ return nil, m.Root.fail("ambiguous type %s", name)
153+ }
154+ found = t
155+ }
156+ }
157+ if found != nil {
158+ return found, nil
159+ }
160+ return nil, m.Root.fail("unknown type %s", name)
161+}
162+func qualified(m, owner *Module, name string) string {
163+ if owner == nil || owner == m {
164+ return name
165+ }
166+ return owner.Pkg + "." + name
167+}
168+func resolveType(t *Type) error {
169+ if t.Resolved {
170+ return nil
171+ }
172+ if t.Resolving {
173+ return t.Node.fail("recursive type %s", t.Name)
174+ }
175+ t.Resolving = true
176+ switch t.Kind {
177+ case "xidtype", "xidunion":
178+ t.Size = 4
179+ t.Base = "uint32"
180+ case "typedef":
181+ base, err := lookup(t.Module, t.Node.Attr["oldname"])
182+ if err != nil {
183+ return err
184+ }
185+ if err := resolveType(base); err != nil {
186+ return err
187+ }
188+ t.Size = base.Size
189+ t.Base = base.Base
190+ t.Variable = base.Variable
191+ default:
192+ off := 0
193+ for _, n := range t.Node.Children {
194+ if n.Kind == "doc" {
195+ continue
196+ }
197+ f := Field{Name: goName(n.Attr["name"]), Param: paramName(n.Attr["name"]), Kind: n.Kind, Node: n, Off: off}
198+ if t.Kind == "union" {
199+ f.Off = 0
200+ }
201+ switch n.Kind {
202+ case "pad":
203+ if n.Attr["align"] != "" {
204+ f.Align, _ = strconv.Atoi(n.Attr["align"])
205+ f.Size = (-off) & (f.Align - 1)
206+ } else {
207+ f.Size, _ = strconv.Atoi(n.Attr["bytes"])
208+ }
209+ case "field", "list":
210+ elem, err := lookup(t.Module, n.Attr["type"])
211+ if err != nil {
212+ return n.fail("%v", err)
213+ }
214+ if err := resolveType(elem); err != nil {
215+ return err
216+ }
217+ f.Type = elem
218+ f.GoType = qualified(t.Module, elem.Module, elem.GoType)
219+ f.Size = elem.Size
220+ if n.Kind == "list" {
221+ if len(n.Children) > 0 {
222+ f.Expr = n.Children[0]
223+ }
224+ if f.Expr != nil && f.Expr.Kind == "value" && !elem.Variable {
225+ count, err := strconv.Atoi(f.Expr.Text)
226+ if err != nil || count < 0 {
227+ return n.fail("invalid constant list size")
228+ }
229+ f.Count = count
230+ f.Size *= count
231+ f.GoType = fmt.Sprintf("[%d]%s", count, f.GoType)
232+ } else {
233+ t.Variable = true
234+ f.Size = 0
235+ f.GoType = "[]" + f.GoType
236+ if n.Attr["type"] == "char" {
237+ f.GoType = "string"
238+ }
239+ }
240+ }
241+ if elem.Variable {
242+ t.Variable = true
243+ }
244+ default:
245+ return n.fail("unsupported type member %s", n.Kind)
246+ }
247+ t.Fields = append(t.Fields, f)
248+ if t.Kind == "union" {
249+ if f.Size > off {
250+ off = f.Size
251+ }
252+ } else if !t.Variable {
253+ off += f.Size
254+ }
255+ }
256+ t.Size = off
257+ }
258+ t.Resolving = false
259+ t.Resolved = true
260+ return nil
261+}
262+func findEnum(m *Module, name string) (*Enum, *Module) {
263+ if strings.Contains(name, ":") {
264+ p := strings.SplitN(name, ":", 2)
265+ for _, dep := range m.Imports {
266+ if dep.Header == p[0] {
267+ return findEnum(dep, p[1])
268+ }
269+ }
270+ return nil, nil
271+ }
272+ for i := range m.Enums {
273+ if m.Enums[i].Node.Attr["name"] == name {
274+ return &m.Enums[i], m
275+ }
276+ }
277+ for _, dep := range m.Imports {
278+ for i := range dep.Enums {
279+ if dep.Enums[i].Node.Attr["name"] == name {
280+ return &dep.Enums[i], dep
281+ }
282+ }
283+ }
284+ return nil, nil
285+}
286+func resolveEnums(m *Module) error {
287+ for _, n := range m.Root.Children {
288+ if n.Kind != "enum" {
289+ continue
290+ }
291+ e := Enum{Name: goName(n.Attr["name"]), Base: "uint32", Node: n}
292+ if old := m.Names[e.Name]; old != nil {
293+ if !contains("xidtype xidunion typedef", old.Kind) {
294+ return n.fail("Go name collision %s", e.Name)
295+ }
296+ e.Existing = true
297+ } else if err := m.claim(e.Name, n); err != nil {
298+ return err
299+ }
300+ var max int64
301+ for _, item := range n.Children {
302+ if item.Kind == "doc" {
303+ continue
304+ }
305+ ex := item.Children[0]
306+ v, err := strconv.ParseInt(ex.Text, 0, 64)
307+ if err != nil {
308+ return ex.fail("invalid enum value")
309+ }
310+ if ex.Kind == "bit" {
311+ if v < 0 || v > 31 {
312+ return ex.fail("enum bit out of range")
313+ }
314+ v = 1 << uint(v)
315+ }
316+ if v < 0 || v > 0xffffffff {
317+ return ex.fail("enum value out of range")
318+ }
319+ if v > max {
320+ max = v
321+ }
322+ name := e.Name + goName(item.Attr["name"])
323+ if err := m.claim(name, item); err != nil {
324+ return err
325+ }
326+ e.Items = append(e.Items, EnumItem{name, v})
327+ }
328+ // Narrow only when every wire use has the same narrower width.
329+ width := 0
330+ mixed := false
331+ var scan func(*Node)
332+ scan = func(x *Node) {
333+ if x.Attr["enum"] == n.Attr["name"] || x.Attr["mask"] == n.Attr["name"] || x.Attr["altenum"] == n.Attr["name"] {
334+ typ, err := lookup(m, x.Attr["type"])
335+ if err == nil {
336+ if width != 0 && width != typ.Size {
337+ mixed = true
338+ }
339+ width = typ.Size
340+ }
341+ }
342+ for _, c := range x.Children {
343+ scan(c)
344+ }
345+ }
346+ scan(m.Root)
347+ if !mixed && width == 1 && max <= 255 {
348+ e.Base = "uint8"
349+ }
350+ if !mixed && width == 2 && max <= 65535 {
351+ e.Base = "uint16"
352+ }
353+ m.Enums = append(m.Enums, e)
354+ }
355+ return nil
356+}
A cmd/xtgen/resolve_test.go +165 -0
@@ -0,0 +1,165 @@
1+package main
2+
3+import (
4+ "os"
5+ "strings"
6+ "testing"
7+)
8+
9+func testModules(t *testing.T) []*Module {
10+ t.Helper()
11+ old, err := os.Getwd()
12+ if err != nil {
13+ t.Fatal(err)
14+ }
15+ if err = os.Chdir("../.."); err != nil {
16+ t.Fatal(err)
17+ }
18+ defer func() {
19+ if err := os.Chdir(old); err != nil {
20+ t.Fatal(err)
21+ }
22+ }()
23+ ms, err := loadModules()
24+ if err != nil {
25+ t.Fatal(err)
26+ }
27+ return ms
28+}
29+func TestResolveLayouts(t *testing.T) {
30+ ms := testModules(t)
31+ expected := map[string]int{"Setup": 40, "Screen": 40, "Rectangle": 8, "Point": 4, "Arc": 12, "Visualtype": 24, "Format": 8, "Depth": 8, "ClientMessageData": 20}
32+ for _, typ := range ms[0].Types {
33+ if want, ok := expected[typ.GoType]; ok {
34+ if typ.Size != want {
35+ t.Errorf("%s size=%d want %d", typ.GoType, typ.Size, want)
36+ }
37+ delete(expected, typ.GoType)
38+ }
39+ if typ.GoType == "Rectangle" {
40+ for i, want := range []int{0, 2, 4, 6} {
41+ if typ.Fields[i].Off != want {
42+ t.Errorf("rectangle field %d offset", i)
43+ }
44+ }
45+ }
46+ }
47+ if len(expected) != 0 {
48+ t.Fatal("missing types", expected)
49+ }
50+ for _, name := range []string{"Window", "Atom", "Cursor", "Pixmap", "Font"} {
51+ found := false
52+ for _, e := range ms[0].Enums {
53+ if e.Name == name {
54+ found = e.Existing
55+ }
56+ }
57+ if !found {
58+ t.Errorf("%s must reuse XID type", name)
59+ }
60+ }
61+ for _, m := range ms {
62+ if err := packetSizeChecks(m); err != nil {
63+ t.Fatal(err)
64+ }
65+ }
66+}
67+func TestResolveRejectsCollisions(t *testing.T) {
68+ for _, body := range []string{
69+ `<struct name="A"/><enum name="A"><item name="B"><value>0</value></item></enum>`,
70+ `<xidtype name="A"/><xidtype name="a"/>`,
71+ `<enum name="A"><item name="B"><value>0</value></item></enum><enum name="A"><item name="C"><value>0</value></item></enum>`,
72+ } {
73+ n, err := parseXML("collision.xml", strings.NewReader(`<xcb header="test">`+body+`</xcb>`))
74+ if err != nil {
75+ t.Fatal(err)
76+ }
77+ _, err = resolveModules([]*Node{n})
78+ if err == nil || !strings.Contains(err.Error(), "collision.xml:1:") || !strings.Contains(err.Error(), "collision") {
79+ t.Fatalf("got %v", err)
80+ }
81+ }
82+}
83+func TestNaming(t *testing.T) {
84+ for in, want := range map[string]string{"WINDOW": "Window", "data_len": "DataLen", "ClientIdSpec": "ClientIdSpec", "2d": "X2d", "BOOL32": "Bool32"} {
85+ if got := goName(in); got != want {
86+ t.Errorf("%s => %s want %s", in, got, want)
87+ }
88+ }
89+ if paramName("type") != "type_" {
90+ t.Fatal("keyword parameter")
91+ }
92+}
93+
94+func TestRenderExpr(t *testing.T) {
95+ n, err := parseXML("expr.xml", strings.NewReader(`<xcb header="test"><request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="*"><fieldref>n</fieldref><value>4</value></op></exprfield></request></xcb>`))
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ got, err := renderExpr(n.Children[0].Children[0].Children[0], func(s string) string { return "int(v." + goName(s) + ")" }, nil)
100+ if err != nil || got != "(int(v.N) * 4)" {
101+ t.Fatalf("%s %v", got, err)
102+ }
103+}
104+
105+func TestFixedRequests(t *testing.T) {
106+ ms := testModules(t)
107+ for _, m := range ms {
108+ requests, err := resolveRequests(m)
109+ if err != nil {
110+ t.Fatal(err)
111+ }
112+ for _, r := range requests {
113+ decl, err := requestDecl(m, r)
114+ if err != nil {
115+ t.Fatal(err)
116+ }
117+ if !strings.Contains(decl, "if p==nil { return 0 }") {
118+ t.Fatal("missing sticky-error guard")
119+ }
120+ if m.Header == "xproto" && r.Name == "MapWindow" {
121+ if r.Fixed != 4 || len(r.Params) != 1 || r.Fields[0].Off != 0 {
122+ t.Fatal("MapWindow layout")
123+ }
124+ }
125+ if m.Header == "xfixes" && r.Name == "QueryVersion" {
126+ if r.Fixed != 8 || r.DataByte != nil || r.Fields[1].Off != 4 {
127+ t.Fatal("extension layout")
128+ }
129+ }
130+ }
131+ }
132+}
133+
134+func TestListResolution(t *testing.T) {
135+ for _, m := range testModules(t) {
136+ rs, err := resolveRequests(m)
137+ if err != nil {
138+ t.Fatal(err)
139+ }
140+ for _, r := range rs {
141+ switch r.Name {
142+ case "InternAtom":
143+ if r.Derived["name_len"] != "name" || len(r.Params) != 2 {
144+ t.Fatal("InternAtom derived length")
145+ }
146+ case "ChangeProperty":
147+ if len(r.Derived) != 0 || len(r.Params) != 7 {
148+ t.Fatal("ChangeProperty explicit expression fields")
149+ }
150+ case "AddGlyphs":
151+ if r.Derived["glyphs_len"] != "" {
152+ t.Fatal("shared count must remain explicit")
153+ }
154+ case "QueryClientIds":
155+ if r.Derived["num_specs"] != "specs" || r.Params[0].GoType != "[]ClientIdSpec" {
156+ t.Fatal("fixed struct list")
157+ }
158+ case "SetFontPath":
159+ if r.Params[0].GoType != "[]Str" || !r.Lists[0].Type.Variable {
160+ t.Fatal("variable struct list")
161+ }
162+ }
163+ }
164+ }
165+}
@@ -0,0 +1,165 @@
1+package main
2+
3+import (
4+ "os"
5+ "strings"
6+ "testing"
7+)
8+
9+func testModules(t *testing.T) []*Module {
10+ t.Helper()
11+ old, err := os.Getwd()
12+ if err != nil {
13+ t.Fatal(err)
14+ }
15+ if err = os.Chdir("../.."); err != nil {
16+ t.Fatal(err)
17+ }
18+ defer func() {
19+ if err := os.Chdir(old); err != nil {
20+ t.Fatal(err)
21+ }
22+ }()
23+ ms, err := loadModules()
24+ if err != nil {
25+ t.Fatal(err)
26+ }
27+ return ms
28+}
29+func TestResolveLayouts(t *testing.T) {
30+ ms := testModules(t)
31+ expected := map[string]int{"Setup": 40, "Screen": 40, "Rectangle": 8, "Point": 4, "Arc": 12, "Visualtype": 24, "Format": 8, "Depth": 8, "ClientMessageData": 20}
32+ for _, typ := range ms[0].Types {
33+ if want, ok := expected[typ.GoType]; ok {
34+ if typ.Size != want {
35+ t.Errorf("%s size=%d want %d", typ.GoType, typ.Size, want)
36+ }
37+ delete(expected, typ.GoType)
38+ }
39+ if typ.GoType == "Rectangle" {
40+ for i, want := range []int{0, 2, 4, 6} {
41+ if typ.Fields[i].Off != want {
42+ t.Errorf("rectangle field %d offset", i)
43+ }
44+ }
45+ }
46+ }
47+ if len(expected) != 0 {
48+ t.Fatal("missing types", expected)
49+ }
50+ for _, name := range []string{"Window", "Atom", "Cursor", "Pixmap", "Font"} {
51+ found := false
52+ for _, e := range ms[0].Enums {
53+ if e.Name == name {
54+ found = e.Existing
55+ }
56+ }
57+ if !found {
58+ t.Errorf("%s must reuse XID type", name)
59+ }
60+ }
61+ for _, m := range ms {
62+ if err := packetSizeChecks(m); err != nil {
63+ t.Fatal(err)
64+ }
65+ }
66+}
67+func TestResolveRejectsCollisions(t *testing.T) {
68+ for _, body := range []string{
69+ `<struct name="A"/><enum name="A"><item name="B"><value>0</value></item></enum>`,
70+ `<xidtype name="A"/><xidtype name="a"/>`,
71+ `<enum name="A"><item name="B"><value>0</value></item></enum><enum name="A"><item name="C"><value>0</value></item></enum>`,
72+ } {
73+ n, err := parseXML("collision.xml", strings.NewReader(`<xcb header="test">`+body+`</xcb>`))
74+ if err != nil {
75+ t.Fatal(err)
76+ }
77+ _, err = resolveModules([]*Node{n})
78+ if err == nil || !strings.Contains(err.Error(), "collision.xml:1:") || !strings.Contains(err.Error(), "collision") {
79+ t.Fatalf("got %v", err)
80+ }
81+ }
82+}
83+func TestNaming(t *testing.T) {
84+ for in, want := range map[string]string{"WINDOW": "Window", "data_len": "DataLen", "ClientIdSpec": "ClientIdSpec", "2d": "X2d", "BOOL32": "Bool32"} {
85+ if got := goName(in); got != want {
86+ t.Errorf("%s => %s want %s", in, got, want)
87+ }
88+ }
89+ if paramName("type") != "type_" {
90+ t.Fatal("keyword parameter")
91+ }
92+}
93+
94+func TestRenderExpr(t *testing.T) {
95+ n, err := parseXML("expr.xml", strings.NewReader(`<xcb header="test"><request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="*"><fieldref>n</fieldref><value>4</value></op></exprfield></request></xcb>`))
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ got, err := renderExpr(n.Children[0].Children[0].Children[0], func(s string) string { return "int(v." + goName(s) + ")" }, nil)
100+ if err != nil || got != "(int(v.N) * 4)" {
101+ t.Fatalf("%s %v", got, err)
102+ }
103+}
104+
105+func TestFixedRequests(t *testing.T) {
106+ ms := testModules(t)
107+ for _, m := range ms {
108+ requests, err := resolveRequests(m)
109+ if err != nil {
110+ t.Fatal(err)
111+ }
112+ for _, r := range requests {
113+ decl, err := requestDecl(m, r)
114+ if err != nil {
115+ t.Fatal(err)
116+ }
117+ if !strings.Contains(decl, "if p==nil { return 0 }") {
118+ t.Fatal("missing sticky-error guard")
119+ }
120+ if m.Header == "xproto" && r.Name == "MapWindow" {
121+ if r.Fixed != 4 || len(r.Params) != 1 || r.Fields[0].Off != 0 {
122+ t.Fatal("MapWindow layout")
123+ }
124+ }
125+ if m.Header == "xfixes" && r.Name == "QueryVersion" {
126+ if r.Fixed != 8 || r.DataByte != nil || r.Fields[1].Off != 4 {
127+ t.Fatal("extension layout")
128+ }
129+ }
130+ }
131+ }
132+}
133+
134+func TestListResolution(t *testing.T) {
135+ for _, m := range testModules(t) {
136+ rs, err := resolveRequests(m)
137+ if err != nil {
138+ t.Fatal(err)
139+ }
140+ for _, r := range rs {
141+ switch r.Name {
142+ case "InternAtom":
143+ if r.Derived["name_len"] != "name" || len(r.Params) != 2 {
144+ t.Fatal("InternAtom derived length")
145+ }
146+ case "ChangeProperty":
147+ if len(r.Derived) != 0 || len(r.Params) != 7 {
148+ t.Fatal("ChangeProperty explicit expression fields")
149+ }
150+ case "AddGlyphs":
151+ if r.Derived["glyphs_len"] != "" {
152+ t.Fatal("shared count must remain explicit")
153+ }
154+ case "QueryClientIds":
155+ if r.Derived["num_specs"] != "specs" || r.Params[0].GoType != "[]ClientIdSpec" {
156+ t.Fatal("fixed struct list")
157+ }
158+ case "SetFontPath":
159+ if r.Params[0].GoType != "[]Str" || !r.Lists[0].Type.Variable {
160+ t.Fatal("variable struct list")
161+ }
162+ }
163+ }
164+ }
165+}
A cmd/xtgen/templates/ext.tmpl +10 -0
@@ -0,0 +1,10 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package xturbo
3+
4+type ExtID uint8
5+const (
6+ ExtCore ExtID = iota
7+{{range .}} Ext{{.ExtName}}
8+{{end}} extCount
9+)
10+var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}}
@@ -0,0 +1,10 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package xturbo
3+
4+type ExtID uint8
5+const (
6+ ExtCore ExtID = iota
7+{{range .}} Ext{{.ExtName}}
8+{{end}} extCount
9+)
10+var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}}
A cmd/xtgen/templates/replies.tmpl +5 -0
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
A cmd/xtgen/templates/requests.tmpl +5 -0
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
A cmd/xtgen/templates/sizes.tmpl +7 -0
@@ -0,0 +1,7 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+import "testing"
4+func TestWireSizes(t *testing.T) {
5+{{range .SizeChecks}}{{.}}
6+{{end}}
7+}
@@ -0,0 +1,7 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+import "testing"
4+func TestWireSizes(t *testing.T) {
5+{{range .SizeChecks}}{{.}}
6+{{end}}
7+}
A cmd/xtgen/templates/types.tmpl +5 -0
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
A cmd/xtgen/values.go +136 -0
A cmd/xtgen/values_test.go +93 -0
A composite/replies_fuzz_test.go +39 -0
A composite/replies_generated.go +42 -0
A composite/requests_generated.go +176 -0
A composite/sizes_test.go +14 -0
A composite/types_generated.go +7 -0
A ext.go +27 -0
A ext_generated.go +19 -0
A ge/replies_fuzz_test.go +22 -0
A ge/replies_generated.go +24 -0
A ge/requests_generated.go +24 -0
A ge/sizes_test.go +11 -0
A ge/types_generated.go +2 -0
A generate.go +3 -0
A internal/xtest/README.md +40 -0
A internal/xtest/capture_test.go +405 -0
A internal/xtest/golden.go +22 -0
A lists_test.go +87 -0
A render/replies_fuzz_test.go +135 -0
A render/replies_generated.go +380 -0
A render/requests_generated.go +1276 -0
A render/sizes_test.go +68 -0
A render/types_generated.go +522 -0
A replies_test.go +304 -0
A requests_test.go +62 -0
A res/replies_fuzz_test.go +146 -0
A res/replies_generated.go +348 -0
A res/requests_generated.go +144 -0
A res/sizes_test.go +47 -0
A res/types_generated.go +159 -0
A shape/replies_fuzz_test.go +86 -0
A shape/replies_generated.go +123 -0
A shape/requests_generated.go +214 -0
A shape/sizes_test.go +23 -0
A shape/types_generated.go +20 -0
A testdata/golden/render/CreatePicture.req.hex +3 -0
A testdata/golden/render/QueryPictFormats.reply.hex +3 -0
A testdata/golden/res/QueryClientIds.reply.hex +3 -0
A testdata/golden/res/QueryClientIds.req.hex +3 -0
A testdata/golden/xfixes/QueryVersion.reply.hex +3 -0
A testdata/golden/xfixes/QueryVersion.req.hex +3 -0
A testdata/golden/xproto/ChangeProperty.req.hex +3 -0
A testdata/golden/xproto/ChangeWindowAttributes.req.hex +3 -0
A testdata/golden/xproto/ConfigureWindow.req.hex +3 -0
A testdata/golden/xproto/ConfigureWindowAll.req.hex +3 -0
A testdata/golden/xproto/ConfigureWindowSparse.req.hex +3 -0
A testdata/golden/xproto/CreateWindow.req.hex +3 -0
A testdata/golden/xproto/GetGeometry.reply.hex +3 -0
A testdata/golden/xproto/GetGeometry.req.hex +3 -0
A testdata/golden/xproto/GetProperty.reply.hex +3 -0
A testdata/golden/xproto/GetPropertyEmpty.reply.hex +3 -0
A testdata/golden/xproto/GetWindowAttributes.reply.hex +3 -0
A testdata/golden/xproto/InternAtom.reply.hex +3 -0
A testdata/golden/xproto/InternAtom.req.hex +3 -0
A testdata/golden/xproto/ListExtensions.reply.hex +3 -0
A testdata/golden/xproto/MapWindow.req.hex +3 -0
A testdata/golden/xproto/QueryExtension.reply.hex +3 -0
A testdata/golden/xproto/QueryTree.reply.hex +3 -0
A values_test.go +103 -0
A xcmisc/replies_fuzz_test.go +60 -0
A xcmisc/replies_generated.go +80 -0
A xcmisc/requests_generated.go +59 -0
A xcmisc/sizes_test.go +17 -0
A xcmisc/types_generated.go +2 -0
A xfixes/replies_fuzz_test.go +136 -0
A xfixes/replies_generated.go +242 -0
A xfixes/requests_generated.go +766 -0
A xfixes/sizes_test.go +32 -0
A xfixes/types_generated.go +55 -0
A xproto/replies_fuzz_test.go +895 -0
A xproto/replies_generated.go +1964 -0
A xproto/requests_generated.go +2780 -0
A xproto/requests_test.go +83 -0
A xproto/sizes_test.go +293 -0
A xproto/types_generated.go +1353 -0
A xproto/types_test.go +54 -0

Comments4 open / 4

bnema outdated

T2.1 implemented: strict source-located XML AST for all 9 pinned modules, rejection tests, parser fuzz seed. Local make check and CGO_ENABLED=0 go build ./... pass; 3-second parser fuzz run passed (416270 executions). T1.1 is on main (ba4ad52), CI run 221 green, repo public. Stopped before T2.2/T2.3 for a public naming-contract decision: xproto WINDOW xidtype and Window enum both map to Go Window under the specified rules (likewise ATOM/Atom, CURSOR/Cursor, PIXMAP/Pixmap, FONT/Font; render PICTURE/Picture). Should these enums reuse the XID type for constants, or receive a distinct name (please specify the naming rule)? No workaround chosen. T2.4 must wait for T2.2/T2.3; AST preserves expressions, imports, attributes and XML field order. No goldens captured yet.

bnema outdated

T2.2 (85a51dd) and T2.3 are implemented. Applied the decided enum/XID type reuse rule; other declaration collisions are fatal. All nine packages compile; manifest-ordered root ExtIDs, type/struct/union encoders and size checks are generated. Fixed requests include core data-byte handling, negotiated extension major opcodes, padding and sticky-error guards. Full make check passed independently after each task (unfiltered output), CGO-disabled build passed, request/Buf allocation tests passed, and benchmarks show 0 B/op, 0 allocs/op. Local xtrace is glibc execution tracing, so the permitted raw socketpair tee fallback captures independent reference encodings accepted by Xvfb. Three request goldens committed; capture update also passes with race enabled. Decisions: variable structs have owned encoding forms now (borrowed View/Iter APIs remain with list/reply tasks); generation wiring brought forward so gen-check verifies this work. xfixes signatures use render/shape types where the XML imports require them (matching actual wire types). T2.4: extend resolveRequests beyond its explicit list/switch gate, lower AST length expressions with renderExpr, add derived-length and mismatch handling, extend independent capture encoder for InternAtom/ChangeProperty/res.QueryClientIds. Buf already supports aligned reservation, zeroing, sticky errors and growth; transport/ack updates remain Phase 3. CI 222 and 223 green; checking latest run next. Keep this MR draft; do not merge.

bnema outdated

Final verification: CI 224 logs show make check and make xserver passed, as did gen-check. Additional local 10-second parser fuzz run passed (~3.57 million executions); clean CGO_ENABLED=0 go test -count=1 ./... and go build ./... passed. Both signed task commits verified with GPG. Working trees clean, origin is ermite only; MR remains WIP/unmerged.

bnema outdated

T2.4 completed in signed commit 582be88. Full make check and make xserver passed; CGO_ENABLED=0 build passed; independent raw socketpair/Xvfb tee captured InternAtom, ChangeProperty (GetProperty read-back), and XRes QueryClientIds (negotiated XRes 1.2). Race-enabled capture update passed. Allocation tests cover InternAtom, 4 KiB ChangeProperty, and fixed struct lists; all zero. Benchmarks: InternAtom 7.714 ns/op, ChangeProperty4KiB 73.58 ns/op, both 0 B/op and 0 allocs/op. Decisions: support implicit list-name_len refs used by QueryTextExtents only when no explicit field exists; reject derived length truncation with ErrBadLength; bring SendEvent fixed-array signature forward from the explicit public contract. T2.5 should remove the switch gate in resolveRequests and implement typed Values structs/setters; request list sizes/offsets now use running body offsets, with final four-byte rounding. Shared lengths stay explicit (e.g. render AddGlyphs); variable structs use Size/Put. T2.5–T2.8 and T3.1–T3.5 not started; stopping at this clean boundary due to long context. Same branch/worktree, MR draft, no tag.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close