From 1f50fcaa2be29ab7a3eb45c89f75fc4e12884dc5 Mon Sep 17 00:00:00 2001 From: Magnus Holm Date: Mon, 27 May 2013 19:47:20 +0200 Subject: [PATCH 1/3] =?UTF-8?q?(new=20api)=20Copying=20Micah's=20parser=20?= =?UTF-8?q?algorithm=20from=20=C2=B5paws.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Magnus Holm did all the work here, I'm merging his 'parser+'-branch work into a safe-stage for Master. Signed-off-by: elliottcable --- Source/Paws.coffee | 8 ++++ Source/parser.coffee | 77 +++++++++++++++++++++++++++++++++ Test/parser.tests.coffee | 92 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 Source/parser.coffee create mode 100644 Test/parser.tests.coffee diff --git a/Source/Paws.coffee b/Source/Paws.coffee index 8afc131..69b655b 100644 --- a/Source/Paws.coffee +++ b/Source/Paws.coffee @@ -13,3 +13,11 @@ module.exports = Paws = Unit: Unit Script: Script + + Label: class Label extends Thing + constructor: (@alien) -> + + Execution: class Execution extends Thing + + Native: class Native extends Execution + constructor: (@position) -> diff --git a/Source/parser.coffee b/Source/parser.coffee new file mode 100644 index 0000000..4458c21 --- /dev/null +++ b/Source/parser.coffee @@ -0,0 +1,77 @@ +`require = require('./cov_require.js')(require)` +Paws = require './Paws.coffee' + +class SourceRange + constructor: (@source, @begin, @end) -> + + slice: -> @source.slice(@begin, @end) + +class Expression + constructor: (@contents, @next) -> + + append: (expr) -> + curr = this + curr = curr.next while curr.next + curr.next = expr + +class Parser + labelCharacters = /[^(){} \n]/ # Not currently supporting quote-delimited labels + + constructor: (@text) -> + @i = 0 + + with_range: (expr, begin, end) -> + if expr.contents?.source_range? + expr.source_range = expr.contents.source_range + else + expr.source_range = new SourceRange(@text, begin, end || @i) + expr.contents.source_range = expr.source_range if expr.contents? + expr + + character: (char) -> + @text[@i] is char && ++@i + + whitespace: -> + true while @character(' ') || @character('\n') + true + + label: -> + @whitespace() + start = @i + res = '' + while @text[@i] && labelCharacters.test(@text[@i]) + res += @text[@i++] + res && @with_range(new Paws.Label(res), start) + + braces: (delim, constructor) -> + start = @i + if @whitespace() && + @character(delim[0]) && + (it = @expr()) && + @whitespace() && + @character(delim[1]) + @with_range(new constructor(it), start) + + paren: -> @braces('()', (it) -> it) + scope: -> @braces('{}', Paws.Native) + + expr: -> + start = @i + substart = @i + res = new Expression + while sub = (@label() || @paren() || @scope()) + res.append(@with_range(new Expression(sub), substart)) + substart = @i + @with_range(res, start) + + parse: -> + @expr() + +module.exports = + parse: (text) -> + parser = new Parser(text) + parser.parse() + + Expression: Expression + SourceRange: SourceRange + diff --git a/Test/parser.tests.coffee b/Test/parser.tests.coffee new file mode 100644 index 0000000..c55df01 --- /dev/null +++ b/Test/parser.tests.coffee @@ -0,0 +1,92 @@ +`require = require('../Source/cov_require.js')(require)` +expect = require 'expect.js' +Paws = require '../Source/Paws.coffee' + +describe 'Parser', -> + parser = require "../Source/parser.coffee" + + it 'should be defined', -> + expect(parser).to.be.ok() + expect(parser.parse).to.be.a('function') + expect(parser.Expression).to.be.ok() + + it 'should parse nothing', -> + expr = parser.parse('') + expect(expr).to.be.ok() + expect(expr).to.be.a(parser.Expression) + expect(expr.contents).to.be(undefined) + expect(expr.next).to.be(undefined) + + it 'should parse a label expression', -> + expr = parser.parse('hello').next + expect(expr.contents).to.be.a(Paws.Label) + expect(expr.contents.alien.toString()).to.be('hello') + + it 'should parse multiple labels', -> + expr = parser.parse('hello world').next + expect(expr.contents).to.be.a(Paws.Label) + expect(expr.contents.alien.toString()).to.be('hello') + expect(expr.next.contents).to.be.a(Paws.Label) + expect(expr.next.contents.alien.toString()).to.be('world') + + it 'should parse subexpressions', -> + expr = parser.parse('(hello) (world)').next + expect(expr.contents).to.be.a(parser.Expression) + expect(expr.contents.next.contents).to.be.a(Paws.Label) + expect(expr.next.contents).to.be.a(parser.Expression) + expect(expr.next.contents.next.contents).to.be.a(Paws.Label) + + it 'should parse Execution', -> + expr = parser.parse('{hello world}').next + expect(expr.contents).to.be.a(Paws.Native) + + it 'should keep track of locations', -> + expr = parser.parse('hello world') + expect(expr.source_range).to.be.a(parser.SourceRange) + expect(expr.source_range.begin).to.be(0) + expect(expr.source_range.end).to.be(11) + + hello = expr.next + expect(hello.source_range).to.be.a(parser.SourceRange) + expect(hello.source_range.begin).to.be(0) + expect(hello.source_range.end).to.be(5) + + hello_label = hello.contents + expect(hello_label.source_range).to.be.a(parser.SourceRange) + expect(hello_label.source_range.begin).to.be(0) + expect(hello_label.source_range.end).to.be(5) + + world = expr.next.next + expect(world.source_range).to.be.a(parser.SourceRange) + expect(world.source_range.begin).to.be(6) + expect(world.source_range.end).to.be(11) + + world_label = world.contents + expect(world_label.source_range).to.be.a(parser.SourceRange) + expect(world_label.source_range.begin).to.be(6) + expect(world_label.source_range.end).to.be(11) + + it 'should keep track of tricky locations', -> + expr = parser.parse(' h( a{b } )') + + contains_same = (expr) -> + expect(expr.source_range.slice()).to.be(expr.contents.source_range.slice()) + + hello = expr.next + contains_same(hello) + expect(hello.source_range.slice()).to.be('h') + + list = hello.next + contains_same(list) + expect(list.source_range.slice()).to.be('( a{b } )') + + a = list.contents.next + contains_same(a) + expect(a.source_range.slice()).to.be('a') + + exe = a.next + contains_same(exe) + expect(exe.source_range.slice()).to.be('{b }') + + expect(exe.contents.position.source_range.slice()).to.be('b ') + From 9a98ac9d8c8d2d67fe7f8fcd81349323b9df5458 Mon Sep 17 00:00:00 2001 From: elliottcable Date: Mon, 27 May 2013 16:41:19 -0400 Subject: [PATCH 2/3] (- fix parse) Make the parser properly handle extra whitespace Squashes: 58817, a6ecc --- Source/parser.coffee | 10 ++++++---- Test/parser.tests.coffee | 11 ++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Source/parser.coffee b/Source/parser.coffee index 4458c21..9bf8cc0 100644 --- a/Source/parser.coffee +++ b/Source/parser.coffee @@ -36,7 +36,6 @@ class Parser true label: -> - @whitespace() start = @i res = '' while @text[@i] && labelCharacters.test(@text[@i]) @@ -45,8 +44,7 @@ class Parser braces: (delim, constructor) -> start = @i - if @whitespace() && - @character(delim[0]) && + if @character(delim[0]) && (it = @expr()) && @whitespace() && @character(delim[1]) @@ -56,13 +54,17 @@ class Parser scope: -> @braces('{}', Paws.Native) expr: -> + @whitespace() start = @i + end = @i substart = @i res = new Expression while sub = (@label() || @paren() || @scope()) res.append(@with_range(new Expression(sub), substart)) + end = @i + @whitespace() substart = @i - @with_range(res, start) + @with_range(res, start, end) parse: -> @expr() diff --git a/Test/parser.tests.coffee b/Test/parser.tests.coffee index c55df01..348f2f4 100644 --- a/Test/parser.tests.coffee +++ b/Test/parser.tests.coffee @@ -16,6 +16,15 @@ describe 'Parser', -> expect(expr).to.be.a(parser.Expression) expect(expr.contents).to.be(undefined) expect(expr.next).to.be(undefined) + + it 'should ignore leading/trailing whitespace', -> + expr = parser.parse ' ' + range = expr.source_range + expect(range.end - range.begin).to.be 0 # because there's nothing *in* it + + expr = parser.parse ' abc ' + range = expr.source_range + expect(range.end - range.begin).to.be 3 # because the *code*'s length is 3 characters it 'should parse a label expression', -> expr = parser.parse('hello').next @@ -88,5 +97,5 @@ describe 'Parser', -> contains_same(exe) expect(exe.source_range.slice()).to.be('{b }') - expect(exe.contents.position.source_range.slice()).to.be('b ') + expect(exe.contents.position.source_range.slice()).to.be('b') From 803feef086f516afe2f11b5266862a14d15dd709 Mon Sep 17 00:00:00 2001 From: Magnus Holm Date: Tue, 28 May 2013 10:55:56 +0200 Subject: [PATCH 3/3] (- new parse doc) Add some inline doc for the parsing algo Squashes: 90603 --- Source/parser.coffee | 48 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/Source/parser.coffee b/Source/parser.coffee index 9bf8cc0..41ae9c2 100644 --- a/Source/parser.coffee +++ b/Source/parser.coffee @@ -14,60 +14,86 @@ class Expression curr = curr.next while curr.next curr.next = expr +# A simple recursive descent parser with no backtracking. No lexing is needed here. class Parser labelCharacters = /[^(){} \n]/ # Not currently supporting quote-delimited labels constructor: (@text) -> + # Keep track of the current position into the text @i = 0 + # Accept a single character. If the given +char+ is at the + # current position, proceed and return true. + accept: (char) -> + @text[@i] is char && ++@i + + expect: (char) -> + # TODO: This should raise an exception + @accept(char) + + # Swallow all whitespace + whitespace: -> + true while @accept(' ') || @accept('\n') + true + + # Sets a SourceRange on a expression with_range: (expr, begin, end) -> + # Copy the source range of the contents if possible if expr.contents?.source_range? expr.source_range = expr.contents.source_range else expr.source_range = new SourceRange(@text, begin, end || @i) + # Copy the source range to the contents if possible expr.contents.source_range = expr.source_range if expr.contents? expr - character: (char) -> - @text[@i] is char && ++@i - - whitespace: -> - true while @character(' ') || @character('\n') - true - + # Parses a single label label: -> start = @i res = '' while @text[@i] && labelCharacters.test(@text[@i]) - res += @text[@i++] + res += @text[@i] + @i++ res && @with_range(new Paws.Label(res), start) + # Parses an expression delimited by some characters braces: (delim, constructor) -> start = @i - if @character(delim[0]) && + if @accept(delim[0]) && (it = @expr()) && @whitespace() && - @character(delim[1]) + @expect(delim[1]) @with_range(new constructor(it), start) + # Subexpression paren: -> @braces('()', (it) -> it) + # Execution scope: -> @braces('{}', Paws.Native) + # Parses an expression expr: -> + # Strip leading whitespace @whitespace() + # The whole expression starts at this position start = @i + # and ends here end = @i + # The subexpression starts here substart = @i + res = new Expression while sub = (@label() || @paren() || @scope()) res.append(@with_range(new Expression(sub), substart)) + # Expand the expression range (exclude trailing whitespace) end = @i @whitespace() + # Set the position of the next expression (exclude leading whitespace) substart = @i + @with_range(res, start, end) parse: -> - @expr() + @expr() module.exports = parse: (text) ->