Browse files

add times and check

  • Loading branch information...
1 parent 9aaa887 commit 6e9e5ad68363b1f1a2d81d5a30af908421962469 Robey Pointer committed Jun 13, 2012
Showing with 106 additions and 15 deletions.
  1. +36 −5 README.md
  2. +41 −10 src/packrattle/parser.coffee
  3. +29 −0 test/test_parser.coffee
View
41 README.md
@@ -66,10 +66,15 @@ the real power is in combining the parsers:
- `parser.optional(p)` - match p or return the empty string, succeeding
either way
-- `parser.repeat(p, atLeast, sep)` - match p multiple times (often written
- as "`p*`"), optionally separated by `sep` (for example, a comma); the match
- result will be an array of all the non-null match results, not including
- the separators
+- `parser.check(p)` - verify that p matches, but don't advance the parser's
+ position
+
+- `parser.repeat(p, sep)` - match p multiple times (often written as "`p*`"),
+ optionally separated by `sep` (for example, a comma); the match result will
+ be an array of all the non-null match results, not including the separators
+
+- `parser.times(count, p)` - match p exactly count times; the match result
+ will be an array of all the non-null match results
- `parser.foldLeft(args)` - see below
@@ -98,7 +103,9 @@ each parser has methods on it, also, to allow for combining:
- `optional()` - make this parser optional, like `parser.optional`
-- `repeat(atLeast, sep)` - just like `parser.repeat`
+- `repeat(sep)` - just like `parser.repeat`
+
+- `times(count)` - just like `parser.times`
- `reduce(sep, function)` - a simpler variant of `foldLeft` (see below)
@@ -159,3 +166,27 @@ parser at runtime, to simplify your code:
- a function will be called, under the assumption that it returns a parser --
but only when the parser is `parse`d, allowing for lazy evaluation
+executing
+---------
+
+to execute a parser, call either:
+
+- `parse(string)` - matches as much of the string as it can
+- `consume(string)` - matches the entire string, or fails
+
+each function returns an object with state:
+
+- `ok` - true if the parser succeeded, false if not
+- `state` - a `ParserState` object with the current position (see below)
+- `match` - the match result (if `ok` is true)
+- `message` - a string error message (if `ok` is false)
+
+the `ParserState` object contains:
+
+- `text` - the original string
+- `pos` - the index within the string that the parser stopped
+- `lineno` - the current line number of `pos`, assuming `\n` divides lines,
+ counting from 0
+- `xpos` - the position of `pos` within the current line, counting from 0
+- `line()` - returns the content of the line around `pos` (the `lineno` line)
+
View
51 src/packrattle/parser.coffee
@@ -121,14 +121,19 @@ class Parser
repeat: (sep = null) -> repeat(@, sep)
+ times: (count) -> times(count, @)
+
reduce: (sep, f) -> foldLeft(tail: @, accumulator: ((x) -> x), fold: f, sep: sep)
# throw away the match
- drop: ->
- @onMatch (m) -> null
+ drop: -> @onMatch (m) -> null
- exec: (s) ->
- @parse(newState(s))
+ # verify that this parser matches, but don't advance the position
+ check: ->
+ new Parser @message, (state) =>
+ rv = @parse(state)
+ if not rv.ok then return rv
+ new Match(state, rv.match)
# matches the end of the string
end = new Parser "end", (state) ->
@@ -190,6 +195,19 @@ optional = (p) ->
# optionally separated by a separation parser
repeat = (p, sep = null) -> foldLeft(tail: p, sep: sep)
+# exactly N repetitions of a parser
+times = (count, p) ->
+ p = implicit(p)
+ new Parser "#{count} of (#{p.message})", (state) ->
+ p = resolve(p)
+ results = []
+ for i in [0...count]
+ rv = p.parse(state)
+ if not rv.ok then return @fail(rv.state)
+ if rv.match? then results.push(rv.match)
+ state = rv.state
+ new Match(state, results)
+
# match against the 'first' parser, then any number of occurances of 'sep'
# followed by 'tail', as in: `first (sep tail)*`.
#
@@ -203,8 +221,16 @@ repeat = (p, sep = null) -> foldLeft(tail: p, sep: sep)
foldLeft = (args) ->
tail = implicit(if args.tail? then args.tail else reject)
first = implicit(if args.first? then args.first else args.tail)
- fold = if args.fold? then args.fold else ((a, s, t) -> a.push(t); a)
- accumulator = if args.accumulator? then args.accumulator else ((x) -> [ x ])
+ fold = if args.fold?
+ args.fold
+ else
+ (acc, s, item) ->
+ if item? then acc.push(item)
+ acc
+ accumulator = if args.accumulator?
+ args.accumulator
+ else
+ (x) -> if x? then [ x ] else []
sep = args.sep
message = if args.first?
"(#{first.message}) followed by (#{tail.message})*"
@@ -248,11 +274,11 @@ resolve = (p) ->
implicit(p())
# helper for drop
-drop = (p) ->
- implicit(p).drop()
+drop = (p) -> implicit(p).drop()
-parse = (p, s) ->
- implicit(p).parse(s)
+parse = (p, s) -> implicit(p).parse(s)
+
+check = (p) -> implicit(p).check()
exports.newState = newState
exports.ParserState = ParserState
@@ -267,7 +293,12 @@ exports.regex = regex
exports.seq = seq
exports.optional = optional
exports.repeat = repeat
+exports.times = times
exports.foldLeft = foldLeft
exports.implicit = implicit
exports.drop = drop
exports.parse = parse
+exports.check = check
+
+# to-do: guard (match p but don't move the pointer)
+# times (exactly n times)
View
29 test/test_parser.coffee
@@ -255,6 +255,35 @@ describe "Parser", ->
rv.ok.should.equal(false)
rv.message.should.match(/end/)
+ it "can perform a non-advancing check", ->
+ p = parser.seq("hello", parser.check("there"), "th")
+ rv = p.parse("hellothere")
+ rv.ok.should.equal(true)
+ rv.match.should.eql([ "hello", "there", "th" ])
+ rv = p.parse("helloth")
+ rv.ok.should.equal(false)
+ rv.message.should.match(/there/)
+
+ it "can match exactly N times", ->
+ p = parser.string("hi").times(4)
+ rv = p.parse("hihihihihi")
+ rv.ok.should.equal(true)
+ rv.match.should.eql([ "hi", "hi", "hi", "hi" ])
+ rv.state.pos.should.equal(8)
+ rv = p.parse("hihihi")
+ rv.ok.should.equal(false)
+ rv.message.should.match(/4 of \('hi'\)/)
+
+ it "drops inside repeat/times", ->
+ p = parser.string("123").drop().repeat()
+ rv = p.parse("123123")
+ rv.ok.should.equal(true)
+ rv.match.should.eql([])
+ p = parser.string("123").drop().times(2)
+ rv = p.parse("123123")
+ rv.ok.should.equal(true)
+ rv.match.should.eql([])
+
describe "Parser#foldLeft", ->
it "matches one", ->
p = parser.foldLeft(tail: parser.regex(/\d+/).onMatch((x) -> x[0]), sep: /\s*,\s*/)

0 comments on commit 6e9e5ad

Please sign in to comment.