In this post we will have a look at the interpreter part of
my project.Before interpreting the source code, following things happened:
• Lexer built the tokens from source code.
• AST (Abstract Syntax Tree) was created.
• Parser traversed the AST and populated our Symbol Table
Interpreter should once again traverse the AST and execute the statements.
private Object exec(CommonTree statement) {
switch (statement.getType()) {
case ProgramWalker.BLOCK:
case ProgramWalker.MAINMETHOD:
block(statement);
break;
case ProgramWalker.PRINT:
print(statement);
sendSourceLineMessage(statement);
break;
case ProgramWalker.VARDECL:
currentSpace.put(statement.getChild(1).getText(), null);
sendSourceLineMessage(statement);
case ProgramWalker.METHOD:
break;
case ProgramWalker.INT:
return Integer.parseInt(statement.getText());
//...
When an AST node is a block of statements, then the following method is called:
private void block(CommonTree statement) {
List stats = statement.getChildren();
if (stats == null)
return;
for (int i = 0; i < stats.size(); ++i)
exec((CommonTree) stats.get(i));
}
These methods call each other until all root and leaf nodes are executed.
We could use the Symbol Table to set variable values and retreive them. But consider the situation where many methods call each other:
main() {
int a; a = 0;
foo(a);
print a;
}
int foo(int valFoo) {
int a; a = 10;
print a;
bar(666);
return 0;
}
int bar(int valBar) {
int a; a = 2;
print valBar;
print a;
return 0;
}
For any method call, scope of local variables are determined by the point of execution at runtime. A method can call itself recursively and on each call, we need to keep track of a new scope. This cannot be handled by Symbol Table. We need to keep track of “function spaces” and have a stack structure for them.
For the above code, the thread enters to the global space first. (main method)
Then it enters to the space of function “foo” and “bar”.
private Object exec(CommonTree statement) {
switch (statement.getType()) {
case ProgramWalker.ID:
MemorySpace memorySpace = getSpaceWithSymbol(statement.getText());
if (memorySpace == null) memorySpace = currentSpace;
return memorySpace.get(statement.getText());
//...
}
public MemorySpace getSpaceWithSymbol(String id) {
if (runtimeStack.size() > 0 && runtimeStack.peek().exists(id)) {
return runtimeStack.peek();
}
if (globals.exists(id)) return globals;
return null;
}
We clearly see the use of a stack of memory spaces which is sometimes called a “runtime stack”.
I use print statements to implement integration tests. Below is a test from
j-minus:
@Test
public void shouldExecuteComplexProgram() throws Exception {
String sourceCode = "main() { " +
"int a; a = 5; " +
"while (a < 8) {" +
"show(a);" +
"a = a + 1;" +
"}" +
"}\n " +
"int show(int val) \n" +
"{ print val; val = val + 100; return 0;}";
ProgramWalker symbolTablePopulator = getProgramWalkerFor(sourceCode);
symbolTablePopulator.prog();
interpreter.setRoot(Util.getASTRoot(sourceCode));
interpreter.setSymTabStack(SymbolTablePopulator.instance().getSymTabStack());
interpreter.interpret();
verify(printStream, times(1)).println(5);
verify(printStream, times(1)).println(6);
verify(printStream, times(1)).println(7);
}
I should better read the source code from a text file. I ll do this soon.
Comments