llip 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/History.txt +4 -0
  2. data/MIT-LICENSE +21 -0
  3. data/Manifest.txt +45 -0
  4. data/README.txt +148 -0
  5. data/Rakefile +66 -0
  6. data/examples/ariteval/ariteval.rb +132 -0
  7. data/examples/ariteval/evaluator.rb +61 -0
  8. data/examples/ariteval/exp.rb +104 -0
  9. data/lib/llip.rb +6 -0
  10. data/lib/llip/abstract_parser.rb +170 -0
  11. data/lib/llip/abstract_scanner.rb +83 -0
  12. data/lib/llip/buffer.rb +35 -0
  13. data/lib/llip/llip_error.rb +43 -0
  14. data/lib/llip/parser.rb +93 -0
  15. data/lib/llip/production_compiler.rb +168 -0
  16. data/lib/llip/production_specification.rb +79 -0
  17. data/lib/llip/recursive_production_compiler.rb +35 -0
  18. data/lib/llip/regexp_abstract_scanner.rb +116 -0
  19. data/lib/llip/regexp_parser.rb +197 -0
  20. data/lib/llip/regexp_scanner.rb +33 -0
  21. data/lib/llip/regexp_specification.rb +210 -0
  22. data/lib/llip/token.rb +47 -0
  23. data/lib/llip/visitable.rb +37 -0
  24. data/spec/ariteval/ariteval_spec.rb +111 -0
  25. data/spec/ariteval/evaluator_spec.rb +106 -0
  26. data/spec/ariteval/exp_spec.rb +232 -0
  27. data/spec/llip/abstract_parser_spec.rb +273 -0
  28. data/spec/llip/abstract_scanner_spec.rb +152 -0
  29. data/spec/llip/buffer_spec.rb +60 -0
  30. data/spec/llip/llip_error_spec.rb +77 -0
  31. data/spec/llip/parser_spec.rb +163 -0
  32. data/spec/llip/production_compiler_spec.rb +271 -0
  33. data/spec/llip/production_specification_spec.rb +75 -0
  34. data/spec/llip/recursive_production_compiler_spec.rb +86 -0
  35. data/spec/llip/regexp_abstract_scanner_spec.rb +320 -0
  36. data/spec/llip/regexp_parser_spec.rb +265 -0
  37. data/spec/llip/regexp_scanner_spec.rb +40 -0
  38. data/spec/llip/regexp_specification_spec.rb +734 -0
  39. data/spec/llip/token_spec.rb +70 -0
  40. data/spec/llip/visitable_spec.rb +38 -0
  41. data/spec/spec_helper.rb +10 -0
  42. metadata +110 -0
@@ -0,0 +1,33 @@
1
+ require File.dirname(__FILE__) + '/regexp_abstract_scanner'
2
+ require File.dirname(__FILE__) + '/regexp_specification'
3
+
4
+ module LLIP
5
+ # It's a scanner for the parser RegexpParser. It has two kind of token: :char and :symbol.
6
+ # * char: every character.
7
+ # * symbol: . * + ( ) \ |
8
+ class RegexpScanner < RegexpAbstractScanner
9
+
10
+ # It represents the regular expression '.'
11
+ CHAR = LLIP::RegexpSpecification.new(:char)
12
+
13
+ CHAR.add_state
14
+ CHAR.init.error = CHAR.add_state(:final => true)
15
+
16
+ add_regexp(CHAR)
17
+
18
+ # It represents the regular expression '(.|*|+|\(|\)|\\|\|)' so it matches the chars: . * + ( ) \ |
19
+ SYMBOL = LLIP::RegexpSpecification.new(:symbol)
20
+
21
+ SYMBOL.add_state
22
+ final = SYMBOL.add_state(:final => true)
23
+ SYMBOL.init['.'] = final
24
+ SYMBOL.init['*'] = final
25
+ SYMBOL.init['+'] = final
26
+ SYMBOL.init['('] = final
27
+ SYMBOL.init[')'] = final
28
+ SYMBOL.init['\\'] = final
29
+ SYMBOL.init['|'] = final
30
+
31
+ add_regexp(SYMBOL)
32
+ end
33
+ end
@@ -0,0 +1,210 @@
1
+ require 'forwardable'
2
+
3
+ module LLIP
4
+ # This class represents a specification for a Regexp as an Hash of State because of the equivalence between finite state machines and regular expressions.
5
+ class RegexpSpecification
6
+
7
+ extend Forwardable
8
+
9
+ def_delegators :@init, :[], :[]=, :keys, :values, :each, :error, :error=, :final?, :final=, :regexp, :regexp=
10
+
11
+ # The name of the RegexpSpecification. Its default is nil
12
+ attr_accessor :name
13
+
14
+ # The first value inserted in the RegexpSpecification
15
+ attr_reader :init
16
+
17
+ # It's an hash containing all the states that compose this RegexpSpecification
18
+ attr_reader :states
19
+
20
+ # The +name+ is stored in the attribute name.
21
+ def initialize(name=nil)
22
+ @states = Hash.new { |hash,key| raise "Unknown RegexpSpecification::State #{key}" }
23
+ @name = name.to_sym if name
24
+ end
25
+
26
+ # :call-seq:
27
+ # add_state(RegexpSpecification::State) => RegexpSpecification::State
28
+ # add_state({}) => RegexpSpecification::State
29
+ # add_state => RegexpSpecification::State
30
+ #
31
+ # Adds a State to the RegexpSpecification with the name as a key.
32
+ # If an hash is passed, it will create a State with that hash as a parameter.
33
+ # If nothing is passed, an empty Hash is taken as the default.
34
+ def add_state(arg={})
35
+ unless arg.kind_of? State or arg.kind_of? RegexpSpecification
36
+ arg = State.new(arg)
37
+ end
38
+ @init ||= arg
39
+ arg.regexp = self
40
+ @states[arg.name]=arg
41
+ end
42
+
43
+ # Returns :everything if the init State has an error which is not a State. Returns the init State keys otherwise.
44
+ def starting_chars
45
+ if self.init
46
+ if self.init.error.kind_of? State
47
+ :everything
48
+ else
49
+ self.init.keys
50
+ end
51
+ else
52
+ []
53
+ end
54
+ end
55
+
56
+ # Calls init.last
57
+ #
58
+ # See State#last
59
+ def last
60
+ return [] unless @init
61
+ @init.last
62
+ end
63
+
64
+ public
65
+ class State < Hash
66
+
67
+ # It's a Numeric and it globally identifies a State.
68
+ attr_reader :name
69
+
70
+ # see State#final?
71
+ attr_writer :final
72
+
73
+ # The RegexpSpecification of this state
74
+ attr_accessor :regexp
75
+
76
+ @@next_name = 0
77
+
78
+ # The defaults are:
79
+ # * :final => false
80
+ # * :error => :error
81
+ #
82
+ # If :error is set to :self, the error code it's set to the name of the State, i.e. state[:unknown_key] == state.name => true. This is used to have a everything-like behaviour.
83
+ def initialize(hash = {})
84
+ @name = (@@next_name += 1)
85
+
86
+ if hash[:error] == :self
87
+ super self
88
+ elsif hash[:error].nil?
89
+ super :error
90
+ else
91
+ hash[:error] = hash[:error].to_sym if hash[:error].respond_to? :to_sym
92
+ super hash[:error]
93
+ end
94
+
95
+ @final = hash[:final] || false
96
+
97
+ self
98
+ end
99
+
100
+ # :call-seq:
101
+ # final? => true
102
+ # final? => false
103
+ #
104
+ # It identifies if a State is final or not.
105
+ def final?
106
+ @final
107
+ end
108
+
109
+ # As a State is globally identified by it's name so it's valid to use it as the hash code.
110
+ def hash
111
+ @name.hash
112
+ end
113
+
114
+ alias :error :default
115
+ alias :error= :default=
116
+
117
+ def ==(other)
118
+ if other.respond_to? :error
119
+ return false unless other.error === error
120
+ end
121
+ super
122
+ end
123
+
124
+ # Return an Array which contains all the last states reachable starting from this state, those which must be marked as final.
125
+ #
126
+ # It internally calls RegexpSpecification.last_accessor
127
+ def last
128
+ RegexpSpecification.last_accessor(self).uniq
129
+ end
130
+
131
+ end
132
+
133
+ # :call-seq:
134
+ # RegexpSpecification.last_accessor(RegexpSpecification::State) => Array
135
+ #
136
+ # Returns an Array which contains all the last states reachable starting from _state_. The states in the array may be duplicated.
137
+ def self.last_accessor(state,last=[],examined={},prev=nil)
138
+
139
+ future_states = state.values.uniq
140
+
141
+ future_states << state.error if ( state.error.kind_of? RegexpSpecification::State or state.error.kind_of? RegexpSpecification ) and state.error != state
142
+
143
+ unless examined.has_key? state
144
+ examined[state] = {}
145
+ examined[state][prev] = true
146
+
147
+ if future_states.size == 0
148
+ last << state
149
+ else
150
+ future_states.each do |s|
151
+ last_accessor(s,last,examined,state)
152
+ end
153
+ end
154
+ else
155
+ future_state_unvisited = future_states.select { |s| not examined[s] }
156
+ last << state if future_state_unvisited.size == 0 and ( examined[state][prev] or examined[state][state] or prev == state )
157
+ examined[state][prev] = true
158
+ end
159
+ last
160
+ end
161
+
162
+ # This method is used by RegexpAbstractScanner to mix two different RegexpSpecification which have starting chars in common.
163
+ # It raises an exception if the two regexp have some common chars marked as final.
164
+ def self.mix(first,second)
165
+ regexp = self.new("mix between '#{first.name}' and '#{second.name}'")
166
+ mix_accessor(first.init,second.init,regexp,regexp.add_state)
167
+ regexp
168
+ end
169
+
170
+ private
171
+ def self.mix_accessor(first, second, regexp, last,examined={first => last , second => last}) # :nodoc:
172
+ first_keys = first.keys - second.keys
173
+ common_keys = first.keys - first_keys
174
+ second_keys = second.keys - common_keys
175
+
176
+ accessor = lambda do |new_first,new_second,state|
177
+ examined[new_first] ||= state
178
+ examined[new_second] ||= state
179
+
180
+ if new_first.final? or new_second.final?
181
+ raise "It's impossible to mix two regexp with final states in common."
182
+ end
183
+ mix_accessor(new_first,new_second,regexp,state,examined)
184
+ end
185
+
186
+ common_keys.each do |key|
187
+ unless examined.has_key? first[key] and examined.has_key? second[key]
188
+ state = regexp.add_state
189
+ last[key] = state
190
+ accessor.call(first[key],second[key],state)
191
+ else
192
+ last[key] = examined[first[key]] # because examined[first[key]] and examined[second[key]] are the same
193
+ end
194
+ end
195
+
196
+ if first.error.kind_of? State and second.error.kind_of? State
197
+ state = regexp.add_state
198
+ last.error = state
199
+ accessor.call(first.error,second.error,state)
200
+ elsif first.error.kind_of? State
201
+ last.error = first.error
202
+ elsif second.error.kind_of? State
203
+ last.error = second.error
204
+ end
205
+
206
+ first_keys.each { |key| last[key] = first[key] }
207
+ second_keys.each { |key| last[key] = second[key] }
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,47 @@
1
+ module LLIP
2
+ class Token
3
+
4
+ # The name of the Regexp that generated this token
5
+ attr_reader :name
6
+
7
+ # The matched String
8
+ attr_reader :value
9
+
10
+ # The line at which this token was matched
11
+ attr_reader :line
12
+
13
+ # The position of the first char in the token
14
+ attr_reader :char
15
+
16
+ alias :to_s :value
17
+ alias :to_str :value
18
+
19
+ def initialize(name=:nil,value=nil,line=-1,char = -1)
20
+ @name = name
21
+ @value = value
22
+ @line = line
23
+ @char = char
24
+ end
25
+
26
+ def nil?
27
+ value.nil?
28
+ end
29
+
30
+ def ==(other)
31
+ if other.respond_to? :name
32
+ other.name == @name
33
+ elsif other.respond_to? :to_str
34
+ @value == other.to_str
35
+ elsif other.respond_to? :to_sym
36
+ return true if other == :everything
37
+ other.to_sym == @name
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ def =~(regexp)
44
+ @value =~ regexp
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+
2
+ module LLIP
3
+
4
+ # It makes a class visitable like it's defined in the Visitor pattern, using the
5
+ # double dispatch tecnique.
6
+ #
7
+ # It adds the accept method so for every instance of a class, ie TempClass, including it,
8
+ # it's possible to call instance.accept(visitor) and the visitor will receive
9
+ # a :visit_temp_class message.
10
+ #
11
+ # It passes this ability to its subclasses, so if the subclass is TempClassChild the visitor
12
+ # method which will be called is :visit_temp_class_child.
13
+ module Visitable
14
+ def self.included(other)
15
+ add_accept(other)
16
+ other.extend(ClassMethods)
17
+ end
18
+
19
+ # It adds the accept method following the visitor pattern and the double dispatch tecnique.
20
+ def self.add_accept(klass)
21
+ name = klass.name.gsub(/[A-Z]+/) { |s| " " + s.downcase}.strip.gsub(" ","_")
22
+ klass.class_eval <<-CODE
23
+ def accept(visitor)
24
+ visitor.visit_#{name}(self)
25
+ end
26
+ CODE
27
+ end
28
+
29
+ module ClassMethods
30
+ def inherited(other)
31
+ Visitable.add_accept(other)
32
+ other.extend(Visitable::ClassMethods)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,111 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'ariteval'
3
+
4
+ describe "An Ariteval should evaluate" do
5
+
6
+ before(:each) do
7
+ @ariteval = Ariteval.new
8
+ end
9
+
10
+ it "a single complex expression" do
11
+ expression = "3 * (4 - 2) + 5*(4/2)/(3-2)"
12
+ @ariteval.evaluate(expression).should == eval(expression)
13
+ end
14
+
15
+ it "two different expressions" do
16
+ exp1 = "5 - 32"
17
+ exp2 = "3*(2-4)"
18
+
19
+ @ariteval.evaluate(exp1).should == eval(exp1)
20
+ @ariteval.evaluate(exp2).should == eval(exp2)
21
+ end
22
+ end
23
+
24
+ describe "An Ariteval should be able to parse" do
25
+
26
+ before(:each) do
27
+ @parser = Ariteval.new
28
+ @parser.should respond_to(:parse)
29
+ @parser.scanner.should be_kind_of(Buffer)
30
+ end
31
+
32
+ it "'1 + 1 + 1'" do
33
+ result = @parser.parse('1+ 1 + 1')
34
+
35
+ result.to_s.should == "( ( 1 + 1 ) + 1 )"
36
+ end
37
+
38
+ it "'5 - 1 - 2'" do
39
+ result = @parser.parse('5 - 1 - 2')
40
+
41
+ result.to_s.should == "( ( 5 - 1 ) - 2 )"
42
+ end
43
+
44
+ it "'1 + 5 * 3'" do
45
+ @parser.parse('1 + 5 * 3').to_s.should == "( 1 + ( 5 * 3 ) )"
46
+ end
47
+
48
+ it "'5 * 3 / 2'" do
49
+ @parser.parse('5 * 3 / 2').to_s.should == "( ( 5 * 3 ) / 2 )"
50
+ end
51
+
52
+ it "'5 * ( 4 + 1 )'" do
53
+ @parser.parse('5 * ( 4 + 1 )').to_s.should == "( 5 * ( 4 + 1 ) )"
54
+ end
55
+
56
+ it "'( 3 - 4 ) * ( 3 + 2 )'" do
57
+ @parser.parse('( 3 - 4 ) * ( 3 + 2 )').to_s.should == '( ( 3 - 4 ) * ( 3 + 2 ) )'
58
+ end
59
+
60
+ it "twice" do
61
+ result = @parser.parse("1+1+1")
62
+
63
+ result.to_s.should == "( ( 1 + 1 ) + 1 )"
64
+
65
+ result = @parser.parse("5-1-2")
66
+
67
+ result.to_s.should == "( ( 5 - 1 ) - 2 )"
68
+ end
69
+
70
+ it "a = 5" do
71
+ Ariteval.parser.productions[:factor].tokens[[:ident,:assign]].should_not be_nil
72
+ @parser.scanner.should respond_to(:lookahead)
73
+ @parser.parse("a = 5").to_s.should == "( a = 5 )"
74
+ end
75
+
76
+ it "a + 5" do
77
+ @parser.parse("a + 5").to_s.should == "( a + 5 )"
78
+ end
79
+
80
+ it "'( a = 3 * 2 ) - ( 24 + a )'" do
81
+ @parser.parse('( a = 3 * 2 ) - ( 24 + a )').to_s.should == "( ( a = ( 3 * 2 ) ) - ( 24 + a ) )"
82
+ end
83
+ end
84
+
85
+ describe "An Ariteval shouldn't be able to parse" do
86
+
87
+ before(:each) do
88
+ @parser = Ariteval.new
89
+ end
90
+
91
+ it "'1 + +'" do
92
+ lambda { @parser.parse('1 + +') }.should raise_error(ParserError)
93
+ end
94
+
95
+ it "'- 1'" do
96
+ lambda { @parser.parse('- 1') }.should raise_error(ParserError)
97
+ end
98
+
99
+ it "'* 2'" do
100
+ lambda { @parser.parse('* 2') }.should raise_error(ParserError)
101
+ end
102
+
103
+ it "'2 /'" do
104
+ lambda { @parser.parse('2 /') }.should raise_error(ParserError)
105
+ end
106
+
107
+ it "'2 * ( 1 + 3'" do
108
+ lambda { @parser.parse('2 * ( 1 + 3') }.should raise_error(ParserError)
109
+ end
110
+
111
+ end
@@ -0,0 +1,106 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'evaluator'
3
+ require 'buffer'
4
+
5
+
6
+ describe 'An Evaluator' do
7
+
8
+ before(:each) do
9
+ @eval = Evaluator.new
10
+ end
11
+
12
+ it "should be able to visit all the elements" do
13
+ @eval.should respond_to(:visit_num_exp)
14
+ @eval.should respond_to(:visit_plus_exp)
15
+ @eval.should respond_to(:visit_minus_exp)
16
+ @eval.should respond_to(:visit_mul_exp)
17
+ @eval.should respond_to(:visit_div_exp)
18
+ end
19
+
20
+ it "should return the computated result" do
21
+ @eval.should respond_to(:result)
22
+ end
23
+ end
24
+
25
+ describe "An Evaluator should be able to eval" do
26
+
27
+ before(:each) do
28
+ @eval = Evaluator.new
29
+ @parser = Ariteval.new
30
+ end
31
+
32
+ it "a NumExp" do
33
+ num = mock "NumExp"
34
+ num.should_receive(:value).and_return(5)
35
+
36
+ @eval.visit_num_exp(num)
37
+ @eval.result.should be_equal(5)
38
+ end
39
+
40
+ it "a PlusExp" do
41
+ @parser.parse("1 + 1").accept(@eval)
42
+ @eval.result.should == 2
43
+ end
44
+
45
+ it "a MinusExp" do
46
+ @parser.parse("5 - 1").accept(@eval)
47
+ @eval.result.should == 4
48
+ end
49
+
50
+ it "a MulExp" do
51
+ @parser.parse("5 * 2").accept(@eval)
52
+ @eval.result.should == 10
53
+ end
54
+
55
+ it "a DivExp" do
56
+ @parser.parse("4 / 2").accept(@eval)
57
+ @eval.result.should == 2
58
+ end
59
+
60
+ it "a complex expression" do
61
+ expression = "3 * (4 - 2) + 5*(4/2)/(3-2)"
62
+
63
+ exp = PlusExp.new(
64
+ MulExp.new(
65
+ NumExp.new(3),
66
+ MinusExp.new(
67
+ NumExp.new(4),
68
+ NumExp.new(2)
69
+ )
70
+ ),
71
+ DivExp.new(
72
+ MulExp.new(
73
+ NumExp.new(5),
74
+ DivExp.new(
75
+ NumExp.new(4),
76
+ NumExp.new(2)
77
+ )
78
+ ),
79
+ DivExp.new(
80
+ NumExp.new(3),
81
+ NumExp.new(2)
82
+ )
83
+ )
84
+ )
85
+
86
+ exp.accept(@eval)
87
+ @eval.result.should == eval(expression)
88
+ end
89
+
90
+ it "should have a ident_table" do
91
+ @eval.should respond_to(:ident_table)
92
+ @eval.ident_table.should == {}
93
+ end
94
+
95
+ it "an AssignIdentExp" do
96
+ @parser.parse("a = 4").accept(@eval)
97
+ @eval.result.should == 4
98
+ @eval.ident_table["a"].should == 4
99
+ end
100
+
101
+ it "an IdentExp" do
102
+ @eval.ident_table["a"] = 5
103
+ @parser.parse("a").accept(@eval)
104
+ @eval.result.should == @eval.ident_table["a"]
105
+ end
106
+ end