Skip to content

Commit

Permalink
Merge branch 'parser' into Master
Browse files Browse the repository at this point in the history
Conflicts:
	Source/Paws.coffee
  • Loading branch information
ELLIOTTCABLE committed Jun 9, 2013
2 parents 9574030 + 803feef commit da07ba0
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Source/Paws.coffee
Expand Up @@ -10,3 +10,11 @@ paws.Script = Script = require './Script.coffee'
paws.Thing = Thing = parameterizable class Thing
constructor: ->
return this

paws.Label = Label = class Label extends Thing
constructor: (@alien) ->

paws.Execution = Execution = class Execution extends Thing

paws.Native = Native = class Native extends Execution
constructor: (@position) ->
105 changes: 105 additions & 0 deletions Source/parser.coffee
@@ -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

101 changes: 101 additions & 0 deletions Test/parser.tests.coffee
@@ -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')

0 comments on commit da07ba0

Please sign in to comment.