main 602baa7

parse.go RawHistoryFolder

1package main
2
3import (
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.
15type Node struct {
16	Kind     string
17	Attr     map[string]string
18	Text     string
19	Children []*Node
20	File     string
21	Line     int
22}
23
24func (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.
30var 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}
40var 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
52func 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
61func 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}
69func 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}
143func 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}
151func 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}
158func 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}
167func 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}

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
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close