llip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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