In the spirit of Go, slimmed down the ParseAPI interface. I'm no longer using ParseAPI.On(..).<DoSomething>(), but now it's simply ParseAPI.<DoSomething>(). I also dropped the difference between a Stay() and an Accept(). All that is possible now is ParseAPI.Peek() and ParseAPI.Accept().

This commit is contained in:
Maurice Makaay 2019-06-09 10:25:49 +00:00
parent 9f5caa2024
commit add28feb33
9 changed files with 120 additions and 180 deletions

View File

@ -75,7 +75,7 @@ var bareInteger = parsekit.C.Seq(dropBlank, parsekit.A.Integer, dropBlank)
var int64Token = parsekit.T.Int64(nil, bareInteger)
func (c *simpleCalculator) number(p *parsekit.ParseAPI) {
if p.On(int64Token).Accept() {
if p.Accept(int64Token) {
c.Result += c.op * p.Result().Value(0).(int64)
p.Handle(c.operatorOrEndOfFile)
} else {
@ -86,13 +86,13 @@ func (c *simpleCalculator) number(p *parsekit.ParseAPI) {
func (c *simpleCalculator) operatorOrEndOfFile(p *parsekit.ParseAPI) {
var A = parsekit.A
switch {
case p.On(A.Add).Skip():
case p.Accept(A.Add):
c.op = +1
p.Handle(c.number)
case p.On(A.Subtract).Skip():
case p.Accept(A.Subtract):
c.op = -1
p.Handle(c.number)
case !p.On(A.EndOfFile).Stay():
case !p.Accept(A.EndOfFile):
p.Expected("operator, '+' or '-'")
default:
p.ExpectEndOfFile()

View File

@ -96,7 +96,7 @@ func (calc *calculator) expr(p *parsekit.ParseAPI) {
var C, A = parsekit.C, parsekit.A
if p.Handle(calc.term) {
for p.On(C.Any(A.Add, A.Subtract)).Accept() {
for p.Accept(C.Any(A.Add, A.Subtract)) {
op := p.Result().Rune(0)
if !p.Handle(calc.term) {
return
@ -114,7 +114,7 @@ func (calc *calculator) term(p *parsekit.ParseAPI) {
var C, A = parsekit.C, parsekit.A
if p.Handle(calc.factor) {
for p.On(C.Any(A.Multiply, A.Divide)).Accept() {
for p.Accept(C.Any(A.Multiply, A.Divide)) {
op := p.Result().Rune(0)
if !p.Handle(calc.factor) {
return
@ -130,16 +130,16 @@ func (calc *calculator) term(p *parsekit.ParseAPI) {
// <factor> = <space> (FLOAT | LPAREN <expr> RPAREN) <space>
func (calc *calculator) factor(p *parsekit.ParseAPI) {
var A, T = parsekit.A, parsekit.T
p.On(A.Blanks).Skip()
p.Accept(A.Blanks)
switch {
case p.On(T.Float64(nil, A.Signed(A.Float))).Accept():
case p.Accept(T.Float64(nil, A.Signed(A.Float))):
value := p.Result().Value(0).(float64)
calc.interpreter.pushValue(value)
case p.On(A.LeftParen).Skip():
case p.Accept(A.LeftParen):
if !p.Handle(calc.expr) {
return
}
if !p.On(A.RightParen).Skip() {
if !p.Accept(A.RightParen) {
p.Expected("')'")
return
}
@ -147,7 +147,7 @@ func (calc *calculator) factor(p *parsekit.ParseAPI) {
p.Expected("factor or parenthesized expression")
return
}
p.On(A.Blanks).Skip()
p.Accept(A.Blanks)
}
// ---------------------------------------------------------------------------

View File

@ -82,7 +82,7 @@ func (h *helloparser1) Parse(input string) (string, *parsekit.Error) {
func (h *helloparser1) start(p *parsekit.ParseAPI) {
a := parsekit.A
if p.On(a.StrNoCase("hello")).Skip() {
if p.Accept(a.StrNoCase("hello")) {
p.Handle(h.comma)
} else {
p.Expected("hello")
@ -92,9 +92,9 @@ func (h *helloparser1) start(p *parsekit.ParseAPI) {
func (h *helloparser1) comma(p *parsekit.ParseAPI) {
a := parsekit.A
switch {
case p.On(a.Blanks).Skip():
case p.Accept(a.Blanks):
p.Handle(h.comma)
case p.On(a.Comma).Skip():
case p.Accept(a.Comma):
p.Handle(h.startName)
default:
p.Expected("comma")
@ -103,8 +103,8 @@ func (h *helloparser1) comma(p *parsekit.ParseAPI) {
func (h *helloparser1) startName(p *parsekit.ParseAPI) {
a := parsekit.A
p.On(a.Blanks).Skip()
if p.On(a.AnyRune).Stay() {
p.Accept(a.Blanks)
if p.Peek(a.AnyRune) {
p.Handle(h.name)
} else {
p.Expected("name")
@ -114,9 +114,9 @@ func (h *helloparser1) startName(p *parsekit.ParseAPI) {
func (h *helloparser1) name(p *parsekit.ParseAPI) {
a := parsekit.A
switch {
case p.On(a.Excl).Stay():
case p.Peek(a.Excl):
p.Handle(h.exclamation)
case p.On(a.AnyRune).Accept():
case p.Accept(a.AnyRune):
h.greetee += p.Result().String()
p.Handle(h.name)
default:
@ -126,7 +126,7 @@ func (h *helloparser1) name(p *parsekit.ParseAPI) {
func (h *helloparser1) exclamation(p *parsekit.ParseAPI) {
a := parsekit.A
if p.On(a.Excl).Accept() {
if p.Accept(a.Excl) {
p.Handle(h.end)
} else {
p.Expected("exclamation")
@ -138,7 +138,7 @@ func (h *helloparser1) exclamation(p *parsekit.ParseAPI) {
// error message.
func (h *helloparser1) end(p *parsekit.ParseAPI) {
var a = parsekit.A
if !p.On(a.EndOfFile).Stay() {
if !p.Accept(a.EndOfFile) {
p.Expected("end of greeting")
return
}

View File

@ -80,15 +80,15 @@ func (h *helloparser2) Parse(input string) (string, *parsekit.Error) {
func (h *helloparser2) start(p *parsekit.ParseAPI) {
c, a, m := parsekit.C, parsekit.A, parsekit.M
if !p.On(a.StrNoCase("hello")).Skip() {
if !p.Accept(a.StrNoCase("hello")) {
p.Error("the greeting is not being friendly")
return
}
if !p.On(c.Seq(c.Opt(a.Blank), a.Comma, c.Opt(a.Blank))).Skip() {
if !p.Accept(c.Seq(c.Opt(a.Blank), a.Comma, c.Opt(a.Blank))) {
p.Error("the greeting is not properly separated")
return
}
if p.On(m.TrimSpace(c.OneOrMore(c.Except(a.Excl, a.AnyRune)))).Accept() {
if p.Accept(m.TrimSpace(c.OneOrMore(c.Except(a.Excl, a.AnyRune)))) {
h.greetee = p.Result().String()
if h.greetee == "" {
p.Error("the name cannot be empty")
@ -98,9 +98,9 @@ func (h *helloparser2) start(p *parsekit.ParseAPI) {
p.Error("the greeting is targeted at thin air")
return
}
if !p.On(a.Excl).Skip() {
if !p.Accept(a.Excl) {
p.Error("the greeting is not loud enough")
} else if !p.On(a.EndOfFile).Stay() {
} else if !p.Accept(a.EndOfFile) {
p.Error("too much stuff going on after the closing '!'")
} else {
p.Stop()

View File

@ -20,7 +20,7 @@ func (l *Chunks) AddChopped(s string, chunkSize int) *parsekit.Error {
chunkOfRunes := c.MinMax(1, chunkSize, a.AnyRune)
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
for p.On(chunkOfRunes).Accept() {
for p.Accept(chunkOfRunes) {
*l = append(*l, p.Result().String())
}
})

View File

@ -15,6 +15,59 @@ type ParseAPI struct {
stopped bool // a boolean set to true by Stop(), further ParseAPI calls are ignored
}
// Peek checks if the upcoming input data matches the provided TokenHandler.
// If it does, then true will be returned, false otherwise. The read cursor
// will be kept at the same position, so the next call to Peek() or Accept()
// will start from the same cursor position.
//
// After calling this method, you can retrieve the produced TokenHandlerResult
// using the ParseAPI.Result() method.
func (p *ParseAPI) Peek(tokenHandler TokenHandler) bool {
p.result = nil
forkedTokenAPI, ok := p.invokeTokenHandler("Peek", tokenHandler)
if ok {
p.result = forkedTokenAPI.Result()
p.tokenAPI.clearResults()
p.tokenAPI.detachChilds()
}
return ok
}
// Accept checks if the upcoming input data matches the provided TokenHandler.
// If it does, then true will be returned, false otherwise. The read cursor
// will be moved forward to beyond the match that was found.
//
// After calling this method, you can retrieve the produced TokenHandlerResult
// using the ParseAPI.Result() method.
func (p *ParseAPI) Accept(tokenHandler TokenHandler) bool {
p.result = nil
forkedTokenAPI, ok := p.invokeTokenHandler("Accept", tokenHandler)
if ok {
forkedTokenAPI.Merge()
p.result = p.tokenAPI.Result()
p.tokenAPI.detachChilds()
if p.tokenAPI.flushReader() {
p.initLoopCheck()
}
}
return ok
}
func (p *ParseAPI) invokeTokenHandler(name string, tokenHandler TokenHandler) (*TokenAPI, bool) {
p.panicWhenStoppedOrInError()
p.checkForLoops()
if tokenHandler == nil {
callerPanic(2, "parsekit.ParseAPI.%s(): %s() called with nil tokenHandler argument at {caller}", name, name)
}
p.result = nil
p.tokenAPI.clearResults()
child := p.tokenAPI.Fork()
ok := tokenHandler(child)
return child, ok
}
// panicWhenStoppedOrInError will panic when the parser has produced an error
// or when it has been stopped. It is used from the ParseAPI methods, to
// prevent further calls to the ParseAPI on these occasions.
@ -47,147 +100,23 @@ func (p *ParseAPI) initLoopCheck() {
}
func (p *ParseAPI) checkForLoops() {
filepos := callerFilepos(2)
filepos := callerFilepos(3)
if _, ok := p.loopCheck[filepos]; ok {
callerPanic(2, "parsekit.ParseAPI: Loop detected in parser at {caller}")
callerPanic(3, "parsekit.ParseAPI: Loop detected in parser at {caller}")
}
p.loopCheck[filepos] = true
}
// On checks if the input at the current cursor position matches the provided
// TokenHandler. On must be chained with another method that tells the parser
// what action to perform when a match was found:
//
// 1) On(...).Skip() - Move read cursor forward, ignoring the match results.
//
// 2) On(...).Accept() - Move cursor, making results available through Result()
//
// 3) On(...).Stay() - Do nothing, the cursor stays at the same position.
//
// So an example chain could look like this:
//
// p.On(parsekit.A.Blank).Skip()
//
// The chain as a whole returns a boolean that indicates whether or not at match
// was found. When no match was found, false is returned and Skip() and Accept()
// will have no effect. Because of this, typical use of an On() chain is as
// expression for a conditional statement (if, switch/case, for). E.g.:
//
// // Skip multiple exclamation marks.
// for p.On(parsekit.A.Excl).Skip() { }
//
// // Fork a route based on the input.
// switch {
// case p.On(parsekit.A.Excl).Stay()
// p.RouteTo(stateHandlerA)
// case p.On(parsekit.A.Colon).Stay():
// p.RouteTo(stateHandlerB)
// default:
// p.RouteTo(stateHandlerC)
// }
//
// // Echo back a sequence of digits on the input.
// if p.On(parsekit.A.Digits).Accept() {
// fmt.Println(p.Result().String())
// }
func (p *ParseAPI) On(tokenHandler TokenHandler) *ParseAPIOnAction {
p.panicWhenStoppedOrInError()
p.checkForLoops()
if tokenHandler == nil {
callerPanic(1, "parsekit.ParseAPI.On(): On() called with nil tokenHandler argument at {caller}")
}
p.result = nil
p.tokenAPI.clearResults()
child := p.tokenAPI.Fork()
ok := tokenHandler(child)
return &ParseAPIOnAction{
parseAPI: p,
tokenAPI: p.tokenAPI,
forkedTokenAPI: child,
ok: ok,
}
}
// ParseAPIOnAction is a struct that is used for building the On()-method chain.
// The On() method will return an initialized struct of this type.
type ParseAPIOnAction struct {
parseAPI *ParseAPI
tokenAPI *TokenAPI
forkedTokenAPI *TokenAPI
ok bool
}
// Accept tells the parser to move the read cursor past a match that was
// found by a TokenHandler, and to make the TokenHandlerResult from the
// TokenAPI available in the ParseAPI through the ParseAPI.Result() method.
//
// Returns true in case a match was found by On().
// When no match was found, then no action is taken, no results are
// exposed and false is returned.
func (a *ParseAPIOnAction) Accept() bool {
if a.ok {
a.forkedTokenAPI.Merge()
a.parseAPI.result = a.tokenAPI.Result()
a.tokenAPI.detachChilds()
if a.tokenAPI.flushReader() {
a.parseAPI.initLoopCheck()
}
}
return a.ok
}
// Skip tells the parser to move the cursor past a match that was found,
// without making the results available through the ParseAPI.
//
// Note that functionally, you could call Accept() just as well, simply
// ignoring the results. However, the Skip() call is a bit more efficient
// than the Accept() call and (more important if you ask me) the code
// expresses more clearly that your intent is to skip the match.
//
// Returns true in case a match was found by On().
// When no match was found, then no action is taken and false is returned.
func (a *ParseAPIOnAction) Skip() bool {
if a.ok {
a.parseAPI.result = nil
a.forkedTokenAPI.clearResults()
a.tokenAPI.detachChilds()
a.forkedTokenAPI.syncCursorTo(a.tokenAPI)
if a.tokenAPI.flushReader() {
a.parseAPI.initLoopCheck()
}
}
return a.ok
}
// Stay tells the parser to not move the cursor after finding a match.
//
// A typical use of Stay() is to let one ParseHandler detect the start
// of some kind of token, but without moving the read cursor forward.
// When a match is found, it hands off control to another ParseHandler
// to take care of the actual token parsing.
//
// Returns true in case a match was found by On(), false otherwise.
func (a *ParseAPIOnAction) Stay() bool {
if a.ok {
a.parseAPI.result = nil
a.tokenAPI.clearResults()
a.tokenAPI.detachChilds()
}
return a.ok
}
// Result returns a TokenHandlerResult struct, containing results as produced by the
// last ParseAPI.On().Accept() call.
// last Peek() or Accept() call.
//
// When Result() is called without first doing a ParsAPI.On().Accept(), then no
// When Result() is called without first doing a Peek() or Accept(), then no
// result will be available and the method will panic.
func (p *ParseAPI) Result() *TokenHandlerResult {
result := p.result
if p.result == nil {
callerPanic(1, "parsekit.ParseAPI.TokenHandlerResult(): TokenHandlerResult() called "+
"at {caller} without calling ParseAPI.Accept() on beforehand")
"at {caller} without calling ParseAPI.Peek() or ParseAPI.Accept() on beforehand")
}
return result
}
@ -246,7 +175,7 @@ func (p *ParseAPI) Error(format string, args ...interface{}) {
// as the expectation.
func (p *ParseAPI) ExpectEndOfFile() {
p.panicWhenStoppedOrInError()
if p.On(A.EndOfFile).Stay() {
if p.Peek(A.EndOfFile) {
p.Stop()
} else {
p.Expected("end of file")

View File

@ -14,7 +14,7 @@ func ExampleParser_usingAcceptedRunes() {
matches := []string{}
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
for p.On(a.AnyRune).Accept() {
for p.Accept(a.AnyRune) {
matches = append(matches, p.Result().String())
}
p.ExpectEndOfFile()
@ -31,7 +31,7 @@ func ExampleParser_usingTokens() {
c, a, tok := parsekit.C, parsekit.A, parsekit.T
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
if p.On(c.OneOrMore(tok.Rune("RUNE", a.AnyRune))).Accept() {
if p.Accept(c.OneOrMore(tok.Rune("RUNE", a.AnyRune))) {
fmt.Printf("Runes accepted: %q\n", p.Result().String())
fmt.Printf("Token values: %s\n", p.Result().Tokens())
}
@ -55,12 +55,12 @@ func ExampleParseAPI_Expected() {
// unexpected input (expected a thing) at start of file
}
func ExampleParseAPIOnAction_Accept() {
func ExampleParseAPI_Accept_inIfStatement() {
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
// When a case-insensitive match on "Yowza!" is found by the
// tokenizer, then Accept() will make the result available
// through ParseAPI.Result()
if p.On(parsekit.A.StrNoCase("Yowza!")).Accept() {
if p.Accept(parsekit.A.StrNoCase("Yowza!")) {
// Result.String() returns a string containing all
// accepted runes that were matched against.
fmt.Println(p.Result().String())
@ -72,14 +72,14 @@ func ExampleParseAPIOnAction_Accept() {
// YOWZA!
}
func ExampleParseAPIOnAction_Skip() {
func ExampleParseAPI_Accept_inSwitchStatement() {
var result string
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
for loop := true; loop; {
switch {
case p.On(parsekit.A.Rune('X')).Skip():
case p.Accept(parsekit.A.Rune('X')):
// NOOP, skip this rune
case p.On(parsekit.A.AnyRune).Accept():
case p.Accept(parsekit.A.AnyRune):
result += p.Result().String()
default:
loop = false
@ -98,7 +98,7 @@ func ExampleParseAPI_Stop() {
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
fmt.Printf("First word: ")
for p.On(C.Not(A.Space)).Accept() {
for p.Accept(C.Not(A.Space)) {
fmt.Printf("%s", p.Result())
}
p.Stop()
@ -114,7 +114,7 @@ func ExampleParseAPI_Stop_notCalledAndNoInputPending() {
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
fmt.Printf("Word: ")
for p.On(C.Not(A.Space)).Accept() {
for p.Accept(C.Not(A.Space)) {
fmt.Printf("%s", p.Result())
}
fmt.Printf("\n")
@ -132,7 +132,7 @@ func ExampleParseAPI_Stop_notCalledButInputPending() {
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
fmt.Printf("First word: ")
for p.On(C.Not(A.Space)).Accept() {
for p.Accept(C.Not(A.Space)) {
fmt.Printf("%s", p.Result())
}
fmt.Printf("\n")
@ -145,14 +145,14 @@ func ExampleParseAPI_Stop_notCalledButInputPending() {
// Error: unexpected input (expected end of file) at line 1, column 6
}
func ExampleParseAPIOnAction_Stay() {
func ExampleParseAPI_Peek() {
// Definition of a fantasy serial number format.
C, A := parsekit.C, parsekit.A
serialnr := C.Seq(A.Asterisk, A.ASCIIUpper, A.ASCIIUpper, A.Digits)
// This handler is able to handle serial numbers.
serialnrHandler := func(p *parsekit.ParseAPI) {
if p.On(serialnr).Accept() {
if p.Accept(serialnr) {
fmt.Println(p.Result().String())
}
}
@ -160,7 +160,7 @@ func ExampleParseAPIOnAction_Stay() {
// Start could function as a sort of dispatcher, handing over
// control to the correct ParseHandler function, based on the input.
start := func(p *parsekit.ParseAPI) {
if p.On(parsekit.A.Asterisk).Stay() {
if p.Peek(parsekit.A.Asterisk) {
p.Handle(serialnrHandler)
return
}
@ -195,14 +195,25 @@ func TestGivenNullHandler_HandlePanics(t *testing.T) {
Expect: `parsekit\.ParseAPI\.Handle\(\): Handle\(\) called with nil input ` +
`at /.*/parser_test\.go:\d+`})
}
func TestGivenNilTokenHandler_OnPanics(t *testing.T) {
func TestGivenNilTokenHandler_AcceptPanics(t *testing.T) {
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {
p.On(nil)
p.Accept(nil)
})
parsekit.AssertPanic(t, parsekit.PanicT{
Function: func() { p.Execute("") },
Regexp: true,
Expect: `parsekit\.ParseAPI\.On\(\): On\(\) called with nil ` +
Expect: `parsekit\.ParseAPI\.Accept\(\): Accept\(\) called with nil ` +
`tokenHandler argument at /.*/parser_test\.go:\d+`})
}
func TestGivenNilTokenHandler_PeekPanics(t *testing.T) {
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {
p.Peek(nil)
})
parsekit.AssertPanic(t, parsekit.PanicT{
Function: func() { p.Execute("") },
Regexp: true,
Expect: `parsekit\.ParseAPI\.Peek\(\): Peek\(\) called with nil ` +
`tokenHandler argument at /.*/parser_test\.go:\d+`})
}
@ -236,7 +247,7 @@ func TestGivenParserWithErrorSet_HandlePanics(t *testing.T) {
`at /.*/parser_test\.go:\d+: no calls allowed after ParseAPI\.Error\(\)`})
}
func TestGivenParserWithoutCallToAccept_ResultPanics(t *testing.T) {
func TestGivenParserWithoutCallToPeekOrAccept_ResultPanics(t *testing.T) {
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {
p.Result()
})
@ -244,7 +255,7 @@ func TestGivenParserWithoutCallToAccept_ResultPanics(t *testing.T) {
Function: func() { p.Execute("") },
Regexp: true,
Expect: `parsekit\.ParseAPI\.TokenHandlerResult\(\): TokenHandlerResult\(\) called at ` +
`/.*/parser_test.go:\d+ without calling ParseAPI.Accept\(\) on beforehand`})
`/.*/parser_test.go:\d+ without calling ParseAPI.Peek\(\) or ParseAPI.Accept\(\) on beforehand`})
}
func TestGivenParserWhichIsNotStopped_WithNoMoreInput_FallbackExpectEndOfFileKicksIn(t *testing.T) {
@ -264,12 +275,12 @@ type parserWithLoop struct {
}
func (l *parserWithLoop) first(p *parsekit.ParseAPI) {
p.On(parsekit.A.ASCII).Accept()
p.Accept(parsekit.A.ASCII)
p.Handle(l.second)
}
func (l *parserWithLoop) second(p *parsekit.ParseAPI) {
p.On(parsekit.A.ASCII).Accept()
p.Accept(parsekit.A.ASCII)
p.Handle(l.third)
}
@ -278,7 +289,7 @@ func (l *parserWithLoop) third(p *parsekit.ParseAPI) {
p.Error("Loop not detected by parsekit")
return
}
p.On(parsekit.A.ASCII).Accept()
p.Accept(parsekit.A.ASCII)
p.Handle(l.first)
}
@ -307,7 +318,7 @@ func TestGivenLoopingParserDefinition_ParserPanics(t *testing.T) {
func TestGivenLoopingParserDefinition2_ParserPanics(t *testing.T) {
var c, a = parsekit.C, parsekit.A
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
for p.On(c.Max(5, a.AnyRune)).Accept() {
for p.Accept(c.Max(5, a.AnyRune)) {
}
p.Stop()
})

View File

@ -390,7 +390,7 @@ func TestSequenceOfRunes(t *testing.T) {
input := "#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
output := ""
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
if p.On(sequence).Accept() {
if p.Accept(sequence) {
output = p.Result().String()
p.Stop()
} else {

View File

@ -26,7 +26,7 @@ type TokenHandler func(t *TokenAPI) bool
func NewTokenizer(tokenHandler TokenHandler) *Tokenizer {
tokenizer := &Tokenizer{}
tokenizer.parser = NewParser(func(p *ParseAPI) {
if p.On(tokenHandler).Accept() {
if p.Accept(tokenHandler) {
tokenizer.result = p.Result()
p.Stop()
} else {