From b80c2c3f02e8526d06f58d31c1fceb85d83b418f Mon Sep 17 00:00:00 2001 From: Brian Mock Date: Tue, 20 Jun 2017 19:06:17 -0700 Subject: [PATCH] Fixes #177 adds seqObj --- API.md | 39 ++++++++++++++++++++++-- CHANGELOG.md | 6 ++++ examples/python-ish.js | 10 +++--- examples/seqobj.js | 44 +++++++++++++++++++++++++++ package.json | 2 +- src/parsimmon.js | 66 +++++++++++++++++++++++++++++++++++++++- test/core/seqObj.test.js | 65 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 examples/seqobj.js create mode 100644 test/core/seqObj.test.js diff --git a/API.md b/API.md index 4da551a..a367951 100644 --- a/API.md +++ b/API.md @@ -178,13 +178,14 @@ Returns a parser that doesn't consume any input, and yields `result`. This is an alias for `Parsimmon.succeed(result)`. +## Parsimmon.formatError(string, error) + +Takes the `string` passed to `parser.parse(string)` and the `error` returned from `parser.parse(string)` and turns it into a human readable error message string. Note that there are certainly better ways to format errors, so feel free to write your own. + ## Parsimmon.seq(p1, p2, ...pn) Accepts any number of parsers and returns a new parser that expects them to match in order, yielding an array of all their results. -## Parsimmon.formatError(string, error) - -Takes the `string` passed to `parser.parse(string)` and the `error` returned from `parser.parse(string)` and turns it into a human readable error message string. Note that there are certainly better ways to format errors, so feel free to write your own. ## Parsimmon.seqMap(p1, p2, ...pn, function(r1, r2, ...rn)) @@ -204,6 +205,38 @@ Parsimmon.seqMap( ).parse('a+x') ``` +## Parsimmon.seqObj(...args) + +Similar to `Parsimmon.seq(...parsers)`, but yields an object of results named based on arguments. + +Takes one or more arguments, where each argument is either a parser or a named parser pair (`[stringKey, parser]`). + +Requires at least one named parser. + +All named parser keys must be unique. + +Example: + +```js +var _ = Parsimmon.optWhitespace; +var identifier = Parsimmon.regexp(/[a-z_][a-z0-9_]*/i); +var lparen = Parsimmon.string('('); +var rparen = Parsimmon.string(')'); +var comma = Parsimmon.string(','); +var functionCall = + Parsimmon.seqObj( + ['function', identifier], + lparen, + ['arguments', identifier.trim(_).sepBy(comma)], + rparen + ); +functionCall.tryParse('foo(bar, baz, quux)'); +// => { function: 'foo', +// arguments: [ 'bar', 'baz', 'quux' ] } +``` + +Tip: Combines well with `.node(name)` for a full-featured AST node. + ## Parsimmon.alt(p1, p2, ...pn) Accepts any number of parsers, yielding the value of the first one that succeeds, backtracking in between. diff --git a/CHANGELOG.md b/CHANGELOG.md index f91a252..8bc0726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +## version 1.6.0 (2017-06-26) + +* Adds `Parsimmon.seqObj(...args)` + ## version 1.5.0 (2017-06-17) +NOTE: Code was completed on 2017-06-17, but due to human error, was not published on npm until 2017-06-26. + * Adds `parser.sepBy(separator)` alias for `Parsimmon.sepBy(parser, separator)` * Adds `parser.sepBy1(separator)` alias for `Parsimmon.sepBy1(parser, separator)` * Adds `Parsimmon.range(begin, end)` diff --git a/examples/python-ish.js b/examples/python-ish.js index a89615b..22b3409 100644 --- a/examples/python-ish.js +++ b/examples/python-ish.js @@ -51,14 +51,16 @@ let Pythonish = P.createLanguage({ // block, and require that every other statement has the same exact string of // indentation in front of it. Block: r => - P.seq( - P.string('block:\n').then(P.regexp(/[ ]+/)), - r.Statement + P.seqObj( + P.string('block:'), + P.string('\n'), + ['indent', P.regexp(/[ ]+/)], + ['statement', r.Statement] ).chain(args => { // `.chain` is called after a parser succeeds. It returns the next parser // to use for parsing. This allows subsequent parsing to be dependent on // previous text. - let [indent, statement] = args; + let {indent, statement} = args; let indentSize = indent.length; let currentSize = indentPeek(); // Indentation must be deeper than the current block context. Otherwise diff --git a/examples/seqobj.js b/examples/seqobj.js new file mode 100644 index 0000000..ff71141 --- /dev/null +++ b/examples/seqobj.js @@ -0,0 +1,44 @@ +'use strict'; + +// Run me with Node to see my output! + +let util = require('util'); +let P = require('../'); + +/////////////////////////////////////////////////////////////////////// + +let Lang = P.createLanguage({ + + _: () => P.optWhitespace, + + LParen: () => P.string('('), + RParen: () => P.string(')'), + Comma: () => P.string(','), + Dot: () => P.string('.'), + + Identifier: () => P.letters.node('Identifier'), + + MethodCall: r => + P.seqObj( + ['receiver', r.Identifier], + r.Dot.trim(r._), + ['method', r.Identifier], + r.LParen, + ['arguments', r.Identifier.trim(r._).sepBy(r.Comma)], + r.RParen + ).node('MethodCall'), + +}); + +/////////////////////////////////////////////////////////////////////// + +let text = 'console.log(bar, baz, quux)'; + +function prettyPrint(x) { + let opts = {depth: null, colors: 'auto'}; + let s = util.inspect(x, opts); + console.log(s); +} + +let ast = Lang.MethodCall.tryParse(text); +prettyPrint(ast); diff --git a/package.json b/package.json index f58d9bd..24dd739 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parsimmon", - "version": "1.5.0", + "version": "1.6.0", "description": "A monadic LL(infinity) parser combinator library", "keywords": [ "parsing", diff --git a/src/parsimmon.js b/src/parsimmon.js index 9ba1e01..d299f0a 100644 --- a/src/parsimmon.js +++ b/src/parsimmon.js @@ -15,6 +15,10 @@ function isParser(obj) { return obj instanceof Parsimmon; } +function isArray(x) { + return {}.toString.call(x) === '[object Array]'; +} + function makeSuccess(index, value) { return { status: true, @@ -107,7 +111,7 @@ function assertParser(p) { // TODO[ES5]: Switch to Array.isArray eventually. function assertArray(x) { - if ({}.toString.call(x) !== '[object Array]') { + if (!isArray(x)) { throw new Error('not an array: ' + x); } } @@ -203,6 +207,65 @@ function seq() { }); } +function seqObj() { + var seenKeys = {}; + var totalKeys = 0; + var parsers = [].slice.call(arguments); + var numParsers = parsers.length; + for (var j = 0; j < numParsers; j += 1) { + var p = parsers[j]; + if (isParser(p)) { + continue; + } + if (isArray(p)) { + var isWellFormed = + p.length === 2 && + typeof p[0] === 'string' && + isParser(p[1]); + if (isWellFormed) { + var key = p[0]; + if (seenKeys[key]) { + throw new Error('seqObj: duplicate key ' + key); + } + seenKeys[key] = true; + totalKeys++; + continue; + } + } + throw new Error( + 'seqObj arguments must be parsers or ' + + '[string, parser] array pairs.' + ); + } + if (totalKeys === 0) { + throw new Error('seqObj expects at least one named parser, found zero'); + } + return Parsimmon(function(input, i) { + var result; + var accum = {}; + for (var j = 0; j < numParsers; j += 1) { + var name; + var parser; + if (isArray(parsers[j])) { + name = parsers[j][0]; + parser = parsers[j][1]; + } else { + name = null; + parser = parsers[j]; + } + result = mergeReplies(parser._(input, i), result); + if (!result.status) { + return result; + } + if (name) { + accum[name] = result.value; + } + i = result.index; + } + return mergeReplies(makeSuccess(i, accum), result); + }); +} + function seqMap() { var args = [].slice.call(arguments); if (args.length === 0) { @@ -720,6 +783,7 @@ Parsimmon.sepBy = sepBy; Parsimmon.sepBy1 = sepBy1; Parsimmon.seq = seq; Parsimmon.seqMap = seqMap; +Parsimmon.seqObj = seqObj; Parsimmon.string = string; Parsimmon.succeed = succeed; Parsimmon.takeWhile = takeWhile; diff --git a/test/core/seqObj.test.js b/test/core/seqObj.test.js new file mode 100644 index 0000000..0571025 --- /dev/null +++ b/test/core/seqObj.test.js @@ -0,0 +1,65 @@ +'use strict'; + +suite('Parsimmon.seqObj', function() { + + test('does not accept duplicate keys', function() { + assert.throws(function() { + Parsimmon.seqObj( + ['a', Parsimmon.of(1)], + ['b', Parsimmon.of(2)], + ['a', Parsimmon.of(3)] + ); + }); + }); + + test('requires at least one key', function() { + assert.throws(function() { + Parsimmon.seqObj(); + }); + }); + + test('every key is present in the result object', function() { + var parser = Parsimmon.seqObj( + ['a', Parsimmon.of(1)], + ['b', Parsimmon.of(2)], + ['c', Parsimmon.of(3)] + ); + var result = parser.tryParse(''); + assert.deepStrictEqual(result, { + a: 1, + b: 2, + c: 3, + }); + }); + + test('every parser is used', function() { + var parser = Parsimmon.seqObj( + Parsimmon.string('a'), + ['b', Parsimmon.string('b')], + Parsimmon.string('c'), + ['d', Parsimmon.string('d')], + Parsimmon.string('e') + ); + var result = parser.tryParse('abcde'); + assert.deepStrictEqual(result, { + b: 'b', + d: 'd', + }); + }); + + test('named parser arrays are formed properly', function() { + assert.throws(function() { + Parsimmon.seqObj([]); + }); + assert.throws(function() { + Parsimmon.seqObj(['a', Parsimmon.of(1), 'oops extra']); + }); + assert.throws(function() { + Parsimmon.seqObj([123, Parsimmon.of(1)]); + }); + assert.throws(function() { + Parsimmon.seqObj(['cool', 'potato']); + }); + }); + +});