You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
208 lines
4.7 KiB
208 lines
4.7 KiB
2 years ago
|
#!/usr/bin/env ruby -w
|
||
|
|
||
2 years ago
|
require "strscan"
|
||
|
|
||
2 years ago
|
module Lox
|
||
2 years ago
|
class Error < StandardError
|
||
|
def initialize(line:, where: "", message:)
|
||
|
@line, @where, @message = line, where, message
|
||
|
end
|
||
|
|
||
|
def to_s
|
||
|
"[line #@line] Error#@where: #@message"
|
||
|
end
|
||
2 years ago
|
end
|
||
|
|
||
|
def self.error(line, msg)
|
||
2 years ago
|
raise Error(line:, message:)
|
||
2 years ago
|
end
|
||
|
|
||
2 years ago
|
class Runner
|
||
2 years ago
|
def initialize(scanner: Scanner.new)
|
||
2 years ago
|
@scanner = scanner
|
||
|
end
|
||
|
|
||
|
def run(src)
|
||
2 years ago
|
@scanner.scan(src).each do |token|
|
||
|
puts token
|
||
|
end
|
||
2 years ago
|
end
|
||
|
end
|
||
|
|
||
|
class Scanner
|
||
2 years ago
|
TOKENS = %w[
|
||
|
( LEFT_PAREN
|
||
|
) RIGHT_PAREN
|
||
|
{ LEFT_BRACE
|
||
|
} RIGHT_BRACE
|
||
|
, COMMA
|
||
|
. DOT
|
||
|
- MINUS
|
||
|
+ PLUS
|
||
|
; SEMICOLON
|
||
|
* STAR
|
||
|
!= BANG_EQUAL
|
||
|
! BANG
|
||
|
== EQUAL_EQUAL
|
||
|
= EQUAL
|
||
|
<= LESS_EQUAL
|
||
|
< LESS
|
||
|
>= GREATER_EQUAL
|
||
|
> GREATER
|
||
|
/ SLASH
|
||
2 years ago
|
].each_slice(2).to_h.transform_values(&:to_sym)
|
||
2 years ago
|
TOKENS_RE = Regexp.union(TOKENS.keys)
|
||
|
|
||
|
KEYWORDS = %w[
|
||
|
and AND
|
||
|
class CLASS
|
||
|
else ELSE
|
||
|
false FALSE
|
||
|
for FOR
|
||
|
fun FUN
|
||
|
if IF
|
||
|
nil NIL
|
||
|
or OR
|
||
|
print PRINT
|
||
|
return RETURN
|
||
|
super SUPER
|
||
|
this THIS
|
||
|
true TRUE
|
||
|
var VAR
|
||
|
while WHILE
|
||
|
].each_slice(2).to_h.transform_values(&:to_sym)
|
||
2 years ago
|
|
||
2 years ago
|
class State < Struct.new(:ss, :tokens, :errors, :line)
|
||
2 years ago
|
def eos? = ss.eos?
|
||
|
def scan(re) = ss.scan(re)
|
||
|
def pos = ss.pos
|
||
|
|
||
2 years ago
|
def initialize(src)
|
||
|
super(StringScanner.new(src), [], [], 1)
|
||
|
end
|
||
|
|
||
2 years ago
|
def add_token(type, text: nil, literal: nil)
|
||
|
text ||= ss.matched
|
||
|
self.tokens << Token.new(type, text, literal, line)
|
||
|
end
|
||
|
end
|
||
|
|
||
2 years ago
|
def scan(src)
|
||
2 years ago
|
state = State.new(src)
|
||
2 years ago
|
|
||
|
until state.eos?
|
||
|
case
|
||
2 years ago
|
when state.scan(/\/\/(?~\n)/)
|
||
|
# ignore line comment
|
||
2 years ago
|
when state.scan(/\/\*/)
|
||
|
scan_block_comment(state)
|
||
2 years ago
|
when matched = state.scan(TOKENS_RE)
|
||
|
state.add_token(TOKENS.fetch(matched))
|
||
2 years ago
|
when state.scan(/[ \r\t]/)
|
||
|
# ignore whitespace
|
||
2 years ago
|
when state.scan(/\n/)
|
||
|
state.line += 1
|
||
2 years ago
|
when state.scan(/"/)
|
||
|
scan_str(state)
|
||
|
when number = state.scan(/\d+(\.\d+)?/)
|
||
|
state.add_token(:NUMBER, literal: number.to_f)
|
||
2 years ago
|
when identifier = state.scan(/[a-zA-Z_]\w*/)
|
||
2 years ago
|
type = KEYWORDS.fetch(identifier, :IDENTIFIER)
|
||
|
state.add_token(type)
|
||
2 years ago
|
else
|
||
2 years ago
|
state.errors << Error.new(line: state.line, message: "Unexpected character.")
|
||
2 years ago
|
state.scan(/./) # keep scanning
|
||
2 years ago
|
end
|
||
|
end
|
||
|
|
||
2 years ago
|
fail unless state.errors.empty?
|
||
|
|
||
2 years ago
|
state.add_token(:EOF, text: "")
|
||
2 years ago
|
state.tokens
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def scan_str(state)
|
||
|
text = ?"
|
||
|
loop do
|
||
|
case
|
||
|
when state.scan(/"/)
|
||
|
text << ?"
|
||
|
state.add_token(:STRING, text:, literal: text[1..-2])
|
||
|
return
|
||
|
when state.scan(/\n/)
|
||
|
text << ?\n
|
||
|
state.line += 1
|
||
|
when state.eos?
|
||
|
state.errors << Error.new(line: state.line, message: "Unterminated string.")
|
||
|
return
|
||
2 years ago
|
when c = state.scan(/(?~"|\n)/)
|
||
2 years ago
|
text << c
|
||
|
else
|
||
|
fail "unreachable!"
|
||
|
end
|
||
|
end
|
||
|
end
|
||
2 years ago
|
|
||
|
def scan_block_comment(state)
|
||
|
loop do
|
||
|
case
|
||
|
when state.scan(/\/\*/)
|
||
|
scan_block_comment(state)
|
||
|
when state.scan(/\*\//)
|
||
|
return
|
||
|
when state.scan(/\n/)
|
||
|
state.line += 1
|
||
|
when state.eos?
|
||
|
state.errors << Error.new(line: state.line, message: "Unterminated block comment.")
|
||
|
return
|
||
2 years ago
|
when state.scan(/./)
|
||
2 years ago
|
# no-op
|
||
|
else
|
||
|
fail "unreachable!"
|
||
|
end
|
||
|
end
|
||
|
end
|
||
2 years ago
|
end
|
||
|
|
||
|
Token = Struct.new(:type, :lexeme, :literal, :line) do
|
||
|
def to_s
|
||
|
"#{type} #{lexeme} #{literal}"
|
||
2 years ago
|
end
|
||
2 years ago
|
end
|
||
2 years ago
|
|
||
|
module Expr
|
||
|
Binary = Struct.new(:left, :op, :right) do
|
||
|
def accept(visitor) = visitor.visit_binary(self)
|
||
|
end
|
||
|
|
||
|
Grouping = Struct.new(:expr) do
|
||
|
def accept(visitor) = visitor.visit_grouping(self)
|
||
|
end
|
||
|
|
||
|
Literal = Struct.new(:value) do
|
||
|
def accept(visitor) = visitor.visit_literal(self)
|
||
|
end
|
||
|
|
||
|
Unary = Struct.new(:op, :right) do
|
||
|
def accept(visitor) = visitor.visit_unary(self)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class AstPrinter
|
||
|
def print(expr) = expr.accept(self)
|
||
|
|
||
|
def visit_binary(expr) = parenthesize(expr.op.lexeme, expr.left, expr.right)
|
||
|
def visit_grouping(expr) = parenthesize("group", expr.expr)
|
||
|
def visit_literal(expr) = expr.value&.to_s || "nil"
|
||
|
def visit_unary(expr) = parenthesize(expr.op.lexeme, expr.right)
|
||
|
|
||
|
private
|
||
|
|
||
|
def parenthesize(name, *exprs)
|
||
|
"(#{name} #{exprs.map {|expr| expr.accept(self) }.join(" ")})"
|
||
|
end
|
||
|
end
|
||
2 years ago
|
end
|