llip 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|