loxby 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/gem-push.yml +44 -0
- data/.rubocop.yml +8 -0
- data/Gemfile.lock +47 -0
- data/README.md +13 -0
- data/bin/loxby +8 -0
- data/lib/loxby/core.rb +81 -0
- data/lib/loxby/helpers/ast.rb +94 -0
- data/lib/loxby/helpers/environment.rb +47 -0
- data/lib/loxby/helpers/errors.rb +16 -0
- data/lib/loxby/helpers/token_type.rb +40 -0
- data/lib/loxby/interpreter.rb +153 -0
- data/lib/loxby/parser.rb +293 -0
- data/lib/loxby/runner.rb +29 -0
- data/lib/loxby/scanner.rb +192 -0
- data/lib/loxby/version.rb +5 -0
- data/lib/loxby/visitors/ast_printer.rb +37 -0
- data/lib/loxby/visitors/base.rb +27 -0
- data/lib/loxby/visitors/rpn_converter.rb +25 -0
- data/lib/loxby.rb +5 -0
- data/loxby.gemspec +33 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4c24f85fecb8840b0c2b6331b6b4dda6c5a4899e830d81f52ed6ffa12065b432
|
|
4
|
+
data.tar.gz: 3b694bcad715f645c52501d192944c207932d71909189f0b9f2dd0eae9e3d392
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 14167ca0449fa15e8b5eba780ab63af4d9b702c9b762c8bb3d2724840549481a25ba1806a4d5702e8ed0b925518872a1ec72ec17db0bc7f8170011d1891e9903
|
|
7
|
+
data.tar.gz: 6b8366382f8c51bffdfe71802a0b551aee4938e0880c878012778c8508094544a9f1c02ca1f7dde57dcc1e03919545387b652877dc2ea38266c8422d6ba89b14
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Ruby Gem
|
|
2
|
+
|
|
3
|
+
on: workflow_dispatch
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
name: Build + Publish
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
packages: write
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Set up Ruby 3.1
|
|
16
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
|
17
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
|
18
|
+
uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: 3.1.3
|
|
21
|
+
|
|
22
|
+
- name: Publish to GPR
|
|
23
|
+
run: |
|
|
24
|
+
mkdir -p $HOME/.gem
|
|
25
|
+
touch $HOME/.gem/credentials
|
|
26
|
+
chmod 0600 $HOME/.gem/credentials
|
|
27
|
+
printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
28
|
+
gem build *.gemspec
|
|
29
|
+
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
|
30
|
+
env:
|
|
31
|
+
GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
|
|
32
|
+
OWNER: ${{ github.repository_owner }}
|
|
33
|
+
|
|
34
|
+
- name: Publish to RubyGems
|
|
35
|
+
run: |
|
|
36
|
+
mkdir -p $HOME/.gem
|
|
37
|
+
touch $HOME/.gem/credentials
|
|
38
|
+
chmod 0600 $HOME/.gem/credentials
|
|
39
|
+
chmod +x bin/loxby
|
|
40
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
41
|
+
gem build *.gemspec
|
|
42
|
+
gem push *.gem
|
|
43
|
+
env:
|
|
44
|
+
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
data/.rubocop.yml
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
loxby (0.0.1)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
ast (2.4.2)
|
|
10
|
+
json (2.7.2)
|
|
11
|
+
language_server-protocol (3.17.0.3)
|
|
12
|
+
parallel (1.24.0)
|
|
13
|
+
parser (3.3.2.0)
|
|
14
|
+
ast (~> 2.4.1)
|
|
15
|
+
racc
|
|
16
|
+
racc (1.8.0)
|
|
17
|
+
rainbow (3.1.1)
|
|
18
|
+
regexp_parser (2.9.2)
|
|
19
|
+
rexml (3.2.8)
|
|
20
|
+
strscan (>= 3.0.9)
|
|
21
|
+
rubocop (1.64.1)
|
|
22
|
+
json (~> 2.3)
|
|
23
|
+
language_server-protocol (>= 3.17.0)
|
|
24
|
+
parallel (~> 1.10)
|
|
25
|
+
parser (>= 3.3.0.2)
|
|
26
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
27
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
28
|
+
rexml (>= 3.2.5, < 4.0)
|
|
29
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
|
30
|
+
ruby-progressbar (~> 1.7)
|
|
31
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
|
32
|
+
rubocop-ast (1.31.3)
|
|
33
|
+
parser (>= 3.3.1.0)
|
|
34
|
+
ruby-progressbar (1.13.0)
|
|
35
|
+
strscan (3.1.0)
|
|
36
|
+
unicode-display_width (2.5.0)
|
|
37
|
+
|
|
38
|
+
PLATFORMS
|
|
39
|
+
x64-mingw-ucrt
|
|
40
|
+
|
|
41
|
+
DEPENDENCIES
|
|
42
|
+
loxby!
|
|
43
|
+
rubocop
|
|
44
|
+
strscan
|
|
45
|
+
|
|
46
|
+
BUNDLED WITH
|
|
47
|
+
2.5.10
|
data/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Loxby
|
|
2
|
+
*A Lox interpreter written in Ruby*
|
|
3
|
+
|
|
4
|
+
Loxby is written following the first half of Robert Nystrom's wonderful web-format book [Crafting Interpreters](https://www.craftinginterpreters.com), adapting the Java code to modern Ruby. This project is intended to explore what elegant object-oriented code can look like and accomplish.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
1. `gem install loxby` or `gem 'loxby'`
|
|
8
|
+
2. `loxby [filename]` to run a file or `loxby` to run in REPL mode
|
|
9
|
+
3. To run the interpreter from Ruby:
|
|
10
|
+
```ruby
|
|
11
|
+
require 'loxby/runner'
|
|
12
|
+
Lox::Runner.new(ARGV, $stdout, $stderr)
|
|
13
|
+
```
|
data/bin/loxby
ADDED
data/lib/loxby/core.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'scanner'
|
|
4
|
+
require_relative 'parser'
|
|
5
|
+
require_relative 'interpreter'
|
|
6
|
+
require_relative 'helpers/token_type'
|
|
7
|
+
|
|
8
|
+
# Lox interpreter.
|
|
9
|
+
# Each interpreter keeps track of its own
|
|
10
|
+
# environment, including variable and
|
|
11
|
+
# function scope.
|
|
12
|
+
class Lox
|
|
13
|
+
attr_reader :errored, :interpreter
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@errored = false
|
|
17
|
+
@errored_in_runtime = false
|
|
18
|
+
@interpreter = Interpreter.new(self) # Make static so REPL sessions reuse it
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Run from file
|
|
22
|
+
def run_file(path)
|
|
23
|
+
if File.exist? path
|
|
24
|
+
run File.read(path)
|
|
25
|
+
else
|
|
26
|
+
report(0, '', "No such file: '#{path}'")
|
|
27
|
+
end
|
|
28
|
+
exit(65) if @errored # Don't execute malformed code
|
|
29
|
+
exit(70) if @errored_in_runtime
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Run interactively
|
|
33
|
+
def run_prompt
|
|
34
|
+
loop do
|
|
35
|
+
print '> '
|
|
36
|
+
line = gets
|
|
37
|
+
break unless line # Trap eof (Ctrl+D unix, Ctrl+Z win)
|
|
38
|
+
|
|
39
|
+
result = run(line)
|
|
40
|
+
puts "=> #{@interpreter.lox_obj_to_str result}" unless @errored
|
|
41
|
+
@errored = false # Reset so a mistake doesn't kill the repl
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Run a string
|
|
46
|
+
def run(source)
|
|
47
|
+
tokens = Scanner.new(source, self).scan_tokens
|
|
48
|
+
parser = Parser.new(tokens, self)
|
|
49
|
+
statements = parser.parse
|
|
50
|
+
return if @errored
|
|
51
|
+
|
|
52
|
+
@interpreter.interpret statements
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def error(line, message)
|
|
56
|
+
if line.is_a? Lox::Token
|
|
57
|
+
# Parse/runtime error
|
|
58
|
+
where = line.type == :eof ? 'end' : "'#{line.lexeme}'"
|
|
59
|
+
report(line.line, " at #{where}", message)
|
|
60
|
+
else
|
|
61
|
+
# Scan error
|
|
62
|
+
report(line, '', message)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# rubocop:disable Style/StderrPuts
|
|
67
|
+
|
|
68
|
+
def runtime_error(err)
|
|
69
|
+
$stderr.puts err.message
|
|
70
|
+
$stderr.puts "[line #{err.token.line}]"
|
|
71
|
+
@errored_in_runtime = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def report(line, where, message)
|
|
77
|
+
$stderr.puts "[line #{line}] Error#{where}: #{message}"
|
|
78
|
+
@errored = true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
# rubocop:enable Style/StderrPuts
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../visitors/base'
|
|
4
|
+
|
|
5
|
+
class String # rubocop:disable Style/Documentation
|
|
6
|
+
def to_camel_case
|
|
7
|
+
to_s.split(/[-_]/).map(&:capitalize).join('')
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Symbol # rubocop:disable Style/Documentation
|
|
12
|
+
def to_camel_case
|
|
13
|
+
to_s.to_camel_case.to_sym
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Lox
|
|
18
|
+
# Interface:
|
|
19
|
+
# Lox::AST.define_ast(
|
|
20
|
+
# "ASTBaseClass",
|
|
21
|
+
# {
|
|
22
|
+
# :ast_type => [[:field_one_type, :field_one_name], [:field_two_type, :field_two_name]],
|
|
23
|
+
# :other_ast_type => [[:field_type, :field_name]]
|
|
24
|
+
# }
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# This defines Lox::AST::ASTBaseClass, which ::AstType and ::OtherAstType descend from
|
|
28
|
+
# and are scoped under. It also defines the Visitor pattern: AstType defines #accept(visitor),
|
|
29
|
+
# which calls `visitor.visit_ast_type(self)`
|
|
30
|
+
module AST
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def define_ast(base_name, types)
|
|
34
|
+
base_class = Class.new
|
|
35
|
+
base_class.include Visitable
|
|
36
|
+
# Define boilerplate visitor methods
|
|
37
|
+
Visitor.define_types(base_name, types.keys)
|
|
38
|
+
# Dynamically create subclasses for each AST type
|
|
39
|
+
types.each do |class_name, fields|
|
|
40
|
+
define_type(base_class, base_name, class_name, fields)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
define_class base_name.to_camel_case, base_class
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def define_type(base_class, base_class_name, subtype_name, fields) # rubocop:disable Metrics/MethodLength
|
|
47
|
+
subtype = Class.new(base_class)
|
|
48
|
+
parameters = fields.map { _1[1].to_s }
|
|
49
|
+
|
|
50
|
+
subtype.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
51
|
+
include Visitable # Visitor pattern
|
|
52
|
+
attr_reader #{parameters.map { ":#{_1}" }.join(', ')}
|
|
53
|
+
def initialize(#{parameters.map { "#{_1}:" }.join(', ')})
|
|
54
|
+
#{parameters.map { "@#{_1}" }.join(', ')} = #{parameters.join ', '}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Dynamically generated for visitor pattern.
|
|
58
|
+
# Expects visitor to define #visit_#{subtype_name}
|
|
59
|
+
def accept(visitor)
|
|
60
|
+
visitor.visit_#{subtype_name}_#{base_class_name}(self)
|
|
61
|
+
end
|
|
62
|
+
RUBY
|
|
63
|
+
|
|
64
|
+
define_class(subtype_name.to_camel_case, subtype, base_class:)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def define_class(class_name, klass, base_class: Lox::AST)
|
|
68
|
+
base_class.const_set class_name, klass
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
Lox::AST.define_ast(
|
|
74
|
+
:expression,
|
|
75
|
+
{
|
|
76
|
+
assign: [%i[token name], %i[expr value]],
|
|
77
|
+
binary: [%i[expr left], %i[token operator], %i[expr right]],
|
|
78
|
+
ternary: [%i[expr left], %i[token left_operator], %i[expr center], %i[token right_operator], %i[expr right]],
|
|
79
|
+
grouping: [%i[expr expression]],
|
|
80
|
+
literal: [%i[object value]],
|
|
81
|
+
unary: [%i[token operator], %i[expr right]],
|
|
82
|
+
variable: [%i[token name]]
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
Lox::AST.define_ast(
|
|
87
|
+
:statement,
|
|
88
|
+
{
|
|
89
|
+
block: [%i[stmt_list statements]],
|
|
90
|
+
expression: [%i[expr expression]],
|
|
91
|
+
print: [%i[expr expression]],
|
|
92
|
+
var: [%i[token name], %i[expr initializer]]
|
|
93
|
+
}
|
|
94
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
|
|
5
|
+
class Lox
|
|
6
|
+
# Lox::Environment stores namespace for
|
|
7
|
+
# a Lox interpreter. Environments can be
|
|
8
|
+
# nested (for scope).
|
|
9
|
+
class Environment
|
|
10
|
+
def initialize(enclosing = nil)
|
|
11
|
+
@enclosing = enclosing
|
|
12
|
+
@values = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def undefined_variable(name)
|
|
16
|
+
Lox::RunError.new(name, "Undefined variable '#{name.lexeme}'")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def []=(name, value)
|
|
20
|
+
@values[name.lexeme] = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def exists?(name)
|
|
24
|
+
@values.keys.member? name.lexeme
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def [](name)
|
|
28
|
+
if exists? name
|
|
29
|
+
@values[name.lexeme]
|
|
30
|
+
elsif @enclosing
|
|
31
|
+
@enclosing[name]
|
|
32
|
+
else
|
|
33
|
+
raise undefined_variable(name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def assign(name, value)
|
|
38
|
+
if exists? name
|
|
39
|
+
self[name] = value
|
|
40
|
+
elsif @enclosing
|
|
41
|
+
@enclosing.assign(name, value)
|
|
42
|
+
else
|
|
43
|
+
raise undefined_variable(name)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lox
|
|
4
|
+
class ParseError < RuntimeError; end
|
|
5
|
+
|
|
6
|
+
class RunError < RuntimeError # rubocop:disable Style/Documentation
|
|
7
|
+
attr_reader :token
|
|
8
|
+
|
|
9
|
+
def initialize(token, message)
|
|
10
|
+
super(message)
|
|
11
|
+
@token = token
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class DividedByZeroError < RunError; end
|
|
16
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lox
|
|
4
|
+
class Token
|
|
5
|
+
TOKENS = [
|
|
6
|
+
# Single-character tokens.
|
|
7
|
+
:left_paren, :right_paren, :left_brace, :right_brace,
|
|
8
|
+
:comma, :dot, :minus, :plus, :semicolon, :slash, :star,
|
|
9
|
+
:question, :colon,
|
|
10
|
+
|
|
11
|
+
# 1-2 character tokens.
|
|
12
|
+
:bang, :bang_equal,
|
|
13
|
+
:equal, :equal_equal,
|
|
14
|
+
:greater, :greater_equal,
|
|
15
|
+
:less, :less_equal,
|
|
16
|
+
|
|
17
|
+
# Literals.
|
|
18
|
+
:identifier, :string, :number,
|
|
19
|
+
|
|
20
|
+
# Keywords.
|
|
21
|
+
:and, :class, :else, :false, :fun, :for, :if, :nil, :or,
|
|
22
|
+
:print, :return, :super, :this, :true, :var, :while,
|
|
23
|
+
|
|
24
|
+
:eof
|
|
25
|
+
].freeze
|
|
26
|
+
SINGLE_TOKENS = TOKENS.zip('(){},.-+;/*'.split('')).to_h
|
|
27
|
+
|
|
28
|
+
attr_reader :type, :lexeme, :literal, :line
|
|
29
|
+
|
|
30
|
+
def initialize(type, lexeme, literal, line)
|
|
31
|
+
@type = type
|
|
32
|
+
@lexeme = lexeme
|
|
33
|
+
@literal = literal
|
|
34
|
+
@line = line
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s = "#{type} #{lexeme} #{literal}"
|
|
38
|
+
def inspect = "#<Lox::Token #{self}>"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helpers/environment'
|
|
4
|
+
require_relative 'helpers/errors'
|
|
5
|
+
require_relative 'visitors/base'
|
|
6
|
+
|
|
7
|
+
# Interpreter class. Walks the AST using
|
|
8
|
+
# the Visitor pattern.
|
|
9
|
+
class Interpreter < Visitor
|
|
10
|
+
def initialize(process) # rubocop:disable Lint/MissingSuper
|
|
11
|
+
@process = process
|
|
12
|
+
@environment = Lox::Environment.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def interpret(statements)
|
|
16
|
+
result = nil
|
|
17
|
+
statements.each { result = lox_eval(_1) }
|
|
18
|
+
result
|
|
19
|
+
rescue Lox::RunError => e
|
|
20
|
+
@process.runtime_error e
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def lox_eval(expr)
|
|
24
|
+
expr.accept self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Lox's definition of truthiness follows
|
|
28
|
+
# Ruby's by definition. This does nothing.
|
|
29
|
+
def truthy?(obj)
|
|
30
|
+
obj
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ensure_number(operator, *objs)
|
|
34
|
+
raise Lox::RunError.new(operator, 'Operand must be a number.') unless objs.all? { _1.is_a?(Float) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def lox_obj_to_str(obj)
|
|
38
|
+
case obj
|
|
39
|
+
when nil
|
|
40
|
+
'nil'
|
|
41
|
+
when Float
|
|
42
|
+
obj.to_s[-2..] == '.0' ? obj.to_s[0...-2] : obj.to_s
|
|
43
|
+
else
|
|
44
|
+
obj.to_s
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def visit_expression_statement(statement)
|
|
49
|
+
lox_eval statement.expression
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def visit_print_statement(statement)
|
|
53
|
+
value = lox_eval statement.expression
|
|
54
|
+
puts lox_obj_to_str(value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def visit_var_statement(statement)
|
|
58
|
+
value = statement.initializer ? lox_eval(statement.initializer) : nil
|
|
59
|
+
@environment[statement.name] = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def visit_variable_expression(expr)
|
|
63
|
+
@environment[expr.name]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def visit_assign_expression(expr)
|
|
67
|
+
value = lox_eval expr.value
|
|
68
|
+
@environment.assign expr.name, value
|
|
69
|
+
value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def visit_block_statement(statement)
|
|
73
|
+
execute_block(statement.statements, Lox::Environment.new(@environment))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def execute_block(statements, environment)
|
|
77
|
+
previous = @environment
|
|
78
|
+
@environment = environment
|
|
79
|
+
statements.each { lox_eval _1 }
|
|
80
|
+
ensure
|
|
81
|
+
@environment = previous
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Leaves of the AST. The scanner picks
|
|
85
|
+
# out these values for us beforehand.
|
|
86
|
+
def visit_literal_expression(expr)
|
|
87
|
+
expr.value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def visit_grouping_expression(expr)
|
|
91
|
+
lox_eval expr
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def visit_unary_expression(expr)
|
|
95
|
+
right = lox_eval(expr.right)
|
|
96
|
+
|
|
97
|
+
case expr.operator.type
|
|
98
|
+
when :minus
|
|
99
|
+
ensure_number(expr.operator, right)
|
|
100
|
+
-right.to_f
|
|
101
|
+
when :bang
|
|
102
|
+
truthy? right
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def visit_binary_expression(expr) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/AbcSize
|
|
107
|
+
left = lox_eval expr.left
|
|
108
|
+
right = lox_eval expr.right
|
|
109
|
+
case expr.operator.type
|
|
110
|
+
when :minus
|
|
111
|
+
ensure_number(expr.operator, left, right)
|
|
112
|
+
left.to_f - right.to_f
|
|
113
|
+
when :slash
|
|
114
|
+
raise Lox::DividedByZeroError.new(expr.operator, 'Cannot divide by zero.') if right == 0.0
|
|
115
|
+
|
|
116
|
+
ensure_number(expr.operator, left, right)
|
|
117
|
+
left.to_f / right
|
|
118
|
+
when :star
|
|
119
|
+
ensure_number(expr.operator, left, right)
|
|
120
|
+
left.to_f * right.to_f
|
|
121
|
+
when :plus
|
|
122
|
+
unless (left.is_a?(Float) || left.is_a?(String)) && left.instance_of?(right.class)
|
|
123
|
+
raise Lox::RunError.new(expr.operator, 'Operands must be two numbers or two strings.')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
left + right
|
|
127
|
+
when :greater
|
|
128
|
+
ensure_number(expr.operator, left, right)
|
|
129
|
+
left.to_f > right.to_f
|
|
130
|
+
when :greater_equal
|
|
131
|
+
ensure_number(expr.operator, left, right)
|
|
132
|
+
left.to_f >= right.to_f
|
|
133
|
+
when :less
|
|
134
|
+
ensure_number(expr.operator, left, right)
|
|
135
|
+
left.to_f < right.to_f
|
|
136
|
+
when :less_equal
|
|
137
|
+
ensure_number(expr.operator, left, right)
|
|
138
|
+
left.to_f <= right.to_f
|
|
139
|
+
when :bang_equal
|
|
140
|
+
left != right
|
|
141
|
+
when :equal_equal
|
|
142
|
+
left == right
|
|
143
|
+
when :comma
|
|
144
|
+
right
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def visit_ternary_expression(expr)
|
|
149
|
+
left = lox_eval expr.left
|
|
150
|
+
|
|
151
|
+
left ? lox_eval(expr.center) : lox_eval(expr.right)
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/loxby/parser.rb
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helpers/ast'
|
|
4
|
+
require_relative 'helpers/errors'
|
|
5
|
+
|
|
6
|
+
class Lox
|
|
7
|
+
# Lox::Parser converts a list of tokens
|
|
8
|
+
# from Lox::Scanner to a syntax tree.
|
|
9
|
+
# This tree can be interacted with using
|
|
10
|
+
# the Visitor pattern. (See the Visitor
|
|
11
|
+
# class in visitors/base.rb.)
|
|
12
|
+
class Parser
|
|
13
|
+
def initialize(tokens, interpreter)
|
|
14
|
+
@tokens = tokens
|
|
15
|
+
@interpreter = interpreter
|
|
16
|
+
# Variables for parsing
|
|
17
|
+
@current = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse
|
|
21
|
+
statements = []
|
|
22
|
+
statements << declaration until end_of_input?
|
|
23
|
+
statements
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def declaration
|
|
27
|
+
if matches?(:var)
|
|
28
|
+
var_declaration
|
|
29
|
+
else
|
|
30
|
+
statement
|
|
31
|
+
end
|
|
32
|
+
rescue Lox::ParseError
|
|
33
|
+
synchronize
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def var_declaration
|
|
38
|
+
name = consume :identifier, 'Expect variable name.'
|
|
39
|
+
initializer = matches?(:equal) ? expression : nil
|
|
40
|
+
consume :semicolon, "Expect ';' after variable declaration."
|
|
41
|
+
Lox::AST::Statement::Var.new(name:, initializer:)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def statement
|
|
45
|
+
if matches? :print
|
|
46
|
+
print_statement
|
|
47
|
+
elsif matches? :left_brace
|
|
48
|
+
Lox::AST::Statement::Block.new(statements: block)
|
|
49
|
+
else
|
|
50
|
+
expression_statement
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def block
|
|
55
|
+
statements = []
|
|
56
|
+
statements << declaration until check(:right_brace) || end_of_input?
|
|
57
|
+
consume :right_brace, "Expect '}' after block."
|
|
58
|
+
statements
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def print_statement
|
|
62
|
+
value = expression_list
|
|
63
|
+
consume :semicolon, "Expect ';' after value."
|
|
64
|
+
Lox::AST::Statement::Print.new(expression: value)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def expression_statement
|
|
68
|
+
expr = expression_list
|
|
69
|
+
consume :semicolon, "Expect ';' after expression."
|
|
70
|
+
Lox::AST::Statement::Expression.new(expression: expr)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def expression_list
|
|
74
|
+
expr = conditional
|
|
75
|
+
|
|
76
|
+
while matches? :comma
|
|
77
|
+
operator = previous
|
|
78
|
+
right = conditional
|
|
79
|
+
expr = Lox::AST::Expression::Binary.new(left: expr, operator:, right:)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
expr
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Ternary operator
|
|
86
|
+
def conditional
|
|
87
|
+
expr = expression
|
|
88
|
+
|
|
89
|
+
if matches? :question
|
|
90
|
+
left_operator = previous
|
|
91
|
+
center = check(:colon) ? Lox::AST::Expression::Literal.new(value: nil) : expression_list
|
|
92
|
+
consume :colon, "Expect ':' after expression (ternary operator)."
|
|
93
|
+
right_operator = previous
|
|
94
|
+
right = conditional # Recurse, right-associative
|
|
95
|
+
expr = Lox::AST::Expression::Ternary.new(left: expr, left_operator:, center:, right_operator:, right:)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
expr
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def expression
|
|
102
|
+
assignment
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def assignment # rubocop:disable Metrics/MethodLength
|
|
106
|
+
expr = equality
|
|
107
|
+
|
|
108
|
+
if matches? :equal
|
|
109
|
+
equals = previous
|
|
110
|
+
value = assignment
|
|
111
|
+
|
|
112
|
+
if expr.is_a? Lox::AST::Expression::Variable
|
|
113
|
+
name = expr.name
|
|
114
|
+
return Lox::AST::Expression::Assign.new(name:, value:)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
error equals, 'Invalid assignment target.'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
expr
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def equality
|
|
124
|
+
expr = comparison
|
|
125
|
+
while matches?(:bang_equal, :equal_equal)
|
|
126
|
+
operator = previous
|
|
127
|
+
right = comparison
|
|
128
|
+
# Compose (equality is left-associative)
|
|
129
|
+
expr = Lox::AST::Expression::Binary.new(left: expr, operator:, right:)
|
|
130
|
+
end
|
|
131
|
+
expr
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def comparison
|
|
135
|
+
expr = term
|
|
136
|
+
|
|
137
|
+
while matches?(:greater, :greater_equal, :less, :less_equal)
|
|
138
|
+
operator = previous
|
|
139
|
+
right = term
|
|
140
|
+
expr = Lox::AST::Expression::Binary.new(left: expr, operator:, right:)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
expr
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def term
|
|
147
|
+
expr = factor
|
|
148
|
+
|
|
149
|
+
while matches?(:minus, :plus)
|
|
150
|
+
operator = previous
|
|
151
|
+
right = factor
|
|
152
|
+
expr = Lox::AST::Expression::Binary.new(left: expr, operator:, right:)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
expr
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def factor
|
|
159
|
+
expr = unary
|
|
160
|
+
|
|
161
|
+
while matches?(:slash, :star)
|
|
162
|
+
operator = previous
|
|
163
|
+
right = unary
|
|
164
|
+
expr = Lox::AST::Expression::Binary.new(left: expr, operator:, right:)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
expr
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def unary
|
|
171
|
+
# Unary operators are right-associative, so we match
|
|
172
|
+
# first, then recurse.
|
|
173
|
+
if matches?(:bang, :minus)
|
|
174
|
+
operator = previous
|
|
175
|
+
right = unary
|
|
176
|
+
|
|
177
|
+
Lox::AST::Expression::Unary.new(operator:, right:)
|
|
178
|
+
else
|
|
179
|
+
primary
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def primary # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
184
|
+
if matches? :false
|
|
185
|
+
Lox::AST::Expression::Literal.new(value: false)
|
|
186
|
+
elsif matches? :true
|
|
187
|
+
Lox::AST::Expression::Literal.new(value: true)
|
|
188
|
+
elsif matches? :nil
|
|
189
|
+
Lox::AST::Expression::Literal.new(value: nil)
|
|
190
|
+
elsif matches? :number, :string
|
|
191
|
+
Lox::AST::Expression::Literal.new(value: previous.literal)
|
|
192
|
+
elsif matches? :identifier
|
|
193
|
+
Lox::AST::Expression::Variable.new(name: previous)
|
|
194
|
+
elsif matches? :left_paren
|
|
195
|
+
expr = expression_list
|
|
196
|
+
consume :right_paren, "Expect ')' after expression."
|
|
197
|
+
Lox::AST::Expression::Grouping.new(expression: expr)
|
|
198
|
+
# Error productions--binary operator without left operand
|
|
199
|
+
elsif matches? :comma
|
|
200
|
+
err = error(previous, "Expect expression before ',' operator.")
|
|
201
|
+
conditional # Parse and throw away
|
|
202
|
+
raise err
|
|
203
|
+
elsif matches? :question
|
|
204
|
+
err = error(previous, 'Expect expression before ternary operator.')
|
|
205
|
+
expression_list
|
|
206
|
+
consume :colon, "Expect ':' after '?' (ternary operator)."
|
|
207
|
+
conditional
|
|
208
|
+
raise err
|
|
209
|
+
elsif matches? :bang_equal, :equal_equal
|
|
210
|
+
err = error(previous, "Expect value before '#{previous.lexeme}'.")
|
|
211
|
+
comparison
|
|
212
|
+
raise err
|
|
213
|
+
elsif matches? :greater, :greater_equal, :less, :less_equal
|
|
214
|
+
err = error(previous, "Expect value before '#{previous.lexeme}'.")
|
|
215
|
+
term
|
|
216
|
+
raise err
|
|
217
|
+
elsif matches? :plus
|
|
218
|
+
err = error(previous, "Expect value before '+'.")
|
|
219
|
+
factor
|
|
220
|
+
raise err
|
|
221
|
+
elsif matches? :slash, :star
|
|
222
|
+
err = error(previous, "Expect value before '#{previous.lexeme}'.")
|
|
223
|
+
unary
|
|
224
|
+
raise err
|
|
225
|
+
# Base case--no match.
|
|
226
|
+
else
|
|
227
|
+
raise error(peek, 'Expect expression.')
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
def consume(type, message)
|
|
234
|
+
return advance if check(type)
|
|
235
|
+
|
|
236
|
+
# Call #error to report the error to the interpreter, and
|
|
237
|
+
# raise the error in prep for synchronizing (panic mode).
|
|
238
|
+
raise error(peek, message)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def error(token, message)
|
|
242
|
+
@interpreter.error(token, message)
|
|
243
|
+
Lox::ParseError.new
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Synchronize the parser (i.e. panic mode).
|
|
247
|
+
# We're skipping possibly erroneous tokens
|
|
248
|
+
# to prevent cascading errors.
|
|
249
|
+
def synchronize
|
|
250
|
+
advance
|
|
251
|
+
|
|
252
|
+
until end_of_input?
|
|
253
|
+
return if previous.type == :semicolon
|
|
254
|
+
return if peek.type == :return
|
|
255
|
+
|
|
256
|
+
advance
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Checks if current token is a given type, consuming
|
|
261
|
+
# it if it is.
|
|
262
|
+
def matches?(*types)
|
|
263
|
+
# Array#any? guarantees short-circuit evaluation.
|
|
264
|
+
# #advance returns a Token/Expression, which is truthy.
|
|
265
|
+
# `advance if check(type)` is truthy when `check(type)`
|
|
266
|
+
# is.
|
|
267
|
+
types.any? { |type| advance if check(type) }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Checks if current token is a given type. Does not
|
|
271
|
+
# consume it.
|
|
272
|
+
def check(type)
|
|
273
|
+
peek.type == type && !end_of_input?
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def advance
|
|
277
|
+
@current += 1 unless end_of_input?
|
|
278
|
+
previous
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def end_of_input?
|
|
282
|
+
peek.type == :eof
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def peek
|
|
286
|
+
@tokens[@current]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def previous
|
|
290
|
+
@tokens[@current - 1]
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
data/lib/loxby/runner.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../loxby'
|
|
4
|
+
|
|
5
|
+
class Lox
|
|
6
|
+
# Lox::Runner is the interactive runner
|
|
7
|
+
# which kickstarts the interpreter.
|
|
8
|
+
# An instance is created when loxby is
|
|
9
|
+
# initialized from the command line.
|
|
10
|
+
class Runner
|
|
11
|
+
def initialize(out = $stdout, err = $stderr)
|
|
12
|
+
trap('SIGINT') { exit } # Exit cleanly on Ctrl-C
|
|
13
|
+
@interpreter = Lox.new
|
|
14
|
+
@out = out
|
|
15
|
+
@err = err
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(args)
|
|
19
|
+
if args.size > 1
|
|
20
|
+
@out.puts 'Usage: loxby.rb [script]'
|
|
21
|
+
exit 64
|
|
22
|
+
elsif args.size == 1
|
|
23
|
+
@interpreter.run_file args[0]
|
|
24
|
+
else
|
|
25
|
+
@interpreter.run_prompt # Run interactively
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helpers/token_type'
|
|
4
|
+
|
|
5
|
+
class Lox
|
|
6
|
+
class Scanner
|
|
7
|
+
EXPRESSIONS = {
|
|
8
|
+
whitespace: /\s/,
|
|
9
|
+
number_literal: /\d/,
|
|
10
|
+
identifier: /[a-zA-Z_]/
|
|
11
|
+
}.freeze
|
|
12
|
+
KEYWORDS = %w[and class else false for fun if nil or print return super this true var while]
|
|
13
|
+
.map { [_1, _1.to_sym] }
|
|
14
|
+
.to_h
|
|
15
|
+
|
|
16
|
+
attr_accessor :line
|
|
17
|
+
|
|
18
|
+
def initialize(source, interpreter)
|
|
19
|
+
@source = source
|
|
20
|
+
@tokens = []
|
|
21
|
+
@interpreter = interpreter
|
|
22
|
+
# Variables for scanning
|
|
23
|
+
@start = 0
|
|
24
|
+
@current = 0
|
|
25
|
+
@line = 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scan_tokens
|
|
29
|
+
until end_of_source?
|
|
30
|
+
# Beginnning of next lexeme
|
|
31
|
+
@start = @current
|
|
32
|
+
scan_token
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Implicitly return @tokens
|
|
36
|
+
@tokens << Lox::Token.new(:eof, "", nil, @line)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def scan_token # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
40
|
+
character = advance_character
|
|
41
|
+
|
|
42
|
+
case character
|
|
43
|
+
# Single-character tokens
|
|
44
|
+
when '('
|
|
45
|
+
add_token :left_paren
|
|
46
|
+
when ')'
|
|
47
|
+
add_token :right_paren
|
|
48
|
+
when '{'
|
|
49
|
+
add_token :left_brace
|
|
50
|
+
when '}'
|
|
51
|
+
add_token :right_brace
|
|
52
|
+
when ','
|
|
53
|
+
add_token :comma
|
|
54
|
+
when '.'
|
|
55
|
+
add_token :dot
|
|
56
|
+
when '-'
|
|
57
|
+
add_token :minus
|
|
58
|
+
when '+'
|
|
59
|
+
add_token :plus
|
|
60
|
+
when ';'
|
|
61
|
+
add_token :semicolon
|
|
62
|
+
when '*'
|
|
63
|
+
add_token :star
|
|
64
|
+
when '?'
|
|
65
|
+
add_token :question
|
|
66
|
+
when ':'
|
|
67
|
+
add_token :colon
|
|
68
|
+
# 1-2 character tokens
|
|
69
|
+
when '!'
|
|
70
|
+
add_token match('=') ? :bang_equal : :bang
|
|
71
|
+
when '='
|
|
72
|
+
add_token match('=') ? :equal_equal : :equal
|
|
73
|
+
when '<'
|
|
74
|
+
add_token match('=') ? :less_equal : :less
|
|
75
|
+
when '>'
|
|
76
|
+
add_token match('=') ? :greater_equal : :greater
|
|
77
|
+
when '/'
|
|
78
|
+
# '/' is division, '//' is comment, '/* ... */'
|
|
79
|
+
# is block comment. Needs special care.
|
|
80
|
+
if match('/') # comment line
|
|
81
|
+
advance_character until peek == "\n" || end_of_source?
|
|
82
|
+
elsif match('*') # block comment
|
|
83
|
+
scan_block_comment
|
|
84
|
+
else
|
|
85
|
+
add_token :slash
|
|
86
|
+
end
|
|
87
|
+
# Whitespace
|
|
88
|
+
when "\n"
|
|
89
|
+
@line += 1
|
|
90
|
+
when EXPRESSIONS[:whitespace]
|
|
91
|
+
# Literals
|
|
92
|
+
when '"'
|
|
93
|
+
scan_string
|
|
94
|
+
when EXPRESSIONS[:number_literal]
|
|
95
|
+
scan_number
|
|
96
|
+
# Keywords and identifiers
|
|
97
|
+
when EXPRESSIONS[:identifier]
|
|
98
|
+
scan_identifier
|
|
99
|
+
else
|
|
100
|
+
# Unknown character
|
|
101
|
+
@interpreter.error(@line, 'Unexpected character.')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def scan_block_comment # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
106
|
+
advance_character until (peek == '*' && peek_next == '/') || (peek == '/' && peek_next == '*') || end_of_source?
|
|
107
|
+
|
|
108
|
+
if end_of_source? || peek_next == "\0"
|
|
109
|
+
@interpreter.error(line, 'Unterminated block comment.')
|
|
110
|
+
return
|
|
111
|
+
elsif peek == '/' && peek_next == '*'
|
|
112
|
+
# Nested block comment. Skip opening characters
|
|
113
|
+
match '/'
|
|
114
|
+
match '*'
|
|
115
|
+
scan_block_comment # Skip nested comment
|
|
116
|
+
advance_character until (peek == '*' && peek_next == '/') || (peek == '/' && peek_next == '*') || end_of_source?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Skip closing characters
|
|
120
|
+
match '*'
|
|
121
|
+
match '/'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def scan_string # rubocop:disable Metrics/MethodLength
|
|
125
|
+
until peek == '"' || end_of_source?
|
|
126
|
+
@line += 1 if peek == "\n"
|
|
127
|
+
advance_character
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if end_of_source?
|
|
131
|
+
@interpreter.error(line, 'Unterminated string.')
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Skip closing "
|
|
136
|
+
advance_character
|
|
137
|
+
|
|
138
|
+
# Trim quotes around literal
|
|
139
|
+
value = @source[(@start + 1)...(@current - 1)]
|
|
140
|
+
add_token :string, value
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def scan_number
|
|
144
|
+
advance_character while peek =~ EXPRESSIONS[:number_literal]
|
|
145
|
+
|
|
146
|
+
# Check for decimal
|
|
147
|
+
if peek == '.' && peek_next =~ EXPRESSIONS[:number_literal]
|
|
148
|
+
# Consume decimal point
|
|
149
|
+
advance_character
|
|
150
|
+
advance_character while peek =~ EXPRESSIONS[:number_literal]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
add_token :number, @source[@start...@current].to_f
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def scan_identifier
|
|
157
|
+
advance_character while peek =~ Regexp.union(EXPRESSIONS[:identifier], /\d/)
|
|
158
|
+
text = @source[@start...@current]
|
|
159
|
+
add_token(KEYWORDS[text] || :identifier)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def advance_character
|
|
163
|
+
character = @source[@current]
|
|
164
|
+
@current += 1
|
|
165
|
+
character
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def add_token(type, literal = nil)
|
|
169
|
+
text = @source[@start...@current]
|
|
170
|
+
@tokens << Lox::Token.new(type, text, literal, @line)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def match(expected)
|
|
174
|
+
return false unless @source[@current] == expected || end_of_source?
|
|
175
|
+
|
|
176
|
+
@current += 1
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def end_of_source? = @current >= @source.size
|
|
181
|
+
|
|
182
|
+
# 1-character lookahead
|
|
183
|
+
def peek
|
|
184
|
+
end_of_source? ? "\0" : @source[@current]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# 2-character lookahead
|
|
188
|
+
def peek_next
|
|
189
|
+
(@current + 1) > @source.size ? "\0" : @source[@current + 1]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
class ASTPrinter < Visitor
|
|
6
|
+
def print(expression)
|
|
7
|
+
expression.accept self
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def parenthesize(*args)
|
|
11
|
+
str = "(#{args[0]}"
|
|
12
|
+
args[1..].each do |expr|
|
|
13
|
+
str << " #{expr.accept self}"
|
|
14
|
+
end
|
|
15
|
+
str << ')'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_binary_expression(expr)
|
|
19
|
+
parenthesize expr.operator.lexeme, expr.left, expr.right
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def visit_ternary_expression(expr)
|
|
23
|
+
parenthesize expr.left_operator.lexeme + expr.right_operator.lexeme, expr.left, expr.center, expr.right
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def visit_grouping_expression(expr)
|
|
27
|
+
parenthesize 'group', expr.expression
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_literal_expression(expr)
|
|
31
|
+
expr.value.nil? ? 'nil' : expr.value.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visit_unary_expression(expr)
|
|
35
|
+
parenthesize expr.operator.lexeme, expr.right
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Visitable adds #accept, the only
|
|
4
|
+
# method required to implement the
|
|
5
|
+
# visitor pattern on a class.
|
|
6
|
+
# To use the visitor pattern,
|
|
7
|
+
# `include Visitable` to your class
|
|
8
|
+
# and subclass `Visitor` to implement
|
|
9
|
+
# visitors.
|
|
10
|
+
module Visitable
|
|
11
|
+
def accept(visitor)
|
|
12
|
+
raise NotImplementedError, "#{self.class} has not implemented #accept"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Base visitor class for visitor pattern.
|
|
17
|
+
# See Visitable.
|
|
18
|
+
class Visitor
|
|
19
|
+
def self.define_types(base_type, subtypes)
|
|
20
|
+
subtypes.each do |subtype|
|
|
21
|
+
method_name = "visit_#{subtype}_#{base_type}"
|
|
22
|
+
define_method(method_name.to_sym) do |_|
|
|
23
|
+
raise NotImplementedError, "#{self.class} has not implemented ##{method_name}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
class RPNConverter < Visitor
|
|
6
|
+
def print(expr)
|
|
7
|
+
expr.accept self
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def visit_binary_expression(expr)
|
|
11
|
+
"#{expr.left.accept self} #{expr.right.accept self} #{expr.operator.lexeme}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def visit_grouping_expression(expr)
|
|
15
|
+
expr.accept self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_literal_expression(expr)
|
|
19
|
+
expr.value.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def visit_unary_expression(expr)
|
|
23
|
+
"#{expr.right.accept self} #{expr.operator.lexeme}"
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/loxby.rb
ADDED
data/loxby.gemspec
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/loxby/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = 'loxby'
|
|
7
|
+
s.version = Loxby::VERSION
|
|
8
|
+
s.authors = ['Paul Hartman']
|
|
9
|
+
s.email = ['real.paul.hartman@gmail.com']
|
|
10
|
+
|
|
11
|
+
s.summary = 'A Lox interpreter written in Ruby'
|
|
12
|
+
s.description = 'Loxby is written following the first ' \
|
|
13
|
+
"half of Robert Nystrom's wonderful web-format book " \
|
|
14
|
+
'[Crafting Interpreters](https://www.craftinginterpreters.com), ' \
|
|
15
|
+
'adapting the Java code to modern Ruby. This project is ' \
|
|
16
|
+
'intended to explore what elegant object-oriented code ' \
|
|
17
|
+
'can look like and accomplish.'
|
|
18
|
+
s.homepage = 'https://github.com/paul-c-hartman/loxby'
|
|
19
|
+
s.license = 'MIT'
|
|
20
|
+
s.required_ruby_version = Gem::Requirement.new('>= 3.1.0') # Shorthand hash syntax
|
|
21
|
+
|
|
22
|
+
s.metadata['homepage_uri'] = s.homepage
|
|
23
|
+
s.metadata['source_code_uri'] = s.homepage
|
|
24
|
+
|
|
25
|
+
# Specify which files should be added to the gem when it is released.
|
|
26
|
+
# `git ls-files -z` loads the files in the gem which git is tracking.
|
|
27
|
+
s.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
28
|
+
`git ls-files -z`.split("\x0").reject { _1.match %r{^(test|spec|features)/} } - %w[Gemfile Gemfile.lock.rubocop.yml]
|
|
29
|
+
end
|
|
30
|
+
s.bindir = 'bin'
|
|
31
|
+
s.executables = %w[loxby]
|
|
32
|
+
s.require_paths = %w[lib]
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: loxby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Paul Hartman
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-06-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Loxby is written following the first half of Robert Nystrom's wonderful
|
|
14
|
+
web-format book [Crafting Interpreters](https://www.craftinginterpreters.com), adapting
|
|
15
|
+
the Java code to modern Ruby. This project is intended to explore what elegant object-oriented
|
|
16
|
+
code can look like and accomplish.
|
|
17
|
+
email:
|
|
18
|
+
- real.paul.hartman@gmail.com
|
|
19
|
+
executables:
|
|
20
|
+
- loxby
|
|
21
|
+
extensions: []
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
files:
|
|
24
|
+
- ".github/workflows/gem-push.yml"
|
|
25
|
+
- ".rubocop.yml"
|
|
26
|
+
- Gemfile.lock
|
|
27
|
+
- README.md
|
|
28
|
+
- bin/loxby
|
|
29
|
+
- lib/loxby.rb
|
|
30
|
+
- lib/loxby/core.rb
|
|
31
|
+
- lib/loxby/helpers/ast.rb
|
|
32
|
+
- lib/loxby/helpers/environment.rb
|
|
33
|
+
- lib/loxby/helpers/errors.rb
|
|
34
|
+
- lib/loxby/helpers/token_type.rb
|
|
35
|
+
- lib/loxby/interpreter.rb
|
|
36
|
+
- lib/loxby/parser.rb
|
|
37
|
+
- lib/loxby/runner.rb
|
|
38
|
+
- lib/loxby/scanner.rb
|
|
39
|
+
- lib/loxby/version.rb
|
|
40
|
+
- lib/loxby/visitors/ast_printer.rb
|
|
41
|
+
- lib/loxby/visitors/base.rb
|
|
42
|
+
- lib/loxby/visitors/rpn_converter.rb
|
|
43
|
+
- loxby.gemspec
|
|
44
|
+
homepage: https://github.com/paul-c-hartman/loxby
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata:
|
|
48
|
+
homepage_uri: https://github.com/paul-c-hartman/loxby
|
|
49
|
+
source_code_uri: https://github.com/paul-c-hartman/loxby
|
|
50
|
+
post_install_message:
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: 3.1.0
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.3.26
|
|
66
|
+
signing_key:
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: A Lox interpreter written in Ruby
|
|
69
|
+
test_files: []
|