-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpeg.go
More file actions
342 lines (311 loc) · 11.1 KB
/
Copy pathpeg.go
File metadata and controls
342 lines (311 loc) · 11.1 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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
// Package peg implements the Parsing Expression Grammars inspired by LPeg.
//
// This package implements the Parsing Expression Grammars (PEGs),
// a powerful tool for pattern matching and writing top-down parsers.
// PEGs were designed to focus on expressing the match or parsing progress,
// rather than to describe what text should be matched as regexps do.
// The package was strongly influenced by LPeg for lua, see:
// http://www.inf.puc-rio.br/~roberto/lpeg/.
// Take a look at it for further readings.
//
// Overlook
//
// There were four methods for PEGs pattern matching:
//
// MatchedPrefix(pat, text) (prefix, ok)
// IsFullMatched(pat, text) ok
// Parse(pat, text) (captures, err)
// Match(pat, text) (result, err)
//
// The most general one is `config.Match(pat, text)`, which returns a `*Result`
// typed match result and an error if any error occured.
//
// The config tells the max recursion level, the max repeatition times and
// whether grouping or capturing is enabled. The default config enables
// both grouping and capturing, while limits for recursion and repeat are
// setup to DefaultCallstackLimit and DefaultRepeatLimit.
//
// The result of `config.Match(pat, text)` contains:
// whether pattern was matched, how many bytes were matched,
// the saved groups and the parser captures.
// Saved groups are text pieces captured with an optional name.
// Parser captures are parse trees or user defined structures constructed
// during the parsing process.
//
// Note that, both `MatchedPrefix` and `IsFullMatched` disables capturing.
// That is, the side effects of user defined constructors won't be triggered.
//
// Categories of patterns
//
// Basic patterns, which matches a single rune or a piece of text,
// are listed below:
//
// T(text), TI(insensitivetext), TS(text, ...), TSI(insensitivetext, ...)
// Dot, S(runes), NS(excluderunes), R(low, high, ...), NR(low, high, ...)
// U(unicoderangename)
//
// Patterns are combined by sequence or alternation:
//
// Seq(sequence...), Alt(choices...)
//
// Predicators test if pattern would be matched, but consume no text:
//
// True, False, SOL, EOL, EOF
// B(text), Test(cond), Not(cond), And(assertions...), Or(posiblities...), Abort(msg)
// When(cond, pat), If(cond, yes, no), Switch(cond, pat, ..., [otherwise])
//
// Available pattern qualifiers are:
//
// Skip(n), Until(pat), UntilB(pat)
// Q0(choices...), Q1(choices...), Qn(atleast, choices...)
// Q01(choices...), Q0n(atmost, choices...), Qnn(exact, choices...), Qmn(from, to, choices...)
//
// Pattern where item separated by sep could be expressed using:
//
// J0(item, sep), J1(item, sep), Jn(atleast, item, sep)
// J0n(atmost, item, sep), Jnn(exact, item, sep), Jmn(from, to, item, sep)
//
// Functionalities for groups, references, triggers and injectors:
//
// G(pat), NG(groupname, pat)
// Ref(groupname), RefB(groupname)
// Trigger(hook, pat), Inject(injector, pat)
// Check(checker, pat), Trunc(maxrune, pat)
//
// Functionalities for grammars and parsing captures:
//
// Let(scope, pat), V(varname), CV(varname), CK(tokentype, pat)
// CC(nontermcons, pat), CT(termcons, pat)
//
// Common mistakes
//
// Greedy qualifiers:
//
// The qualifiers are designed to be greedy. Thus, considering the pattern
// `Seq(Q0(A), B)`, text supposed to be matched by `B` could be swallowed
// ahead of time by the preceding `A`, which is usually unexpected.
// It is recommended to wrap `A` with an additional assertion to avoid this.
//
// For example, `Seq(Q0(R('0', '9')), S("02468"), T(" is even"))` is incorrect,
// because the greedy `Q0(R('0', '9'))` would consume the last digit, thus the
// following `S("02468")` would always dismatch. To make everything right,
// `Q0(R('0', '9'))` should be replaced by a pattern like
// `Q0(Seq(R('0', '9'), Test(R('0', '9'))))` (assert one digit follow it),
// which won't consume the last digit.
//
// Unreachable branches:
//
// Branch of `Seq` or `Alt` could be unreachable, considering that Seq searches
// the first dismatch in the sequence, while Alt searches the first match in the
// choices. Thus, a pattern like `Alt(T("match"), T("match more"))` would get an
// unexpected match result, becuase longer patterns are not in prior order.
//
// Infinite loops:
//
// Any pattern which could macth an empty string should not be nested inside
// qualifiers like `Q0`, `Q1`, `Qn`, for this would cause infinite loops.
//
// For example, `Q1(True)` or `Q0(Q0(T("not empty")))` would loop until
// `config.RepeatLimit` is reached.
//
// Left recursion:
//
// PEG parsers are top-down, that is, the grammar rules would be expanded
// immediately, thus a left recursion never terminates until
// `config.CallstackLimit` is reached.
//
// For example, `Let(map[string]Pattern{"var": Seq(T("A"), V("var"))}, V("var"))`
// terminates, while
// `Let(map[string]Pattern{"var": Seq(V("var"), T("A"))}, V("var"))` won't
// terminate until `CallstackLimit` is reached.
package peg // import "github.com/hucsmn/peg"
import (
"fmt"
"strings"
)
// Default limits of pattern matching.
const (
DefaultCallstackLimit = 500
DefaultRepeatLimit = 500
)
var (
defaultConfig = Config{
CallstackLimit: DefaultCallstackLimit,
RepeatLimit: DefaultRepeatLimit,
DisableLineColumnCounting: false,
DisableGrouping: false,
DisableCapturing: false,
}
)
type (
// Pattern is the tree representation for Parse Grammar Expression.
Pattern interface {
match(ctx *context) error
String() string
}
// Config contains configration for pattern matching.
Config struct {
// Maximum callstack size, zero or negative for unlimited.
CallstackLimit int
// Maximum qualifier repeatition times, zero or negative for unlimited.
RepeatLimit int
// Determines if the position calculation is disabled.
DisableLineColumnCounting bool
// Determines if grouping is disabled.
DisableGrouping bool
// Determines if parse tree capturing is disabled.
DisableCapturing bool
}
// Result stores the results from pattern matching.
Result struct {
// Is pattern matched and how many bytes matched.
Ok bool
N int
// Grouped text pieces with optional names.
Groups []string
NamedGroups map[string]string
// Parse captures.
Captures []Capture
}
// Capture stores structures from parse capturing.
// User defined structures (the types implemented Capture interface other
// than the predefined Variable type and Token type) are constructed by
// customed TerminalConstructor or NonTerminalConstructor.
Capture interface {
// IsTerminal tells if it is a terminal type.
IsTerminal() bool
}
// Variable is a predefined non-terminal type for PEG variable capturing.
Variable struct {
Name string
Subs []Capture
}
// Token is a predefined terminal type stores a piece of typed text
// and its position in the source text.
Token struct {
Type int
Value string
Position Position
}
// TerminalConstructor is customed terminal type constructor.
TerminalConstructor func(string, Position) (Capture, error)
// NonTerminalConstructor is customed non-terminal type constructor.
NonTerminalConstructor func([]Capture) (Capture, error)
)
// MatchedPrefix returns the matched prefix of text when successfully matched.
func MatchedPrefix(pat Pattern, text string) (prefix string, ok bool) {
return defaultConfig.MatchedPrefix(pat, text)
}
// IsFullMatched tells if given pattern matches the full text.
// It is recommended to use Seq(Alt(...), EOF) rather than use Alt(...) when
// testing IsFullMatched.
// For example, IsFullMatched(Alt(T("match"), T("match more")), "match more")
// returns false rather than true counter-intuitively.
func IsFullMatched(pat Pattern, text string) bool {
return defaultConfig.IsFullMatched(pat, text)
}
// Parse runs pattern matching on given text, guaranteeing that the text must
// only be full-matched when success.
func Parse(pat Pattern, text string) (caps []Capture, err error) {
return defaultConfig.Parse(pat, text)
}
// Match runs pattern matching on given text, using the default configuration.
// The default configuration uses DefaultCallstackLimit and DefaultLoopLimit,
// while line-column counting, grouping and parse capturing is enabled.
// Returns nil result if any error occurs.
func Match(pat Pattern, text string) (result *Result, err error) {
return defaultConfig.Match(pat, text)
}
// MatchedPrefix returns the matched prefix of text when successfully matched.
func (cfg Config) MatchedPrefix(pat Pattern, text string) (prefix string, ok bool) {
// disable capturing.
config := cfg
config.DisableLineColumnCounting = true
config.DisableCapturing = true
r, err := config.Match(pat, text)
if err != nil || !r.Ok {
return "", false
}
return text[:r.N], true
}
// IsFullMatched tells if given pattern matches the full text.
// It is recommended to use Seq(Alt(...), EOF) rather than use Alt(...) when
// testing IsFullMatched.
// For example, IsFullMatched(Alt(T("match"), T("match more")), "match more")
// returns false rather than true counter-intuitively.
func (cfg Config) IsFullMatched(pat Pattern, text string) bool {
// disable capturing.
config := cfg
config.DisableLineColumnCounting = true
config.DisableCapturing = true
r, err := config.Match(pat, text)
return err == nil && r.Ok && r.N == len(text)
}
// Parse runs pattern matching on given text, guaranteeing that the text must
// only be full-matched when success.
func (cfg Config) Parse(pat Pattern, text string) (caps []Capture, err error) {
// enable capturing.
config := cfg
config.DisableLineColumnCounting = false
config.DisableCapturing = false
r, err := config.Match(pat, text)
if err != nil {
return nil, err
}
if !r.Ok {
return nil, errorDismatch
}
if r.N != len(text) {
return nil, errorNotFullMatched
}
return r.Captures, nil
}
// Match runs pattern matching on given text, using the default configuration.
// The default configuration uses DefaultCallstackLimit and DefaultLoopLimit,
// while line-column counting, grouping and parse capturing is enabled.
// Returns nil result if any error occurs.
func (cfg Config) Match(pat Pattern, text string) (result *Result, err error) {
if pat == nil {
return nil, errorNilMainPattern
}
ctx := newContext(pat, text, cfg)
err = ctx.match()
if err != nil {
return nil, err
}
if ctx.ret.ok {
return &Result{
Ok: true,
N: ctx.ret.n,
Groups: ctx.groups,
NamedGroups: ctx.namedGroups,
Captures: ctx.capstack[0].args,
}, nil
}
return &Result{
Ok: false,
N: 0,
Groups: nil,
NamedGroups: nil,
Captures: nil,
}, nil
}
// IsTerminal method of the Variable type always returns false.
func (v *Variable) IsTerminal() bool {
return false
}
// IsTerminal method of the Token type always returns true.
func (tok *Token) IsTerminal() bool {
return true
}
func (v *Variable) String() string {
strs := make([]string, len(v.Subs))
for i := range v.Subs {
strs[i] = fmt.Sprint(v.Subs[i])
}
return fmt.Sprintf("%s(%s)", v.Name, strings.Join(strs, ", "))
}
func (tok *Token) String() string {
return fmt.Sprintf("token_%d%q@%s",
tok.Type, tok.Value, tok.Position.String())
}