This commit is contained in:
2018-07-24 15:12:07 +02:00
commit 1afbd21973
7 changed files with 293 additions and 0 deletions

5
README.md Normal file
View File

@@ -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.

17
main.rb Normal file
View File

@@ -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

22
rlox/error.rb Normal file
View File

@@ -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

11
rlox/executor.rb Normal file
View File

@@ -0,0 +1,11 @@
require './rlox/scan'
class Executor
def run(source)
scan = Scanner.new source
scan.scan.each{ | token |
puts token
}
end
end

7
rlox/file.rb Normal file
View File

@@ -0,0 +1,7 @@
require './rlox/executor'
def file(path)
exec = Executor.new
read = File.read(path)
exec.run read
end

23
rlox/prompt.rb Normal file
View File

@@ -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

208
rlox/scan.rb Normal file
View File

@@ -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