Skip to content

Commit

Permalink
Spike 1... not too sure where I'm going.
Browse files Browse the repository at this point in the history
  • Loading branch information
d11wtq committed Nov 17, 2011
0 parents commit c15efc5
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
*.gem
.bundle
Gemfile.lock
pkg/*
1 change: 1 addition & 0 deletions .rspec
@@ -0,0 +1 @@
--colour
4 changes: 4 additions & 0 deletions Gemfile
@@ -0,0 +1,4 @@
source "http://rubygems.org"

# Specify your gem's dependencies in whittle.gemspec
gemspec
1 change: 1 addition & 0 deletions Rakefile
@@ -0,0 +1 @@
require "bundler/gem_tasks"
4 changes: 4 additions & 0 deletions lib/whittle.rb
@@ -0,0 +1,4 @@
require "whittle/version"
require "whittle/rule"
require "whittle/rule_set"
require "whittle/parser"
89 changes: 89 additions & 0 deletions lib/whittle/parser.rb
@@ -0,0 +1,89 @@
module Whittle
class Parser
class << self
def rules
@rules ||= {}
end

def rule(name)
raise ArgumentError, "Parser.rule requires a block, but none was given" unless block_given?

RuleSet.new.tap do |rule_set|
rules[name] = rule_set
yield rule_set
end
end

def start(name = nil)
@start = name unless name.nil?
@start
end
end

def rules
self.class.rules
end

def parse(input)
raise "Undefined start rule #{self.class.start}" unless rules.key?(self.class.start)

rule = rules[self.class.start]
token = nil
lookahead = nil
root = {
:name => self.class.start,
:rule => nil,
:args => []
}
stack = [root]

require 'pp'

lex(input) do |received|
token = lookahead
lookahead = received
next if token.nil?

pp rule.table_for_offset(stack.last[:args].length)

stack.last[:args] << token[:value]
stack.last[:rule] = rules[token[:name]].first
end

reduce(stack.pop)
end

def lex(input)
source = input.dup
line = 1

until source.length == 0 do
next_token(source, line).tap do |token|
raise "Unmatched input #{source.inspect} on line #{line}" if token.nil?

line = token[:line]
yield token unless token[:discarded]
end
end

yield nil
end

private

def next_token(source, line)
rules.each do |name, rule|
if token = rule.scan(source, line)
token[:name] = name
return token
end
end

nil
end

def reduce(tree)
tree[:rule].action.call(*tree[:args].map { |arg| Hash === arg ? reduce(arg) : arg })
end
end
end
46 changes: 46 additions & 0 deletions lib/whittle/rule.rb
@@ -0,0 +1,46 @@
module Whittle
class Rule
attr_reader :action
attr_reader :components

def initialize(*components)
@components = components.map do |c|
case c
when String then Regexp.new("^#{Regexp.escape(c)}")
when Regexp then Regexp.new("^#{c}")
when Symbol then c
else raise ArgumentError, "Unsupported rule component #{c.class}"
end
end

@pattern = @components.first
@lexable = (@components.count == 1 && Regexp === @pattern)
end

def as(&block)
raise ArgumentError, "Rule#as requires a block, but none given" unless block_given?

tap do
@action = block
end
end

def scan(source, line)
return nil unless @lexable

copy = source.dup
if match = copy.slice!(@pattern)
source.replace(copy)
{
:value => match,
:line => line + ("~" + match + "~").lines.count - 1,
:discarded => @action.nil?
}
end
end

def table_for_offset(offset)
[{ :token => @components[offset], :lookahead => @components[offset + 1], :rule => self }]
end
end
end
35 changes: 35 additions & 0 deletions lib/whittle/rule_set.rb
@@ -0,0 +1,35 @@
module Whittle
class RuleSet
include Enumerable

def initialize
@rules = []
end

def each(&block)
@rules.each(&block)
end

def [](*components)
Rule.new(*components).tap do |rule|
@rules << rule
end
end

def scan(source, line)
each do |rule|
if token = rule.scan(source, line)
return token
end
end

nil
end

def table_for_offset(offset)
@rules.inject([]) do |table, rule|
table + rule.table_for_offset(offset)
end
end
end
end
3 changes: 3 additions & 0 deletions lib/whittle/version.rb
@@ -0,0 +1,3 @@
module Whittle
VERSION = "0.0.1"
end
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
@@ -0,0 +1,4 @@
require "whittle"

RSpec.configure do |config|
end
90 changes: 90 additions & 0 deletions spec/unit/parser_spec.rb
@@ -0,0 +1,90 @@
require "spec_helper"

describe Whittle::Parser do
context "given no-op program" do
let(:parser) do
Class.new(Whittle::Parser) do
rule(:char) do |r|
r[/./].as { |chr| chr }
end

rule(:prog) do |r|
r[:prog, :char]
r[:char]
end

start(:prog)
end
end

it "returns nil for all inputs" do
pending "I'm undecided on if this should be allowed"

["a b c", "a (b) > *c"].each do |input|
parser.new.parse(input).should be_nil
end
end
end

context "given a program returning its input" do
let(:parser) do
Class.new(Whittle::Parser) do
rule(:foo) do |r|
r["FOO"].as { |str| str }
end

start(:foo)
end
end

context "for matching input" do
it "returns the input" do
parser.new.parse("FOO").should == "FOO"
end
end
end

context "given a program returning an integer" do
let(:parser) do
Class.new(Whittle::Parser) do
rule(:int) do |r|
r[/[0-9]+/].as { |int| Integer(int) }
end

start(:int)
end
end

context "for matching input" do
it "returns the input as an integer" do
parser.new.parse("123").should == 123
end
end
end

context "given a program returning the sum of two integers" do
let(:parser) do
Class.new(Whittle::Parser) do
rule(:int) do |r|
r[/[0-9]+/].as { |int| Integer(int) }
end

rule(:sum) do |r|
r[:int, "+", :int].as { |a, _, b| a + b }
end

rule(:default) do |r|
r[/./].as { |c| c }
end

start(:sum)
end
end

context "for matching input" do
it "returns the sum of the operands" do
parser.new.parse("10+20").should == 30
end
end
end
end
27 changes: 27 additions & 0 deletions whittle.gemspec
@@ -0,0 +1,27 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "whittle/version"

Gem::Specification.new do |s|
s.name = "whittle"
s.version = Whittle::VERSION
s.authors = ["d11wtq"]
s.email = ["chris@w3style.co.uk"]
s.homepage = "https://github.com/d11wtq/whittle"
s.summary = %q{An efficient, easy to use, LALR parser for Ruby}
s.description = %q{Write powerful parsers by defining a series of very simple rules
and operations to perform as those rules are matched. Whittle
parsers are written in pure ruby and as such are extremely flexible.
Anybody familiar with parsers like yacc should find Whittle intuitive.
Those unfamiliar with parsers shouldn't find it difficult to
understand.}

s.rubyforge_project = "whittle"

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]

s.add_development_dependency "rspec", "~> 2.6"
end

0 comments on commit c15efc5

Please sign in to comment.