Command-line Arguments
Interpreters, Iteration 18
This iteration required a change to the Hobbes front end. Make sure you have the latest.
This iteration required a change to CIAT. This has been updated on our lab machines; you'll have to update your gems on your own machines.
User Story
Programs that just compute what's hard coded in the program aren't particularly interested or useful. It'd be nice if you could run a Hobbes program from the command-line and provide it with some input.
User Story #18: Command-line arguments
Command-line arguments can be accessed using an
argv
array. argv[0]
is the first
command-line argument, argv[1]
the second, etc. If an
argument can be parsed as an integer, it should be; otherwise treat
it as a string.
Example: argv[0] + argv[1]
. When run with
command-line arguments 3
and 5
, the
interpreter should return 8
. When run with arguments
foo
and 99
, it should return the string
foo99
.
The syntax for command-line arguments isn't an accident. They're supposed to look like array references. This way when you get to implementing arrays in all of their glory, you won't have to change any of acceptance tests; hopefully, it also reduces the changes that will need to happen internally.
So what will this change? The driver certainly changes since the
command-line arguments go through it. (And keep in mind that "the
driver" really refers to two things: the script/hobbes
shell script and the HobbesCLI
Java class.) It'll also
require a change to the interpreter to handle the array, but this
might not be as tricky as you might think.
Acceptance Tests
Write five CIAT tests for command-line arguments.
Some variations to consider:
- Integers and strings, in different combinations.
- One, two, three, and more command-line arguments.
- Ignore some of the provided command-line arguments (e.g., refer
to
argv[1]
but notargv[0]
). - Use a comparison operator (since it returns a very different result than what you can use on the command line).
- Use the arguments "out of order" with a non-commutative
operator (e.g.,
argv[1] < argv[0]
).
Of course, this begs the question: how does one provide
command-line arguments in a test? Add a command line
element to the file:
adding a string and an integer from the command line ==== source argv[0] + argv[1] ==== command line foo 99 ==== execution foo99
You cannot specify multiple command lines in a CIAT file. If you want to try the same program with different inputs, you have to write multiple CIAT files.
Acceptance tests fail.
To the Interpreter!
If your unimplemented visitX()
methods just return
null
, it's much harder to pinpoint problems.
Have them throw a new IllegalArgumentException
; then
you'll know which method to implement next!
The acceptance tests should fail in the
HobbesInterpreter
, in the
visitVariableETIR()
method. Let's start testing it
with a really simple test:
control.replay(); assertSame(null, myInterpreter.interpret(new VariableETIR(lvalue))); control.verify();
control
is an EasyMock control as before.
lvalue
is a mock object of type LValueTIR
(created from control
). The crappy-but-good-for-now
assertion says that the visitVariableETIR()
method
should return null
.
Add a shouldInterpreterLValue()
method to
HobbesInterpreterTest
with the mock-object assertion
from above. Define the necessary variables. Red bar.
Fix the visitVariableETIR()
method. Green bar.
Replace null
with a new mock-object variable
result
of type ExpressionTIR
. Red
bar.
So the question now is, what should be done with that
lvalue
? Just like the left and right subexpressions of
an operator expression, this l-value expression needs to be
evaluated (i.e., interpreted). However, your interpreter cannot
interpret l-value expressions yet. Unlike the left
and right subexpressions of an operator expression, an l-value
expression is a different kind of expression: an l-value
expression!
This means that l-value expressions have their own hierarchy and
visitor. The quick-and-dirty solution now is to have
HobbesInterpreter
do double duty: interpret normal
expressions (as an ExpressionTIRVisitor
) and interpret
l-value expressions (as an LValueTIRVisitor
).
Let's get the code to force this, though. In terms of expectations, this is how you would state it in EasyMock:
EasyMock.expect(lvalue.accept(myInterpreter)).andReturn(result);
You expect that the lvalue
will be interpreted my
the interpreter under test, and that will return the result.
Add this code to shouldInterpreterLValue()
before
the replay. Compiler error!
The compiler should complain that myInterpreter
is
not an acceptable argument (pardon the pun).
Make HobbesInterpreter
implement
LValueTIRVisitor<ExpressionTIR>
(in
addition to any existing inheritance). Implement the
missing methods (with "not implement" exceptions). Compiler should
be happy. Red bar.
You'll get a complaint about the return value. The solution: use
variableExpression.getLValue().accept(this)
to get a
value that can be returned.
Fix visitVariableETIR()
. Green bar!
Victory is short lived since you've only deferred the computation.
Acceptance tests fail.
To the L-Value Interpreter!
Look at why the acceptance tests are failing. It's the
visitSubscriptLValue()
method that you just added. A
"subscript l-value" consists of another l-value expression and a
subscript. argv[0]
is parsed into a "subscript
l-value" with the argv
as the recursive l-value
expression and 0
as the subscript.
Based on the story for this iteration, you can assume a lot of things:
- The recursive l-value must be
argv
. - The subscript (i.e., the index) must be an integer.
The first assumption means that you can safely ignore the l-value subexpression. The second assumption means that you can use a cast to bypass some evaluation. This makes evaluating a subscript l-value a base-case operation, like evaluating an integer or a string. And base cases are tested with real objects, not mock objects.
Create a shouldInterpretSubscriptExpression()
method.
Here's an assertion to get you started:
assertEquals( new StringETIR("first command line argument"), new SubscriptLValueTIR(null, new IntegerETIR(0)).accept(myInterpreter) );
Add this assertion. Red bar.
The null
is there just to boldly declare that the
computation won't try to do anything with it. (You could replace it
with new SimpleLValueTIR("argv")
if you want to be
more honest.) You can't use interpret(ExpressionTIR)
because you're dealing with an l-value expression, so you need to
use the accept(LValueTIRVisitor<T>)
method
provided by the CITkit library.
The expected value is rather bold. Where do I think this value is going to come from? Hard code it for now!
Have visitSubscriptLValue()
return that expected
value. Green bar!
Whenever testing without mocks, you have to write more than one assertion.
Write two more assertions for
shouldInterpretSubscriptExpression()
. This means first
planning out the command-line arguments for testing. Red bar.
The assertion above establishes what the first command-line
argument must be. At least one of the assertions should
expect an IntegerETIR
. At least one of the assertions
should use an index greater than 3 (but keep in relatively small
since you will have to hard code some data—5 seems like a
good number to me).
And now you get to the main star of this iteration: the actual command-line arguments! Well, more accurately, you're going to fake them in the test for now.
Command-line arguments are another thing for the interpreter to
remember (similar to the map of operator algorithms). So they have
to be passed in as an argument to the
HobbesInterpreter
constructor.
Please name the parameter and instance variable appropriately.
Add an ExpressionTIR[]
argument to the
HobbesInterpreter
constructor. Use that parameter to
initialize a new instance variable.
This should give you two compiler errors.
The first place is in the command-line driver. For now, just
give it an empty array (i.e., new
ExpressionTIR[0]
).
The second place is in the setup method
HobbesInterpreterTest
. You can create an array like
this:
new ExpressionTIR[] { new StringETIR("first command line argument"), new IntegerETIR(42) }
That array has only two elements of course. Yours should probably have at least six.
Use a hard-coded array of command-line arguments for the
interpreter under test in HobbesInterpreterTest
. Base
it off the assertions in
shouldInterpretSubscriptExpression()
. Red bar.
It's red because you haven't generalized
visitSubscriptLValue()
.
Use the command-line-arguments array in
visitSubscriptLValue()
. Use the index from the
subscript l-value (with getIndex()
); you'll have to
cast it to IIntegerETIR
and getValue()
.
Green bar.
Command-Line Arguments as TIRs
Ah, but those acceptance tests still complain!
Acceptance tests fail.
The complaint now should be an array index out of bounds. All of
the tests that refer to command-line arguments are trying to get
values out of an empty array since that's what you put into the
main()
method of the driver.
You want to use the arguments passed in args
to
main(String[] args)
to build an array of
ExpressionTIR
s. It turns out do be relatively easy to
do given the constraints of the story: turn into an integer if
possible, otherwise a string. The IntegerETIR
constructor handles the "turn into an integer if possible", and a
caught NumberFormatException
handles the
"otherwise".
This comes from the Hobbes front end that I provide to you. The
source code for the classes in that library are included in the JAR
file. In Eclipse, use ctrl-click (cmd-click on Mac) on the
CommandLineArguments
in the code to navigate to its
code.
But I did this work for you:
new CommandLineArguments(args).parse()
This will turn the arguments in args
except for the
first one (which is the name of the Hobbes source file) into
ExpressionTIR
s.
Use this expression instead of the empty array of
ExpressionTIR
s. Green bars all around!
Changing the Real Driver
The driver you can run from the command line is actually in
scripts/hobbes
. Presently it won't work with
command-line arguments.
Write a command-line argument Hobbes program in
hobbes
(maybe one that uses <
).
Try running it:
unix-% ./scripts/hobbes hobbes/less_than.hob 5 10
You'll get an error about an index out of bounds.
In script/hobbes
, change the $1
in the
last line to "$@"
.
$@
is the shell-script variable for "all
command-line variables". Putting it in double quotes will quote the
values individually, preserving whitespace you might put in
originally. So you can do things like this:
unix-% ./scripts/hobbes hobbes/string_comparison.hob foo xyz abc true unix-% ./scripts/hobbes hobbes/string_comparison.hob 'foo xyz' abc false
Use Hobbes programs like argv[0]
and
argv[1]
to figure out how the quotes affect
things.