From: Daniel Moore Date: 2010-04-20T15:24:37+09:00 Subject: [QUIZ][SUMMARY] Ruby BASIC (#228) This summary was written by Jean Lazarou. We received no solution for this quiz. Therefore we are going to present the one we wrote. The basic language reference we consider is the one from Dartmouth college (back in October 1964). The language is rather simple, it only computes numbers. It does not contain any string handling, except for output printing. The variable naming is limited to a single letters followed by an optional digit. It does not support interactive input. The language Let us start with a short description of the language. For a deeper specification see annex or get the Dartmouth College’s document. Each line of a basic program is a statement. Most statements are instructions to execute. A line must start with a line number. After the line number comes a word denoting the type of the statement. Basic has fifteen statement types: DATA DEF DIM END FOR GOSUB GOTO IF LET NEXT PRINT READ REM RETURN STOP Our implementation does not support the DEF and DIM statements. Source code does not require lines to be ordered. Basic uses only capital letters and spaces are optional. Expression must appear on a single line. The END statement must be the highest numbered statement, including the data statements. A number must contain up to nine digits with an optional minus sign and an optional decimal point. As basic programs have no input statements, a program uses the data statements for data input. A simple program Next program outputs the first ten square roots. 10 LET X = 0 20 LET X = X + 1 30 PRINT X, SQR(X) 40 IF X <= 10 THEN 20 50 END The parser We are going to write a parser and an interpreter. Separating the parsing phase from the interpretation (or the run) makes writing tests easier, we separate the syntaxical aspects from the running aspects. Parsing basic code is not very difficult, the syntax is not complex. We use a regular expression to validate each line. 01 unless line =~ /^(\d+) *(DATA|DEF|DIM|END|FOR|GOSUB|GOTO|IF|LET|NEXT|PRINT|READ|REM|RETURN|STOP) *(.*)$/ 02 raise error_message("INVALID STATEMENT", line_number, line) 03 end The regular expression matches a string starting with digits, followed by optional spaces, one of the valid basic keywords, optional spaces and anyhting. We use regular expression’s groups so that if the line matches our rule the global variables $1 contains the line number, $2 contains the statement and $3 the rest of the line. We discard any space in between. After validating the statement type, we need statement specific validation. We use a hash table that maps the statement type to a method that parses each statements. 01 STATEMENT_PARSERS = { 02 'DATA' => :parse_data, 03 'END' => :parse_end, 04 'FOR' => :parse_for, 05 'GOSUB' => :parse_gosub, 06 'GOTO' => :parse_goto, 07 'IF' => :parse_if, 08 'LET' => :parse_let, 09 'NEXT' => :parse_next, 10 'PRINT' => :parse_print, 11 'READ' => :parse_read, 12 'REM' => :parse_rem, 13 'RETURN' => :parse_return, 14 'STOP' => :parse_stop 15 } Each parse method needs the statement line number and the rest of the line (the part to parse). It also needs the current position in the script and the whole line to be able to generate a valuable error message. 01 statement = self.send(method, line_number, line, $1.to_i, $3) 02 statements << statement The parser sends the message (the parse method) to it self. If the parse method successfully parses the statement it returns a Satetement object. We add the statement object to the statements array. Once the parser consumes all the input program, we need to sort the satements because the basic language does not require the statement to be ordered. 01 statements.sort! The sorting works because the Statement objects provide an implementation of the comparison method (<=>). The comparison method compares the line numbers. Here is the Statement base class. 01 class Statement 02 03 attr_reader :line, :arguments 04 05 def initialize line, type, arguments = nil 06 @line, @type, @arguments = line, type, arguments 07 end 08 09 def <=> other 10 @line - other.line 11 end 12 13 def to_s 14 "#{@line} #{@type} #{@arguments}".rstrip 15 end 16 17 end We are not going to present all the parse methods, as an example we are going to look at parse_goto which is rather simple. A goto statement looks like: 10 GOTO 90. Which means: the effect of line 10 is to jump at line 90. 01 def parse_goto line_number, source, statement_line, arguments 02 03 scanner = BasicScanner.new(arguments) 04 05 scanner.skip_spaces 06 to_line = scanner.scan_line 07 08 raise error_message("MISSING TO LINE", line_number, source) unless to_line 09 10 scanner.skip_spaces 11 raise error_message("INVALID 'GOTO' STATMENT", line_number, source) unless scanner.eos? 12 13 GotoStatement.new(statement_line, to_line.to_i) 14 15 end We use the BasicScanner class which derives from the StringScanner class. The reason for extending StringScanner is to add methods, like skip_spaces, that makes it easier to read the code, not to mention that regular expressions are not easy to read. The arguments parameter is a string containing everything following the basic statement type. We first skip spaces (line 5), then we try retrieving the line number to go to (line 6). If we don’t find a line number, we raise an error (line 8). Otherwise we skip trailing spaces (line 10) and check if we reached the end of the input (line 11). Finally, input was a valid GOTO statement and we return a GotoStatement. Expressions When a statement expects an expression the parse methods use the parse_expression method. parse_expression a number (a floating point value) for literals, a string for operators (+, -, *, /, ^), parentheses and built-in functions a symbol for variables The array of tokens is still an infix expression. The reason is that we can easier implement the to_s method use by the to_s method of a Statement object which produces a valid basic source code. The to_a method returns the array of tokens. Expression Testing the parser Testing the parser is easy, here are some examples. Testing the expression parsing. 01 expr = @parser.parse_expression('INT(X/Y) + 2') 02 assert_equal ['INT', '(', :X, '/', :Y, ')', '+', 2], expr.to_a Testing converting the expression to the postfix version. 01 expr = @parser.parse_expression('SQR(4) + INT(Y/2) * 3') 02 assert_equal [4, 'SQR', :Y, 2, '/', 'INT', 3, '*', '+'], expr.to_postfix Testing that spaces are optional. 01 def test_program_witout_spaces 02 03 program = <ruby basic_test.rb Loaded suite basic_test Started E Finished in 0.000369 seconds. 1) Error: test_print_one_string(TestBasic): NoMethodError: undefined method `run' for nil:NilClass basic_test.rb:14:in `test_print_one_string' 1 tests, 0 assertions, 0 failures, 1 errors Using programs for tests We wrote some basic programs that we can use to validate our interpreter in automated tests. By using the technique presented above and adding some more methods, the tests become even easier to read. Here if the factorial program. 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 Here is the test that runs the factorial program. 01 class TestRunningBasicPrograms < Test::Unit::TestCase 02 03 def test_factorial 04 05 script 'fact.bas' 06 07 expects 'FACT', 1, 'IS', 1 08 expects 'FACT', 3, 'IS', 6 09 expects 'FACT', 5, 'IS', 120 10 expects 'FACT', 6, 'IS', 720 11 expects 'FACT', 20, 'IS', 2432902008176640000 12 13 assert_output_as_expected 14 15 end 16 17 end At line 5 we set the program file under test, from line 7 up to 11 we declare the expectations and the last line (line 13) checks the outcome. Tests using the runtime As the interpreter returns the runtime object, we can use it during the assertion phase. 01 def test_1et 02 03 program = <ruby basic_parser_test.rb Loaded suite basic_parser_test Started ............. Finished in 0.008335 seconds. 13 tests, 56 assertions, 0 failures, 0 errors $>ruby basic_test.rb Loaded suite basic_test Started ............. Finished in 0.020823 seconds. 13 tests, 23 assertions, 0 failures, 0 errors You can download the code here. Annex Here is a BNF description of the language syntax. ::= {\n } ::= | | | | | | | | | | | | | | ::= LET = ::= READ {, } ::= DATA {, } ::= PRINT