diff --git a/interpreter.rb b/interpreter.rb new file mode 100644 index 0000000..ecf5945 --- /dev/null +++ b/interpreter.rb @@ -0,0 +1,227 @@ +# An interpreter as described by Jack Crenshaw in his famous book +# "Let's Build a Compiler". At least in the beginning, this code will +# closely reflect the Pascal code written by Jack. Over time it may +# become more idiomatic, however this is an academic exercise. +# +# sjs +# may 2009 + +class ParseError < StandardError; end + +class Interpreter + def initialize(input=STDIN) + @look = '' # next lookahead char + @input = input # stream to read from + @vars = Hash.new(0) + + # seed the lexer + get_char + end + + def run + statement until eof? || @look == '.' + end + + # Read the next character from the input stream + def get_char + @look = if @input.eof? + nil + else + @input.readbyte.chr + end + end + + # Report error and halt + def abort(msg) + raise ParseError, msg + end + + # Report what was expected + def expected(what) + msg = if eof? + "Premature end of file, expected: #{what}." + else + "Expected: #{what}, got: #{@look}." + end + abort(msg) + end + + + # Define a variable. + def define(name, value=nil) + abort("already defined '#{name}'") if @vars.has_key?(name) + set(name, value) + end + + # Set the value of a variable. + def set(name, value) + @vars[name] = value + end + + # Retrieve the value of a variable. + def get(name) + abort("undefined variable '#{name}'") unless @vars.has_key?(name) + @vars[name] + end + + + # Recognize an alphabetical character. + def alpha?(char) + ('A'..'Z') === char.upcase + end + + # Recognize a decimal digit. + def digit?(char) + ('0'..'9') === char + end + + # Recognize an alphanumeric character. + def alnum?(char) + alpha?(char) || digit?(char) + end + + def whitespace?(char) + char == ' ' || char == '\t' + end + + + # Match a specific input character. + def match(char) + expected("'#{char}'") unless @look == char + get_char + skip_whitespace + end + + # Parse zero or more consecutive characters for which the test is + # true. + def many(test) + token = '' + while test.call(@look) + token << @look + get_char + end + skip_whitespace + token + end + + # Get an identifier. + def get_name + expected('identifier') unless alpha?(@look) + many(method(:alpha?)) + end + + # Get a number. + def get_num + expected('integer') unless digit?(@look) + many(method(:digit?)).to_i + end + + # Skip all leading whitespace. + def skip_whitespace + get_char while whitespace?(@look) + end + + + + # Parse and evaluate a factor. + def factor + if @look == '(' + match('(') + value = expression + match(')') + elsif alpha?(@look) + value = get(get_name) + else + value = get_num + end + value + end + + # Parse and evaluate a term. + def term + value = factor + while mulop? + case @look + when '*' + match('*') + value *= factor + when '/' + match('/') + value /= factor + end + end + value + end + + # Parse and evaluate a mathematical expression. + def expression + # Fake unary plus and minus by prepending a 0 to them. + value = if addop? then 0 else term end + while addop? + case @look + when '+' + match('+') + value += term + when '-' + match('-') + value -= term + end + end + value + end + + # Parse one or more newlines. + def newline + if @look == "\n" || @look == "\r" + get_char while @look == "\n" || @look == "\r" + else + expected('newline') + end + end + + # Parse and evaluate an assignment statement. + def assignment + name = get_name + match('=') + set(name, expression) + end + + # Parse and evaluate any kind of statement. + def statement + value = case @look + when '?': input + when '!': output + else + assignment + end + newline + value + end + + # Read and store a number from standard input. + def input + match('?') + set(get_name, gets.to_i) + end + + # Print a value to standard output. + def output + match('!') + puts(get(get_name)) + end + + +private + + def eof? + @input.eof? && @look.nil? + end + + def addop? + @look == '+' || @look == '-' + end + + def mulop? + @look == '*' || @look == '/' + end +end diff --git a/testi.code b/testi.code new file mode 100644 index 0000000..d1bd085 --- /dev/null +++ b/testi.code @@ -0,0 +1 @@ +(5*(3 - 5)*2 - 33 - 9/3 - 8/2 + 4*(5 + 5 + 5)) + (4*5 - 21/2 - 15 + 5) diff --git a/testi.rb b/testi.rb new file mode 100644 index 0000000..dbd2dc6 --- /dev/null +++ b/testi.rb @@ -0,0 +1,25 @@ +require 'interpreter' +require 'stringio' + +def error(msg) STDERR.puts(msg) end + +def eval(input) + interpreter = Interpreter.new(input) + interpreter.run + +rescue ParseError => e + error("[error] #{e.message}") + error("Aborting!") + exit(1) +end + +def main(arg) + input = if File.readable?(arg) + File.open(arg) + else + STDIN + end + puts(eval(input)) +end + +main(ARGV[0].to_s)