dhaka 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dhaka.rb CHANGED
@@ -1,44 +1,45 @@
1
- #--
2
- # Copyright (c) 2006 Mushfeq Khan
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining
5
- # a copy of this software and associated documentation files (the
6
- # "Software"), to deal in the Software without restriction, including
7
- # without limitation the rights to use, copy, modify, merge, publish,
8
- # distribute, sublicense, and/or sell copies of the Software, and to
9
- # permit persons to whom the Software is furnished to do so, subject to
10
- # the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #++
1
+ #--
2
+ # Copyright (c) 2006 Mushfeq Khan
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
23
 
24
- require 'grammar/grammar_symbol'
25
- require 'grammar/production'
26
- require 'grammar/closure_hash'
27
- require 'grammar/grammar'
24
+ require File.dirname(__FILE__)+'/grammar/grammar_symbol'
25
+ require File.dirname(__FILE__)+'/grammar/production'
26
+ require File.dirname(__FILE__)+'/grammar/closure_hash'
27
+ require File.dirname(__FILE__)+'/grammar/grammar'
28
+ require File.dirname(__FILE__)+'/grammar/precedence'
28
29
 
29
- require 'parser/parse_result'
30
- require 'parser/item'
31
- require 'parser/channel'
32
- require 'parser/parser_methods'
33
- require 'parser/parse_tree'
34
- require 'parser/parser_state'
35
- require 'parser/token'
36
- require 'parser/action'
37
- require 'parser/parser_run'
38
- require 'parser/parser'
39
- require 'parser/compiled_parser'
30
+ require File.dirname(__FILE__)+'/parser/parse_result'
31
+ require File.dirname(__FILE__)+'/parser/item'
32
+ require File.dirname(__FILE__)+'/parser/channel'
33
+ require File.dirname(__FILE__)+'/parser/parser_methods'
34
+ require File.dirname(__FILE__)+'/parser/parse_tree'
35
+ require File.dirname(__FILE__)+'/parser/parser_state'
36
+ require File.dirname(__FILE__)+'/parser/token'
37
+ require File.dirname(__FILE__)+'/parser/action'
38
+ require File.dirname(__FILE__)+'/parser/parser_run'
39
+ require File.dirname(__FILE__)+'/parser/parser'
40
+ require File.dirname(__FILE__)+'/parser/compiled_parser'
40
41
 
41
- require 'tokenizer/tokenizer'
42
- require 'evaluator/evaluator'
42
+ require File.dirname(__FILE__)+'/tokenizer/tokenizer'
43
+ require File.dirname(__FILE__)+'/evaluator/evaluator'
43
44
 
44
45
 
@@ -11,15 +11,34 @@ module Dhaka
11
11
  @symbol = symbol
12
12
  end
13
13
 
14
- def method_missing(production_name, expansion)
14
+ def method_missing(production_name, expansion, options = {})
15
15
  expansion_symbols = expansion.collect {|name| @grammar.symbols[name]}
16
- production = Production.new(@symbol, expansion_symbols, production_name.to_s)
16
+ if precedence_symbol_name = options[:prec]
17
+ production = Production.new(@symbol, expansion_symbols, production_name.to_s, @grammar.symbol_for_name(precedence_symbol_name).precedence)
18
+ else
19
+ production = Production.new(@symbol, expansion_symbols, production_name.to_s)
20
+ end
17
21
  @symbol.nullable = true if expansion_symbols.empty?
18
22
  @grammar.productions_by_symbol[production.symbol] << production
23
+ raise "Duplicate production named #{production.name}" if @grammar.productions_by_name[production.name]
19
24
  @grammar.productions_by_name[production.name] = production
20
25
  end
21
26
  end
22
27
 
28
+ class PrecedenceBuilder
29
+ def initialize(grammar)
30
+ @grammar = grammar
31
+ @precedence_level = 0
32
+ end
33
+ def method_missing(associativity, symbol_names)
34
+ symbol_names.each do |symbol_name|
35
+ symbol = @grammar.symbols[symbol_name]
36
+ symbol.precedence = Precedence.new(@precedence_level, associativity)
37
+ end
38
+ @precedence_level += 1
39
+ end
40
+ end
41
+
23
42
  class Grammar
24
43
 
25
44
  def self.inherited(grammar)
@@ -83,6 +102,10 @@ module Dhaka
83
102
  return channels, result
84
103
  end
85
104
 
105
+ def self.precedences &blk
106
+ PrecedenceBuilder.new(self).instance_eval(&blk)
107
+ end
108
+
86
109
  def self.first(given_symbol)
87
110
  cached_result = self.__first_cache[given_symbol]
88
111
  return cached_result if cached_result
@@ -2,7 +2,7 @@
2
2
  module Dhaka
3
3
  class GrammarSymbol
4
4
  attr_reader :name
5
- attr_accessor :non_terminal, :nullable
5
+ attr_accessor :non_terminal, :nullable, :precedence, :associativity
6
6
  def initialize(name)
7
7
  @name = name
8
8
  end
@@ -0,0 +1,14 @@
1
+ module Dhaka
2
+ class Precedence
3
+ include Comparable
4
+ attr_reader :precedence_level, :associativity
5
+ def initialize(precedence_level, associativity)
6
+ @precedence_level = precedence_level
7
+ @associativity = associativity
8
+ end
9
+
10
+ def <=> other
11
+ self.precedence_level <=> other.precedence_level
12
+ end
13
+ end
14
+ end
@@ -1,14 +1,28 @@
1
1
  #!/usr/bin/env ruby
2
2
  module Dhaka
3
3
  class Production
4
- attr_reader :symbol, :expansion, :name
5
- def initialize(symbol, expansion, name)
4
+
5
+ attr_reader :symbol, :expansion, :name, :precedence
6
+
7
+ def initialize(symbol, expansion, name, precedence = nil)
6
8
  @symbol = symbol
7
9
  @expansion = expansion
8
10
  @name = name
11
+ if precedence
12
+ @precedence = precedence
13
+ else
14
+ @expansion.reverse_each do |symbol|
15
+ if symbol.terminal
16
+ @precedence = symbol.precedence
17
+ break
18
+ end
19
+ end
20
+ end
9
21
  end
22
+
10
23
  def to_s
11
24
  "#{@name} #{@symbol} ::= #{@expansion.join(' ')}"
12
25
  end
26
+
13
27
  end
14
28
  end
data/lib/parser/parser.rb CHANGED
@@ -1,11 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'set'
3
+ require 'logger'
4
+
3
5
  module Dhaka
4
6
  class Parser
5
7
  include ParserMethods
6
8
  attr_reader :grammar, :start_state
7
9
 
8
- def initialize(grammar)
10
+ def initialize(grammar, logger = nil)
11
+ if logger
12
+ @logger = logger
13
+ else
14
+ @logger = Logger.new(STDOUT)
15
+ @logger.level = Logger::WARN
16
+ end
9
17
  @transitions = Hash.new {|hash, state| hash[state] = {}}
10
18
  @grammar = grammar
11
19
  @channels = []
@@ -14,6 +22,7 @@ module Dhaka
14
22
  @channels += channels.to_a
15
23
  new_state = ParserState.new(self, closure)
16
24
  hash[kernel] = new_state
25
+ @logger.debug("Created #{new_state}.")
17
26
  new_state.transition_items.each do |symbol, items|
18
27
  destination_kernel = ItemSet.new(items.collect{|item| item.next_item})
19
28
  destination_state = hash[destination_kernel]
@@ -31,8 +40,11 @@ module Dhaka
31
40
  start_items = ItemSet.new(start_productions.collect {|production| Item.new(production, 0)})
32
41
  start_items.each {|start_item| start_item.lookaheadset << @grammar.end_symbol}
33
42
  @start_state = @states[start_items]
43
+ @logger.debug("Pumping #{@channels.size} channels...")
34
44
  pump_channels
45
+ @logger.debug("Generating shift actions...")
35
46
  generate_shift_actions
47
+ @logger.debug("Generating reduce actions...")
36
48
  generate_reduce_actions
37
49
  end
38
50
 
@@ -80,10 +92,13 @@ module Dhaka
80
92
 
81
93
  def create_reduction_actions_for_item_and_state item, state
82
94
  item.lookaheadset.each do |lookahead|
83
- existing_action = state.actions[lookahead.name]
84
95
  new_action = ReduceAction.new(item.production)
85
- if existing_action
86
- raise ParserConflictError.new(state, existing_action, new_action)
96
+ if existing_action = state.actions[lookahead.name]
97
+ if ReduceAction === existing_action
98
+ raise ParserReduceReduceConflictError.new(build_conflict_message(state, lookahead, new_action).join("\n"))
99
+ else
100
+ resolve_conflict state, lookahead, new_action
101
+ end
87
102
  else
88
103
  state.actions[lookahead.name] = new_action
89
104
  end
@@ -91,25 +106,64 @@ module Dhaka
91
106
  end
92
107
 
93
108
 
109
+ def resolve_conflict state, lookahead, new_action
110
+ message = build_conflict_message(state, lookahead, new_action)
111
+ shift_precedence = lookahead.precedence
112
+ reduce_precedence = new_action.production.precedence
113
+ if (shift_precedence && reduce_precedence)
114
+ if (shift_precedence > reduce_precedence)
115
+ message << "Resolving with precedence. Choosing shift over reduce."
116
+ elsif (shift_precedence < reduce_precedence)
117
+ message << "Resolving with precedence. Choosing reduce over shift."
118
+ state.actions[lookahead.name] = new_action
119
+ else
120
+ case shift_precedence.associativity
121
+ when :left
122
+ message << "Resolving with left associativity. Choosing reduce over shift."
123
+ state.actions[lookahead.name] = new_action
124
+ when :right
125
+ message << "Resolving with right associativity. Choosing shift over reduce."
126
+ when :nonassoc
127
+ message << "Resolving with non-associativity. Eliminating action."
128
+ state.actions[lookahead.name] = nil
129
+ end
130
+ end
131
+ else
132
+ message << "No precedence rule. Choosing shift over reduce."
133
+ end
134
+ @logger.warn(message.join("\n"))
135
+ end
136
+
137
+ def build_conflict_message state, lookahead, new_action
138
+ message = ["Parser Conflict at State:"] + state.items.values.collect{|it| it.to_s}
139
+ message << "Existing: #{state.actions[lookahead.name]}"
140
+ message << "New: #{new_action}"
141
+ message << "Lookahead: #{lookahead}"
142
+ message
143
+ end
144
+
94
145
  def pump_channels
95
146
  while true
96
- break unless @channels.inject(false) do |pumped, channel|
97
- pumped || channel.pump
147
+ unstable_count = 0
148
+ @channels.each do |channel|
149
+ if channel.pump
150
+ unstable_count += 1
151
+ end
98
152
  end
153
+ break if unstable_count == 0
154
+ @logger.debug("#{unstable_count} unstable channels...")
99
155
  end
100
156
  end
101
157
 
102
158
  end
103
159
 
104
160
 
105
- class ParserConflictError < StandardError
106
- def initialize(state, existing_action, new_action)
107
- @state = state
108
- @existing_action = existing_action
109
- @new_action = new_action
161
+ class ParserReduceReduceConflictError < StandardError
162
+ def initialize(message)
163
+ @message = message
110
164
  end
111
165
  def to_s
112
- "Conflict in state #{@state}\n Existing: #{@existing_action}\n New: #{@new_action}"
166
+ @message
113
167
  end
114
168
  end
115
169
 
data/test/all_tests.rb CHANGED
@@ -8,4 +8,6 @@ require 'compiled_parser_test'
8
8
  require 'evaluator_test'
9
9
  require 'arithmetic_tokenizer_test'
10
10
  require 'malformed_grammar_test'
11
- require 'brackets_test'
11
+ require 'brackets_test'
12
+ require 'arithmetic_precedence_grammar_test'
13
+ require 'arithmetic_precedence_parser_test'
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__)+'/../lib/dhaka'
2
+ require 'arithmetic_precedence_grammar'
3
+
4
+ class ArithmeticPrecedenceEvaluator < Dhaka::Evaluator
5
+
6
+ self.grammar = ArithmeticPrecedenceGrammar
7
+
8
+ define_evaluation_rules do
9
+
10
+ for_subtraction do
11
+ child_nodes[0] - child_nodes[2]
12
+ end
13
+
14
+ for_addition do
15
+ child_nodes[0] + child_nodes[2]
16
+ end
17
+
18
+ for_division do
19
+ child_nodes[0].to_f/child_nodes[2]
20
+ end
21
+
22
+ for_multiplication do
23
+ child_nodes[0] * child_nodes[2]
24
+ end
25
+
26
+ for_literal do
27
+ child_nodes[0].token.value
28
+ end
29
+
30
+ for_parenthetized_expression do
31
+ child_nodes[1]
32
+ end
33
+
34
+ for_negated_expression do
35
+ -child_nodes[1]
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__)+'/../lib/dhaka'
2
+
3
+ class ArithmeticPrecedenceGrammar < Dhaka::Grammar
4
+ precedences do
5
+ left ['+', '-']
6
+ left ['*', '/']
7
+ end
8
+
9
+ for_symbol(Dhaka::START_SYMBOL_NAME) do
10
+ expression ['E']
11
+ end
12
+
13
+ for_symbol('E') do
14
+ addition ['E', '+', 'E']
15
+ subtraction ['E', '-', 'E']
16
+ multiplication ['E', '*', 'E']
17
+ division ['E', '/', 'E']
18
+ literal ['n']
19
+ parenthetized_expression ['(', 'E', ')']
20
+ negated_expression ['-', 'E'], :prec => '*'
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ require "test/unit"
2
+ require 'arithmetic_precedence_grammar'
3
+
4
+ class TestArithmeticPrecedenceGrammar < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @addop = ArithmeticPrecedenceGrammar.symbol_for_name('+')
8
+ @subop = ArithmeticPrecedenceGrammar.symbol_for_name('-')
9
+ @mulop = ArithmeticPrecedenceGrammar.symbol_for_name('*')
10
+ @divop = ArithmeticPrecedenceGrammar.symbol_for_name('/')
11
+ end
12
+
13
+ def test_precedence_levels_and_associativity_of_terminals
14
+ assert_equal(0, @addop.precedence.precedence_level)
15
+ assert_equal(0, @subop.precedence.precedence_level)
16
+ assert_equal(1, @mulop.precedence.precedence_level)
17
+ assert_equal(1, @divop.precedence.precedence_level)
18
+ assert_equal(:left, @addop.precedence.associativity)
19
+ assert_equal(:left, @subop.precedence.associativity)
20
+ assert_equal(:left, @mulop.precedence.associativity)
21
+ assert_equal(:left, @divop.precedence.associativity)
22
+ end
23
+ def test_precedence_of_production
24
+ assert_equal(@addop.precedence, ArithmeticPrecedenceGrammar.production_named("addition").precedence)
25
+ assert_equal(@mulop.precedence, ArithmeticPrecedenceGrammar.production_named("multiplication").precedence)
26
+ assert_equal(@mulop.precedence, ArithmeticPrecedenceGrammar.production_named("negated_expression").precedence)
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ require "test/unit"
2
+ require "arithmetic_precedence_grammar"
3
+ require "arithmetic_tokenizer"
4
+ require "arithmetic_precedence_evaluator"
5
+
6
+ class TestArithmeticPrecedenceParser < Test::Unit::TestCase
7
+
8
+ def test_parses_arithmetic_expressions
9
+ fake_logger = FakeLogger.new
10
+ parser = Dhaka::Parser.new(ArithmeticPrecedenceGrammar, fake_logger)
11
+ assert_equal(20, fake_logger.messages.size)
12
+
13
+ syntax_tree = parser.parse(ArithmeticTokenizer.tokenize("5 * -14/(2*7 - 7) + 2")).syntax_tree
14
+ # File.open('precedence.dot', 'w') {|file| file << syntax_tree.to_dot}
15
+ # File.open('precedence_parser.dot', 'w') {|file| file << parser.to_dot}
16
+ assert_equal(-8, ArithmeticPrecedenceEvaluator.new(syntax_tree).result)
17
+ end
18
+ end
19
+
20
+ class FakeLogger
21
+ attr_reader :messages
22
+ def initialize
23
+ @messages = []
24
+ end
25
+ def debug message
26
+ end
27
+ def warn(message)
28
+ @messages << message
29
+ end
30
+ end
data/test/parser_test.rb CHANGED
@@ -144,12 +144,7 @@ class ParserTest < Test::Unit::TestCase
144
144
 
145
145
  def test_with_a_grammar_that_should_generate_an_RR_conflict
146
146
  grammar = RRConflictGrammar
147
- assert_raise(Dhaka::ParserConflictError) { Dhaka::Parser.new(grammar) }
148
- end
149
-
150
- def test_with_a_grammar_that_should_generate_an_SR_conflict
151
- grammar = SRConflictGrammar
152
- assert_raise(Dhaka::ParserConflictError) { Dhaka::Parser.new(grammar) }
147
+ assert_raise(Dhaka::ParserReduceReduceConflictError) { Dhaka::Parser.new(grammar) }
153
148
  end
154
149
 
155
150
  def set_finder(set1, set2)
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: dhaka
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.0.3
7
- date: 2006-12-04 00:00:00 -05:00
6
+ version: 0.0.4
7
+ date: 2006-12-11 00:00:00 -05:00
8
8
  summary: An LALR1 parser generator written in Ruby
9
9
  require_paths:
10
10
  - lib
@@ -34,6 +34,7 @@ files:
34
34
  - lib/grammar/closure_hash.rb
35
35
  - lib/grammar/grammar.rb
36
36
  - lib/grammar/grammar_symbol.rb
37
+ - lib/grammar/precedence.rb
37
38
  - lib/grammar/production.rb
38
39
  - lib/parser/action.rb
39
40
  - lib/parser/channel.rb
@@ -52,6 +53,10 @@ files:
52
53
  - test/arithmetic_evaluator_test.rb
53
54
  - test/arithmetic_grammar.rb
54
55
  - test/arithmetic_grammar_test.rb
56
+ - test/arithmetic_precedence_evaluator.rb
57
+ - test/arithmetic_precedence_grammar.rb
58
+ - test/arithmetic_precedence_grammar_test.rb
59
+ - test/arithmetic_precedence_parser_test.rb
55
60
  - test/arithmetic_test_methods.rb
56
61
  - test/arithmetic_tokenizer.rb
57
62
  - test/arithmetic_tokenizer_test.rb