From 1afbd2197313a5513183efd20a4f7f3fe6f383a4 Mon Sep 17 00:00:00 2001 From: hellerve Date: Tue, 24 Jul 2018 15:12:07 +0200 Subject: [PATCH] initial --- README.md | 5 ++ main.rb | 17 ++++ rlox/error.rb | 22 +++++ rlox/executor.rb | 11 +++ rlox/file.rb | 7 ++ rlox/prompt.rb | 23 ++++++ rlox/scan.rb | 208 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 293 insertions(+) create mode 100644 README.md create mode 100644 main.rb create mode 100644 rlox/error.rb create mode 100644 rlox/executor.rb create mode 100644 rlox/file.rb create mode 100644 rlox/prompt.rb create mode 100644 rlox/scan.rb diff --git a/README.md b/README.md new file mode 100644 index 0000000..f17193d --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# rlox + +`rlox` is an unpronouncable variant of the tree-walk interpreter laid out in +[Crafting Interpreters](http://craftinginterpreters.com/), written in Ruby, +because I want to learn it. diff --git a/main.rb b/main.rb new file mode 100644 index 0000000..b9d0042 --- /dev/null +++ b/main.rb @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +require './rlox/prompt' +require './rlox/file' + +case ARGV.length + when 0 then prompt + when 1 then + begin + file ARGV[0] + rescue Error => e + STDERR.puts e + end + else + puts "Usage: rlox [script]" + exit 64 +end diff --git a/rlox/error.rb b/rlox/error.rb new file mode 100644 index 0000000..9d0c55d --- /dev/null +++ b/rlox/error.rb @@ -0,0 +1,22 @@ +class LoxError < StandardError + def line_s() + @line > 0 ? "[line #{@line}] " : "" + end + + def to_s() + "#{line_s}Error#{@where}: #{@msg}" + end + + def initialize(line, msg, where="") + @line = line + @where = where + @msg = msg + super(msg) + end +end + +class ParseError < LoxError +end + +class ExecError < LoxError +end diff --git a/rlox/executor.rb b/rlox/executor.rb new file mode 100644 index 0000000..14b4201 --- /dev/null +++ b/rlox/executor.rb @@ -0,0 +1,11 @@ +require './rlox/scan' + +class Executor + def run(source) + scan = Scanner.new source + + scan.scan.each{ | token | + puts token + } + end +end diff --git a/rlox/file.rb b/rlox/file.rb new file mode 100644 index 0000000..47192d5 --- /dev/null +++ b/rlox/file.rb @@ -0,0 +1,7 @@ +require './rlox/executor' + +def file(path) + exec = Executor.new + read = File.read(path) + exec.run read +end diff --git a/rlox/prompt.rb b/rlox/prompt.rb new file mode 100644 index 0000000..6ac3655 --- /dev/null +++ b/rlox/prompt.rb @@ -0,0 +1,23 @@ +require "readline" + +require './rlox/executor' + +def prompt() + exec = Executor.new + while buf = Readline.readline("> ", true) + Readline::HISTORY.pop if /^\s*$/ =~ buf + + begin + if Readline::HISTORY[Readline::HISTORY.length-2] == buf + Readline::HISTORY.pop + end + rescue IndexError + end + + begin + exec.run buf + rescue LoxError => err + STDERR.puts err + end + end +end diff --git a/rlox/scan.rb b/rlox/scan.rb new file mode 100644 index 0000000..f2f02fc --- /dev/null +++ b/rlox/scan.rb @@ -0,0 +1,208 @@ +require './rlox/error' + +Token = Struct.new(:type, :lexeme, :literal, :line) + +KEYWORDS = [ + "and", + "class", + "else", + "false", + "for", + "fn", + "if", + "nil", + "or", + "print", + "return", + "super", + "this", + "true", + "let", + "while", +] + +class Scanner + def initialize(source) + @source = source + @start = 0 + @current = 0 + @line = 1 + end + + def is_at_end() + @current >= @source.length + end + + def advance() + @current += 1 + @source[@current-1] + end + + def add_token(type, literal=nil) + Token.new(type, @source[@start, @current], literal, @line) + end + + def match(expected) + if is_at_end or @source[@current] != expected + return false + end + + @current += 1 + true + end + + def peek() + if is_at_end + '\0' + else + @source[@current] + end + end + + def peek_next() + if @current + 1 >= @source.length + '\0' + else + @source[@current+1] + end + end + + def string() + while peek != '"' and not is_at_end + if peek == '\n' + @line += 1 + end + advance + end + + if is_at_end + raise ParseError.new(@line, "Unterminated string.") + end + + advance + + val = @source[@start+1, @current-1] + add_token(:string, val) + end + + def is_digit(c) + c >= '0' and c <= '9' + end + + def number() + while is_digit(peek) + advance + end + + if peek == '.' and is_digit(peek_next) + advance + + while is_digit(peek) + advance + end + end + + add_token(:number, Float(@source[@start, @current])) + end + + def is_alpha(c) + (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or c == '_' + end + + def is_alnum(c) + is_alpha(c) or is_digit(c) + end + + def identifier() + while is_alnum peek + advance + end + + text = @source[@start, @current] + type = :id + + if KEYWORDS.include?(text) + type = text.to_sym + end + + add_token(type) + end + + def token() + c = advance + + case c + when '(' then add_token(:left_paren) + when ')' then add_token(:right_paren) + when '{' then add_token(:left_brace) + when '}' then add_token(:right_brace) + when ',' then add_token(:comma) + when '.' then add_token(:dot) + when '-' then add_token(:minus) + when '+' then add_token(:plus) + when ';' then add_token(:semicolon) + when '*' then add_token(:star) + when '!' then add_token(match('=') ? :bang_eq : :bang) + when '=' then add_token(match('=') ? :eq_eq : :eq) + when '<' then add_token(match('=') ? :leq : :lt) + when '>' then add_token(match('=') ? :geq : :gt) + when '/' then + if match('/') + while peek != '\n' and not is_at_end + advance + end + elsif match('*') + while peek != '*' and peek_next != '/' and not is_at_end + advance + end + if is_at_end + raise ParseError.new(@line, "Unterminated block comment.") + end + advance + advance + advance + nil + else + add_token(:slash) + end + when ' ', '\r', '\t' then nil + when '\n' then + @line += 1 + nil + when '"' then string + else + if is_digit(c) + number + elsif is_alpha(c) + identifier + else + raise ParseError.new(@line, "Unexpected character: #{c}.") + end + end + end + + def scan() + had_error = false + tokens = [] + while !is_at_end + @start = @current + begin + t = token + if t + tokens.push(t) + end + rescue ParseError => e + STDERR.puts e + had_error = true + end + end + + if had_error + raise ParseError.new(0, "Too many errors, aborting.") + end + + tokens.push(Token.new(:eof, "", nil, @line)) + + tokens + end +end