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.
- data/History.txt +4 -0
- data/MIT-LICENSE +21 -0
- data/Manifest.txt +45 -0
- data/README.txt +148 -0
- data/Rakefile +66 -0
- data/examples/ariteval/ariteval.rb +132 -0
- data/examples/ariteval/evaluator.rb +61 -0
- data/examples/ariteval/exp.rb +104 -0
- data/lib/llip.rb +6 -0
- data/lib/llip/abstract_parser.rb +170 -0
- data/lib/llip/abstract_scanner.rb +83 -0
- data/lib/llip/buffer.rb +35 -0
- data/lib/llip/llip_error.rb +43 -0
- data/lib/llip/parser.rb +93 -0
- data/lib/llip/production_compiler.rb +168 -0
- data/lib/llip/production_specification.rb +79 -0
- data/lib/llip/recursive_production_compiler.rb +35 -0
- data/lib/llip/regexp_abstract_scanner.rb +116 -0
- data/lib/llip/regexp_parser.rb +197 -0
- data/lib/llip/regexp_scanner.rb +33 -0
- data/lib/llip/regexp_specification.rb +210 -0
- data/lib/llip/token.rb +47 -0
- data/lib/llip/visitable.rb +37 -0
- data/spec/ariteval/ariteval_spec.rb +111 -0
- data/spec/ariteval/evaluator_spec.rb +106 -0
- data/spec/ariteval/exp_spec.rb +232 -0
- data/spec/llip/abstract_parser_spec.rb +273 -0
- data/spec/llip/abstract_scanner_spec.rb +152 -0
- data/spec/llip/buffer_spec.rb +60 -0
- data/spec/llip/llip_error_spec.rb +77 -0
- data/spec/llip/parser_spec.rb +163 -0
- data/spec/llip/production_compiler_spec.rb +271 -0
- data/spec/llip/production_specification_spec.rb +75 -0
- data/spec/llip/recursive_production_compiler_spec.rb +86 -0
- data/spec/llip/regexp_abstract_scanner_spec.rb +320 -0
- data/spec/llip/regexp_parser_spec.rb +265 -0
- data/spec/llip/regexp_scanner_spec.rb +40 -0
- data/spec/llip/regexp_specification_spec.rb +734 -0
- data/spec/llip/token_spec.rb +70 -0
- data/spec/llip/visitable_spec.rb +38 -0
- data/spec/spec_helper.rb +10 -0
- 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
|
data/lib/llip/token.rb
ADDED
@@ -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
|