-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathread.go
More file actions
321 lines (281 loc) · 9.61 KB
/
Copy pathread.go
File metadata and controls
321 lines (281 loc) · 9.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
// Copyright (c) 2026 Z5Labs and Contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package dfcad
import (
"fmt"
"strconv"
"time"
sexpr "github.com/z5labs/sexpr-go"
)
// reader reads typed values out of a spanned tree, collecting a diagnostic
// wherever the tree held something other than what was wanted.
//
// Every pass which interprets forms embeds one: the registry loader and the
// entity loader both. "expected a symbol, found a string" is the same answer
// wherever the symbol was wanted, and a second copy of it would be a second
// wording of one sentence which would then drift from the first.
//
// The zero value is ready to use. Diagnostics accumulate in the order the pass
// found them; collecting them into a [Diagnostics] is what puts them in
// reporting order.
type reader struct {
// diags are the problems found so far.
diags []Diagnostic
}
// add records diagnostics.
func (r *reader) add(diags ...Diagnostic) {
r.diags = append(r.diags, diags...)
}
// name reads the positional name of a declaration, with the span a diagnostic
// about that name points at.
func (r *reader) name(node *Node, what string) (string, Span, bool) {
arg, ok := argument(node, 0)
if !ok {
return "", Span{}, false
}
name, ok := r.symbol(arg, what)
return name, arg.Span, ok
}
// identifier reads the positional id of a declaration, with the span a
// diagnostic about that id points at.
func (r *reader) identifier(node *Node, what string) (ID, Span, bool) {
arg, ok := argument(node, 0)
if !ok {
return "", Span{}, false
}
id, ok := r.id(arg, what)
return id, arg.Span, ok
}
// id reads an id, reporting the rule it broke where what was written is not
// one.
//
// An id which is not one yields the zero ID rather than the text it was written
// with, for the reason every other axis of a form does: the diagnostic already
// carries what was written, and a value which is not one of the things it could
// have been would be judged by everything downstream as though somebody had
// written it.
func (r *reader) id(node *Node, what string) (ID, bool) {
written, ok := r.symbol(node, what)
if !ok {
return "", false
}
id, err := ParseID(written)
if malformed, ok := asMalformedID(err); ok {
r.add(malformedID(node.Span, malformed))
return "", false
}
return id, true
}
// registered checks that the namespace of an id is one registry declares.
//
// It is the whole of what this layer has to say about an id: whether the thing
// it names exists is a question for the layer which holds those things, and
// whether the namespace exists is a question only the registry answers
// ([0003](docs/decisions/0003-id-namespaces-are-a-closed-registry.md)).
//
// The zero ID is not checked. It belongs to something which was not written or
// which was already reported as not being an id, and a namespace diagnostic on
// top of that is one mistake reported twice.
func (r *reader) registered(registry *Registry, id ID, span Span) bool {
if id == "" {
return false
}
if !registry.Declares(SortNamespace, id.Namespace()) {
r.add(registry.Undeclared(SortNamespace, id.Namespace(), span))
return false
}
return true
}
// symbol reads a symbol, reporting what was written there instead.
func (r *reader) symbol(node *Node, what string) (string, bool) {
datum, ok := node.Datum.(sexpr.Symbol)
if !ok {
r.wrong(node, what)
return "", false
}
return datum.Value, true
}
// text reads a string.
func (r *reader) text(node *Node, what string) (string, bool) {
datum, ok := node.Datum.(sexpr.String)
if !ok {
r.wrong(node, what)
return "", false
}
return datum.Value, true
}
// boolean reads a boolean.
func (r *reader) boolean(node *Node, what string) (bool, bool) {
datum, ok := node.Datum.(sexpr.Bool)
if !ok {
r.wrong(node, what)
return false, false
}
return datum.Value, true
}
// integer reads a count, which specification section 4.3 writes with neither a
// fraction nor an exponent so that it reads back as an integer.
func (r *reader) integer(node *Node, what string) (int64, bool) {
datum, ok := node.Datum.(sexpr.Int)
if !ok {
r.wrong(node, what)
return 0, false
}
return datum.Value, true
}
// real reads a real number, which specification section 4.3 writes with a
// fraction or an exponent so that it reads back as a real.
//
// A whole number written as one — `5` where `5.0` was meant — is reported
// rather than widened, because the distinction is the only thing telling a
// magnitude apart from a count in a format where both are written as digits.
func (r *reader) real(node *Node, what string) (float64, bool) {
datum, ok := node.Datum.(sexpr.Float)
if !ok {
if _, isInt := node.Datum.(sexpr.Int); isInt {
r.add(Diagnostic{
Severity: SeverityError,
Span: node.Span,
Message: fmt.Sprintf("expected %s, found %s", what, describe(node)),
Hint: "a real number is written with a fraction or an exponent, so that it reads back as a real",
})
return 0, false
}
r.wrong(node, what)
return 0, false
}
return datum.Value, true
}
// dateLayout is the one spelling of a date, per specification section 4.4: RFC
// 3339 full-date, four-digit year, two-digit month, two-digit day, proleptic
// Gregorian, hyphen separated. No time, no zone and no other spelling.
const dateLayout = time.DateOnly
// dateSpelling is that layout as somebody writing one reads it, which is what a
// message about a date which is not one says.
const dateSpelling = "YYYY-MM-DD"
// MalformedDateError reports something written where a date belongs which is
// not one.
//
// There is one spelling of a date in this format and it is the engine's rather
// than the caller's, so the parse and the refusal are here: a command taking a
// date on its command line and a file carrying one are held to the same
// spelling, and neither has a second copy of it to drift from.
type MalformedDateError struct {
// Written is what was there instead.
Written string
}
// Error implements the [error] interface.
func (e MalformedDateError) Error() string {
return fmt.Sprintf(
"malformed date %s: a date is written as %s, with no time and no zone",
strconv.Quote(e.Written), dateSpelling,
)
}
// ParseDate reads a date written the one way this format writes one, per
// specification section 4.4.
//
// It is what every date in the engine goes through, whether it was read out of
// a file or handed in by a caller authoring a change.
func ParseDate(written string) (time.Time, error) {
date, err := time.Parse(dateLayout, written)
if err != nil {
return time.Time{}, MalformedDateError{Written: written}
}
return date, nil
}
// date reads a date, reporting the spelling it was meant to have where what was
// written is not one.
//
// It is read from a string rather than from a bare symbol because it has to be:
// `2026-03-14` begins like a number, so the delegated tokenizer classifies it as
// a malformed one and fails before this format is reached. Quoting it is the
// only spelling which survives that lexis.
//
// A failure yields the zero time rather than a partial reading, for the reason
// every other axis of a form does: the diagnostic already carries what was
// written, and a date nobody wrote would be compared by everything downstream as
// though somebody had.
func (r *reader) date(node *Node, what string) (time.Time, bool) {
written, ok := r.text(node, what)
if !ok {
return time.Time{}, false
}
date, err := ParseDate(written)
if err != nil {
r.add(Diagnostic{
Severity: SeverityError,
Span: node.Span,
Message: fmt.Sprintf("expected %s, found %s", what, strconv.Quote(written)),
Hint: `a date is written as a string in RFC 3339 full-date form, "` + dateSpelling + `", with no time and no zone`,
})
return time.Time{}, false
}
return date, true
}
// wrong reports a datum of the wrong sort written where a form wanted one of
// another.
func (r *reader) wrong(node *Node, what string) {
r.add(Diagnostic{
Severity: SeverityError,
Span: node.Span,
Message: fmt.Sprintf("expected %s, found %s", what, describe(node)),
})
}
// elements is everything written after a form's tag.
func elements(node *Node) []*Node {
if len(node.Children) == 0 {
return nil
}
return node.Children[1:]
}
// argument returns the i-th positional argument of a form, and whether it was
// written.
func argument(node *Node, i int) (*Node, bool) {
written, _ := split(elements(node))
if i < 0 || i >= len(written) {
return nil, false
}
return written[i], true
}
// childForm returns the first child of node written with tag, and whether one
// was written.
func childForm(node *Node, tag string) (*Node, bool) {
_, children := split(elements(node))
for _, child := range children {
if written, ok := formTag(child); ok && written == tag {
return child, true
}
}
return nil, false
}
// childForms returns every child of node written with tag, in the order they
// were written.
func childForms(node *Node, tag string) []*Node {
_, children := split(elements(node))
var out []*Node
for _, child := range children {
if written, ok := formTag(child); ok && written == tag {
out = append(out, child)
}
}
return out
}
// argumentOf returns the single positional argument of the child written with
// tag, which is how every one-value child of a registry or entity form is read.
func argumentOf(node *Node, tag string) (*Node, bool) {
child, ok := childForm(node, tag)
if !ok {
return nil, false
}
return argument(child, 0)
}
// spellings spells a closed set for a diagnostic which lists it.
func spellings[T ~string](set []T) []string {
out := make([]string, 0, len(set))
for _, member := range set {
out = append(out, string(member))
}
return out
}