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:
- ‘load’ a program,
- ‘list’ the loaded file,
- ‘run’ it,
- ‘quit’ the interpreter,
- ‘debug’ the program. Once debugging, the user should be able to set breakpoints, display the active breakpoints, display a variable value or all the variables, remove breakpoints and step the program.
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.