-
Notifications
You must be signed in to change notification settings - Fork 5
/
parser.go
212 lines (197 loc) · 5.56 KB
/
parser.go
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
package gribble
import (
"fmt"
"strconv"
"strings"
"text/scanner"
)
// parser maintains the state of the Gribble parser. It embeds a Scanner value
// and keeps track of the current token and token text (since Peek/Next always
// look at the *next* token, which isn't what we want typically when reporting
// errors about the current token).
//
// errors is a slice of all errors encountered during parsing. Parsing never
// stops because of an error.
type parser struct {
*scanner.Scanner
original string
verbose bool
tok rune
tokText string
negative bool // whether the last token was a '-'
errors []error
}
// command represents a Gribble command invocation; which is simply a name and
// a list of arguments.
type command struct {
name string
params []Value
}
// String returns a recursively formatted command, Lisp style.
func (cmd *command) String() string {
if len(cmd.params) == 0 {
return fmt.Sprintf("(%s)", cmd.name)
}
params := make([]string, len(cmd.params))
for i, param := range cmd.params {
switch concrete := param.(type) {
case int:
params[i] = fmt.Sprintf("%d", concrete)
case float64:
params[i] = fmt.Sprintf("%0.2f", concrete)
case string:
params[i] = concrete
case *command:
params[i] = concrete.String()
default:
panic(fmt.Sprintf("Unexpected type: %T", concrete))
}
}
return fmt.Sprintf("(%s %s)", cmd.name, strings.Join(params, " "))
}
// parse takes a command invocation string and returns its a command value
// representing it. Note that even when an error is returned, the command
// value is also returned since parsing doesn't stop on an error.
func parse(invocation string, verbose bool) (*command, error) {
if len(strings.TrimSpace(invocation)) == 0 {
return &command{}, fmt.Errorf("Empty strings are not valid commands.")
}
p := newParser(invocation, verbose)
cmd := p.command(p.Scan())
if len(p.errors) == 0 && p.tok != scanner.EOF {
p.parseError("EOF")
}
if len(p.errors) > 0 {
reterrs := make([]string, len(p.errors))
for i, err := range p.errors {
reterrs[i] = err.Error()
}
return cmd, e(strings.Join(reterrs, "\n"))
}
return cmd, nil
}
// parseMany takes multiple command invocations in a single string and returns
// a command value for each command.
// Note that even when an error is returned, the command
// value is also returned since parsing doesn't stop on an error.
func parseMany(invocation string, verbose bool) ([]*command, error) {
if len(strings.TrimSpace(invocation)) == 0 {
return nil, fmt.Errorf("Empty strings are not valid commands.")
}
p := newParser(invocation, verbose)
cmds := make([]*command, 0)
p.Scan()
for len(p.errors) == 0 && p.tok != scanner.EOF {
cmds = append(cmds, p.command(p.tok))
}
if len(p.errors) > 0 {
reterrs := make([]string, len(p.errors))
for i, err := range p.errors {
reterrs[i] = err.Error()
}
return cmds, e(strings.Join(reterrs, "\n"))
}
return cmds, nil
}
// newParser is a parser constructor and also initializes the Scanner.
func newParser(invocation string, verbose bool) *parser {
p := &parser{
Scanner: &scanner.Scanner{},
original: invocation,
verbose: verbose,
errors: make([]error, 0),
}
p.Init(strings.NewReader(invocation))
p.Error = func(s *scanner.Scanner, msg string) {
p.errors = append(p.errors, p.error(s.Position, msg))
}
return p
}
// Scan intercepts all Scan requests to update tok and tokText state.
func (p *parser) Scan() rune {
r := p.Scanner.Scan()
p.tok, p.tokText = r, p.TokenText()
if r == '-' {
p.negative = !p.negative
return p.Scan()
}
return r
}
// demands checks the current token for equality with 'c'. If they are equal,
// the scanner progresses unhindered. Otherwise, an error is logged and the
// scanner still progresses.
func (p *parser) demands(c rune) {
if p.tok != c {
p.parseError(string([]rune{c}))
}
p.Scan()
}
// command parses a command invocation. It can handle arbitrarily nested
// parantheses. An error is logged when something other than a scanner.Ident
// or a '(' is found.
func (p *parser) command(tok rune) *command {
switch tok {
case '(':
cmd := p.command(p.Scan())
p.demands(')')
return cmd
case scanner.Ident:
cmd := &command{
name: p.TokenText(),
params: p.params(),
}
return cmd
}
p.parseError("command")
return &command{}
}
// params parses a list of parameters to a command invocation. A parameter
// can be either a string, an integer, a float or a command (surrounded by
// parantheses). If a ')' or an EOF is found, parameter parsing stops.
// If anything else is found, an error is reported.
func (p *parser) params() []Value {
params := make([]Value, 0, 4)
tok := p.Scan()
for tok != scanner.EOF {
switch tok {
case '(':
params = append(params, p.command(p.Scan()))
p.demands(')')
tok = p.tok
continue
case scanner.String:
params = append(params, strings.Trim(p.TokenText(), "\"`"))
case scanner.Int:
if n, err := strconv.ParseInt(p.TokenText(), 0, 32); err != nil {
p.parseError("integer")
params = append(params, nil)
} else {
if p.negative {
params = append(params, -int(n))
p.negative = false
} else {
params = append(params, int(n))
}
}
case scanner.Float:
if n, err := strconv.ParseFloat(p.TokenText(), 64); err != nil {
p.parseError("float")
params = append(params, nil)
} else {
if p.negative {
params = append(params, -float64(n))
p.negative = false
} else {
params = append(params, float64(n))
}
}
case ')':
return params
case '-':
default:
p.parseError("parameter")
}
tok = p.Scan()
}
return params
}