Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Conflicts: Source/Paws.coffee
- Loading branch information
Showing
3 changed files
with
214 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
`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 | ||
|
||
# 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 | ||
|
||
# Parses a single label | ||
label: -> | ||
start = @i | ||
res = '' | ||
while @text[@i] && labelCharacters.test(@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 @accept(delim[0]) && | ||
(it = @expr()) && | ||
@whitespace() && | ||
@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() | ||
|
||
module.exports = | ||
parse: (text) -> | ||
parser = new Parser(text) | ||
parser.parse() | ||
|
||
Expression: Expression | ||
SourceRange: SourceRange | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
`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 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 | ||
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') | ||
|