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