initial
This commit is contained in:
5
README.md
Normal file
5
README.md
Normal 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
17
main.rb
Normal 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
22
rlox/error.rb
Normal 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
11
rlox/executor.rb
Normal 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
7
rlox/file.rb
Normal 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
23
rlox/prompt.rb
Normal 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
208
rlox/scan.rb
Normal 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
|
Reference in New Issue
Block a user