«
do-nothing
» and leave a modified environment
behind. We can think of big-step statement evaluation as a process
that always turns a statement and an initial environment into a final
environment, avoiding the small-step complication of also having to
deal with the intermediate statement generated by
#reduce
. Big-step evaluation of an
assignment statement, for example, should fully evaluate its
expression and return an updated environment containing the resulting
value:
class
Assign
def
evaluate
(
environment
)
environment
.
merge
({
name
=>
expression
.
evaluate
(
environment
)
})
end
end
Similarly,
DoNothing#evaluate
will clearly return the unmodified environment, and
If#evaluate
has a pretty straightforward job
on its hands: evaluate the condition, then return the environment that
results from evaluating either the consequence or the
alternative.
class
DoNothing
def
evaluate
(
environment
)
environment
end
end
class
If
def
evaluate
(
environment
)
case
condition
.
evaluate
(
environment
)
when
Boolean
.
new
(
true
)
consequence
.
evaluate
(
environment
)
when
Boolean
.
new
(
false
)
alternative
.
evaluate
(
environment
)
end
end
end
The two interesting cases aresequence statements and «
while
» loops. For
sequences, we just need to evaluate both statements, but the initial environment needs to
be “threaded through” these two evaluations, so that the result of evaluating the first
statement becomes the environment in which the second statement is evaluated. This can be
written in Ruby by using the first evaluation’s result as the argument to the
second:
class
Sequence
def
evaluate
(
environment
)
second
.
evaluate
(
first
.
evaluate
(
environment
))
end
end
This threading of the environment is vital to allow earlier
statements to prepare variables for later ones:
>>
statement
=
Sequence
.
new
(
Assign
.
new
(
:x
,
Add
.
new
(
Number
.
new
(
1
),
Number
.
new
(
1
))),
Assign
.
new
(
:y
,
Add
.
new
(
Variable
.
new
(
:x
),
Number
.
new
(
3
)))
)
=> «x = 1 + 1; y = x + 3»
>>
statement
.
evaluate
({})
=> {:x=>«2», :y=>«5»}
For «
while
» statements, we need to think through
the stages of completely evaluating a loop:
Evaluate the condition to get either «
true
» or
«
false
».
If the condition evaluates to «
true
», evaluate
the body to get a new environment, then repeat the loop within that new environment
(i.e., evaluate the whole «
while
» statement again)
and return the resulting environment.
If the condition evaluates to «
false
», return
the environment unchanged.
This is a recursive explanation of how a «
while
»
statement should behave. As with sequence statements, it’s important that the updated
environment generated by the loop body is used for the next iteration; otherwise, the
condition will never stop being «
true
», and the loop
will never get a chance to terminate. [ 15 ]
Once we know how big-step «
while
» semantics should behave, we can
implement
While#evaluate
:
class
While
def
evaluate
(
environment
)
case
condition
.
evaluate
(
environment
)
when
Boolean
.
new
(
true
)
evaluate
(
body
.
evaluate
(
environment
))
when
Boolean
.
new
(
false
)
environment
end
end
end
This is where the looping happens:
body.evaluate(environment)
evaluates the
loop body to get a new environment, then we pass that environment back into the current method to kick off the
next iteration. This means we might stack up many nested
invocations of
While#evaluate
until thecondition eventually becomes «
false
» and the final environment is
returned.
Warning
As with any recursive code, there’s a risk that the Ruby call stack will overflow if
the nested invocations become too deep. Some Ruby implementations have experimental
support for
tail call optimization
, a technique that reduces the
risk of overflow by reusing the same stack frame when possible. In the official Ruby
implementation (MRI) we can enable tail call
Judith Robbins Rose
Glorious Dawn
Daniel Smith
Donna Hill
Isabella Rae
William Kienzle
MAGGIE SHAYNE
Franklin W. Dixon
Roxie Noir
Elissa Brent Weissman