221 lines
3.9 KiB
Ruby
221 lines
3.9 KiB
Ruby
require './rlox/error'
|
|
|
|
Token = Struct.new(:type, :lexeme, :literal, :line)
|
|
|
|
KEYWORDS = [
|
|
"and",
|
|
"class",
|
|
"else",
|
|
"false",
|
|
"for",
|
|
"fn",
|
|
"if",
|
|
"nil",
|
|
"or",
|
|
"return",
|
|
"super",
|
|
"self",
|
|
"true",
|
|
"let",
|
|
"while",
|
|
]
|
|
|
|
class Scanner
|
|
def initialize()
|
|
@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-@start], 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)-(@start+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-@start]))
|
|
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-@start]
|
|
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} (char code: #{c.ord}).")
|
|
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
|
|
|
|
def scan_on(source)
|
|
@start = 0
|
|
@current = 0
|
|
@source = source
|
|
|
|
scan
|
|
end
|
|
|
|
def inc_line()
|
|
@line += 1
|
|
end
|
|
end
|