232 Interactive Mode BASIC Interpreter

Description

Ellohay Rubyists,

This quiz is courtesy of Jean Lazarou.

This week’s quiz is to extend the BASIC interpreter from quiz #228 (code)┬áso that it can start in interactive mode.

In interactive mode, we should be able to type following commands:

Optionally, breakpoints could be conditional, user could change the variable values, and more.

Have fun!

Summary

This summary was written by Jean Lazarou

While I was writing a solution for the Ruby Basic quiz #228, I was trying to execute a basic program but the program did not produce the expected result. I thought that a debugger would be helpful. I added the feature, and some others, leading to the interactive mode.

To keep the summary for Ruby Basic short, I decided to remove the debugger part and proposed it as a new quiz. Hence, here we are.

The idea was to start from #228’s solution. We received one partial solution and I am going to present my original solution.

A partial solution

Ben Rho submitted a partially complete solution, supporting all the commands with the exception of the debugger.

When the interpreter starts without any script to run as argument, it enters the interactive mode. Ben takes care to make it a bit more user friendly, you can type help to get a help message, help commands to get the list of the commands and help followed by a command to get information about that command.

The code parses the command using regular expressions, we see some code duplication but the work was not ended.

One thing betrays the fact he developed under Windows, the use of `pause >nul` to pause the program before exiting. It fails under Linux. To make it portable one should display a message like Hit the return key and wait for some input.

A complete solution

The quiz proposed to implement several commands using the basic interpreter from quiz #228. With that interpreter the implementation of some commands is rather simple. Therefore, we are only covering load, list and debug.

I changed my initial code. While I was reading it I thought that I’d better separate the interactive mode implementation and the interpreter. Therefore, I changed the protected part into public in the Basic class. I also added a simple help inspired from Ben’s idea.

Here is a simple session.

code$ ruby basic.rb 
HELLO
> load scripts/fact.bas
scripts/fact.bas loaded
> list
10 REM
15 REM COMPUTE FACTORIAL
20 REM
25 LET F = 1
30 READ N
31 LET X = N
35 IF N = 0 THEN 55
40 LET F = F*N
45 LET N = N-1
50 GOTO 35
55 PRINT "FACT", X, "IS", F
60 GOTO 25
65 DATA 1, 3, 5, 6, 20
99 END
> run
FACT	1	IS	1
FACT	3	IS	6
FACT	5	IS	120
FACT	6	IS	720
FACT	20	IS	2432902008176640000
The main loop

The main loop is obvious, it gets input from the keyboard then uses a case statement with regular expressions to execute the command.

01 loop do
02 
03   print '> '
04   
05   command = $stdin.gets
06 
07   case command
08   when /help/
09     puts "Available commands: debug, list, load, quit, run"
10   when /quit/
11     exit
12   
13   ...
14   
15   else
16     puts 'Unknown command'
17   end
18 
19 end
The load command

Load is not complex but we must handle errors.

01   when /load (.+)/
02     
03     statements = nil
04     
05     begin
06       program = File.open($1).readlines.join
07     rescue Exception
08       puts "Cannot load file"
09     else
10       
11       begin
12         statements = BasicParser.new.parse(program)
13       rescue RuntimeError => e
14         puts e
15       else
16         puts "#{$1} loaded"
17       end
18     
19     end

Line 7 catches exceptions that can happen if the file to load does not exist or is not readable. If load succeeded we fall in the else part at line 9 where we parse the file (line 12) and again must handle exceptions (line 13) or display the loaded message (line 16).

The parser returns an array of statements (line 12).

The list command

The list command displays the program loaded with the load command. Once loaded, the program in memory is an array of Statement objects, stored in the statements variable. Every statement implements the to_s method which produces the basic code. Ruby array’s join creates the output with a simple statement.

01   when /list/
02     puts statements.join("\n") if statements
The debug command

The debug command calls another method where a new loop starts reading from the keyboard and uses a different case statement as the set of commands is different. The debugger displays the first statement and is ready to run the program.

The stop command exits the debugger and comes back to the normal mode.

> debug
10 REM
DBG> help
Available commands: break, breakpoints, continue, quit, step, stop, var, vars
10 REM
DBG> ho!
Unknown command
10 REM
DBG> stop
>

Actually the debugger is not starting a keyboard entry loop. It starts a loop that executes the statements which contains the inner loop implementing the console interaction. The inner loop executes only if we are stepping the program or if we hit a break point.

Break points are very easy to manage because each statement has a line number, we store the break points in a hash with the line number as a key. If the current statement’s line is in the break points hash we know we must stop executing the program and wait for user input.

01   stepping = true
02   break_points = {}
03   runtime = @interpreter.create_runtime(statements)
04   
05   while runtime.running?
06     
07     if stepping or break_points[runtime.program_pos]
08         
09       loop do
10       
11         # keyboard input
12         
13       end
14       
15     end
16   
17     statement = statements[runtime.program_pos]
18     
19     statement.execute(runtime)
20     
21     runtime.program_pos += 1
22 
23   end

Line 3 creates a Runtime object, the runtime object keeps the current program position, the running status and gives access to the variables.

Retrieving the variable value is simple:

01   when /var ([A-Z]\d?)/
02     puts "   #{$1} = #{runtime[$1.to_sym]}"

We only accept variable names starting with a letter and optionally followed by a digit (basic names). We just use the symbolified name to retrieve the value from the runtime object.

Conclusion

I wanted to make the interactive mode, and mainly the debugger, part of a quiz because it is not so complex as we might think. Sure, the basic language we implemented is simple but we didn’t spend weeks to develop it.

You can download the solutions here.


Friday, May 07, 2010