Files
rlox/rlox/scan.rb
2018-07-27 17:50:56 +02:00

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