loxby 0.0.2 → 0.0.3
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 +4 -4
- data/.github/workflows/gem-push.yml +2 -2
- data/.gitignore +8 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +4 -1
- data/bin/loxby +2 -2
- data/lib/loxby/config.rb +93 -0
- data/lib/loxby/core.rb +24 -14
- data/lib/loxby/helpers/ast.rb +18 -31
- data/lib/loxby/helpers/callable.rb +18 -0
- data/lib/loxby/helpers/environment.rb +18 -7
- data/lib/loxby/helpers/errors.rb +7 -1
- data/lib/loxby/helpers/functions.rb +40 -0
- data/lib/loxby/helpers/native_functions.rb +48 -0
- data/lib/loxby/helpers/token_type.rb +9 -21
- data/lib/loxby/interpreter.rb +76 -3
- data/lib/loxby/parser.rb +196 -11
- data/lib/loxby/runner.rb +13 -5
- data/lib/loxby/scanner.rb +33 -45
- data/lib/loxby/version.rb +2 -2
- data/lib/loxby/visitors/ast_printer.rb +18 -0
- data/lib/loxby/visitors/base.rb +1 -1
- data/lib/loxby/visitors/rpn_converter.rb +2 -0
- data/lib/loxby.rb +0 -0
- data/loxby.gemspec +7 -4
- metadata +13 -7
- data/.rubocop.yml +0 -8
- data/Gemfile.lock +0 -47
data/lib/loxby/interpreter.rb
CHANGED
|
@@ -2,14 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'helpers/environment'
|
|
4
4
|
require_relative 'helpers/errors'
|
|
5
|
+
require_relative 'helpers/callable'
|
|
6
|
+
require_relative 'helpers/native_functions'
|
|
7
|
+
require_relative 'helpers/functions'
|
|
5
8
|
require_relative 'visitors/base'
|
|
6
9
|
|
|
7
10
|
# Interpreter class. Walks the AST using
|
|
8
11
|
# the Visitor pattern.
|
|
9
12
|
class Interpreter < Visitor
|
|
13
|
+
attr_reader :globals
|
|
14
|
+
|
|
10
15
|
def initialize(process) # rubocop:disable Lint/MissingSuper
|
|
11
16
|
@process = process
|
|
12
|
-
|
|
17
|
+
# `@globals` always refers to the same environment regardless of scope.
|
|
18
|
+
@globals = Lox::Environment.new
|
|
19
|
+
# `@environment` changes based on scope.
|
|
20
|
+
@environment = @globals
|
|
21
|
+
|
|
22
|
+
define_native_functions
|
|
13
23
|
end
|
|
14
24
|
|
|
15
25
|
def interpret(statements)
|
|
@@ -18,6 +28,7 @@ class Interpreter < Visitor
|
|
|
18
28
|
result
|
|
19
29
|
rescue Lox::RunError => e
|
|
20
30
|
@process.runtime_error e
|
|
31
|
+
nil
|
|
21
32
|
end
|
|
22
33
|
|
|
23
34
|
def lox_eval(expr)
|
|
@@ -25,7 +36,7 @@ class Interpreter < Visitor
|
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# Lox's definition of truthiness follows
|
|
28
|
-
# Ruby's
|
|
39
|
+
# Ruby's (for now), so this is a no-op (for now)
|
|
29
40
|
def truthy?(obj)
|
|
30
41
|
obj
|
|
31
42
|
end
|
|
@@ -49,16 +60,43 @@ class Interpreter < Visitor
|
|
|
49
60
|
lox_eval statement.expression
|
|
50
61
|
end
|
|
51
62
|
|
|
63
|
+
def visit_function_statement(statement)
|
|
64
|
+
function = Lox::Function.new(statement, @environment)
|
|
65
|
+
@environment[statement.name] = function if statement.name
|
|
66
|
+
function
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def visit_if_statement(statement)
|
|
70
|
+
if truthy? lox_eval(statement.condition)
|
|
71
|
+
lox_eval statement.then_branch
|
|
72
|
+
elsif !statement.else_branch.nil?
|
|
73
|
+
lox_eval statement.else_branch
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
52
77
|
def visit_print_statement(statement)
|
|
53
78
|
value = lox_eval statement.expression
|
|
54
79
|
puts lox_obj_to_str(value)
|
|
55
80
|
end
|
|
56
81
|
|
|
82
|
+
def visit_return_statement(statement)
|
|
83
|
+
value = statement.value.nil? ? nil : lox_eval(statement.value)
|
|
84
|
+
throw :return, value # This is not an error, just sending a message up the callstack
|
|
85
|
+
end
|
|
86
|
+
|
|
57
87
|
def visit_var_statement(statement)
|
|
58
88
|
value = statement.initializer ? lox_eval(statement.initializer) : nil
|
|
59
89
|
@environment[statement.name] = value
|
|
60
90
|
end
|
|
61
91
|
|
|
92
|
+
def visit_while_statement(statement)
|
|
93
|
+
catch :break do # Jump beacon for break statements
|
|
94
|
+
value = nil
|
|
95
|
+
(value = lox_eval statement.body) while truthy?(lox_eval(statement.condition))
|
|
96
|
+
value
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
62
100
|
def visit_variable_expression(expr)
|
|
63
101
|
@environment[expr.name]
|
|
64
102
|
end
|
|
@@ -70,13 +108,22 @@ class Interpreter < Visitor
|
|
|
70
108
|
end
|
|
71
109
|
|
|
72
110
|
def visit_block_statement(statement)
|
|
111
|
+
# Pull out a copy of the environment
|
|
112
|
+
# so that blocks are closures
|
|
73
113
|
execute_block(statement.statements, Lox::Environment.new(@environment))
|
|
74
114
|
end
|
|
75
115
|
|
|
116
|
+
def visit_break_statement(_)
|
|
117
|
+
throw :break
|
|
118
|
+
end
|
|
119
|
+
|
|
76
120
|
def execute_block(statements, environment)
|
|
77
121
|
previous = @environment
|
|
78
122
|
@environment = environment
|
|
79
123
|
statements.each { lox_eval _1 }
|
|
124
|
+
nil
|
|
125
|
+
rescue Lox::RunError
|
|
126
|
+
nil
|
|
80
127
|
ensure
|
|
81
128
|
@environment = previous
|
|
82
129
|
end
|
|
@@ -87,8 +134,19 @@ class Interpreter < Visitor
|
|
|
87
134
|
expr.value
|
|
88
135
|
end
|
|
89
136
|
|
|
137
|
+
def visit_logical_expression(expr)
|
|
138
|
+
left = lox_eval expr.left
|
|
139
|
+
|
|
140
|
+
case expr.operator.type
|
|
141
|
+
when :or
|
|
142
|
+
left if truthy? left
|
|
143
|
+
else # Just and, for now
|
|
144
|
+
truthy?(left) ? lox_eval(expr.right) : left
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
90
148
|
def visit_grouping_expression(expr)
|
|
91
|
-
lox_eval expr
|
|
149
|
+
lox_eval expr.expression
|
|
92
150
|
end
|
|
93
151
|
|
|
94
152
|
def visit_unary_expression(expr)
|
|
@@ -150,4 +208,19 @@ class Interpreter < Visitor
|
|
|
150
208
|
|
|
151
209
|
left ? lox_eval(expr.center) : lox_eval(expr.right)
|
|
152
210
|
end
|
|
211
|
+
|
|
212
|
+
def visit_call_expression(expr) # rubocop:disable Metrics/AbcSize
|
|
213
|
+
callee = lox_eval expr.callee
|
|
214
|
+
arguments = expr.arguments.map { lox_eval _1 }
|
|
215
|
+
|
|
216
|
+
unless callee.class.include? Lox::Callable
|
|
217
|
+
raise Lox::RunError.new(expr.paren, 'Can only call functions and classes.')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
unless arguments.size == callee.arity
|
|
221
|
+
raise Lox::RunError.new(expr.paren, "Expected #{callee.arity} arguments but got #{arguments.size}.")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
callee.call(self, arguments)
|
|
225
|
+
end
|
|
153
226
|
end
|
data/lib/loxby/parser.rb
CHANGED
|
@@ -4,11 +4,11 @@ require_relative 'helpers/ast'
|
|
|
4
4
|
require_relative 'helpers/errors'
|
|
5
5
|
|
|
6
6
|
class Lox
|
|
7
|
-
# Lox::Parser converts a list of tokens
|
|
8
|
-
# from Lox::Scanner to a syntax tree.
|
|
7
|
+
# `Lox::Parser` converts a list of tokens
|
|
8
|
+
# from `Lox::Scanner` to a syntax tree.
|
|
9
9
|
# This tree can be interacted with using
|
|
10
|
-
# the Visitor pattern. (See
|
|
11
|
-
#
|
|
10
|
+
# the Visitor pattern. (See `Visitor`
|
|
11
|
+
# in lib/visitors/base.rb.)
|
|
12
12
|
class Parser
|
|
13
13
|
def initialize(tokens, interpreter)
|
|
14
14
|
@tokens = tokens
|
|
@@ -24,7 +24,9 @@ class Lox
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def declaration
|
|
27
|
-
if matches?
|
|
27
|
+
if matches? :fun
|
|
28
|
+
function 'function'
|
|
29
|
+
elsif matches? :var
|
|
28
30
|
var_declaration
|
|
29
31
|
else
|
|
30
32
|
statement
|
|
@@ -34,6 +36,33 @@ class Lox
|
|
|
34
36
|
nil
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
def function(kind) # rubocop:disable Metrics/MethodLength
|
|
40
|
+
name = nil
|
|
41
|
+
if check :identifier
|
|
42
|
+
# Named function
|
|
43
|
+
name = consume :identifier, "Expect #{kind} name."
|
|
44
|
+
end
|
|
45
|
+
consume :left_paren, "Expect '(' after #{kind} name."
|
|
46
|
+
parameters = []
|
|
47
|
+
loop do
|
|
48
|
+
break if check :right_paren
|
|
49
|
+
|
|
50
|
+
# This is mostly arbitrary but it keeps execution time down
|
|
51
|
+
error(peek, "Can't have more than 255 parameters.") if parameters.size > 255
|
|
52
|
+
|
|
53
|
+
parameters << consume(:identifier, 'Expect parameter name.')
|
|
54
|
+
break unless matches? :comma
|
|
55
|
+
end
|
|
56
|
+
consume :right_paren, "Expect ')' after parameters."
|
|
57
|
+
|
|
58
|
+
# Have to consume the first part of the block since
|
|
59
|
+
# it assumes it's already been matched
|
|
60
|
+
consume :left_brace, 'Expect block after parameter list.'
|
|
61
|
+
body = block
|
|
62
|
+
|
|
63
|
+
Lox::AST::Statement::Function.new(name:, params: parameters, body:)
|
|
64
|
+
end
|
|
65
|
+
|
|
37
66
|
def var_declaration
|
|
38
67
|
name = consume :identifier, 'Expect variable name.'
|
|
39
68
|
initializer = matches?(:equal) ? expression : nil
|
|
@@ -41,9 +70,19 @@ class Lox
|
|
|
41
70
|
Lox::AST::Statement::Var.new(name:, initializer:)
|
|
42
71
|
end
|
|
43
72
|
|
|
44
|
-
def statement
|
|
45
|
-
if matches? :
|
|
73
|
+
def statement # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
74
|
+
if matches? :for
|
|
75
|
+
for_statement
|
|
76
|
+
elsif matches? :if
|
|
77
|
+
if_statement
|
|
78
|
+
elsif matches? :print
|
|
46
79
|
print_statement
|
|
80
|
+
elsif matches? :return
|
|
81
|
+
return_statement
|
|
82
|
+
elsif matches? :while
|
|
83
|
+
while_statement
|
|
84
|
+
elsif matches? :fun
|
|
85
|
+
function 'function'
|
|
47
86
|
elsif matches? :left_brace
|
|
48
87
|
Lox::AST::Statement::Block.new(statements: block)
|
|
49
88
|
else
|
|
@@ -51,9 +90,17 @@ class Lox
|
|
|
51
90
|
end
|
|
52
91
|
end
|
|
53
92
|
|
|
93
|
+
def break_or_declaration
|
|
94
|
+
if matches? :break
|
|
95
|
+
break_statement
|
|
96
|
+
else
|
|
97
|
+
declaration
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
54
101
|
def block
|
|
55
102
|
statements = []
|
|
56
|
-
statements <<
|
|
103
|
+
statements << break_or_declaration until check(:right_brace) || end_of_input?
|
|
57
104
|
consume :right_brace, "Expect '}' after block."
|
|
58
105
|
statements
|
|
59
106
|
end
|
|
@@ -64,6 +111,92 @@ class Lox
|
|
|
64
111
|
Lox::AST::Statement::Print.new(expression: value)
|
|
65
112
|
end
|
|
66
113
|
|
|
114
|
+
def return_statement
|
|
115
|
+
keyword = previous
|
|
116
|
+
value = nil
|
|
117
|
+
value = expression unless check :semicolon
|
|
118
|
+
|
|
119
|
+
consume :semicolon, "Expect ';' after return value."
|
|
120
|
+
Lox::AST::Statement::Return.new(keyword:, value:)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def if_statement
|
|
124
|
+
consume :left_paren, "Expect '(' after 'if'."
|
|
125
|
+
condition = expression_list
|
|
126
|
+
consume :right_paren, "Expect ')' after if condition."
|
|
127
|
+
|
|
128
|
+
# We don't go up to var declaration because variables
|
|
129
|
+
# declared inside an if statement should be inside a
|
|
130
|
+
# *block* inside the if statement.
|
|
131
|
+
then_branch = statement
|
|
132
|
+
else_branch = matches?(:else) ? statement : nil
|
|
133
|
+
|
|
134
|
+
Lox::AST::Statement::If.new(condition:, then_branch:, else_branch:)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def while_statement
|
|
138
|
+
consume :left_paren, "Expect '(' after 'while'."
|
|
139
|
+
condition = expression_list
|
|
140
|
+
consume :right_paren, "Expect ')' after condition."
|
|
141
|
+
body = statement
|
|
142
|
+
|
|
143
|
+
Lox::AST::Statement::While.new(condition:, body:)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# `for` loops in loxby are syntactic sugar
|
|
147
|
+
# for `while` loops. Yay!
|
|
148
|
+
|
|
149
|
+
def for_statement # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
150
|
+
consume :left_paren, "Expect '(' after 'for'."
|
|
151
|
+
|
|
152
|
+
initializer =
|
|
153
|
+
if matches? :semicolon
|
|
154
|
+
nil
|
|
155
|
+
elsif matches? :var
|
|
156
|
+
var_declaration
|
|
157
|
+
else
|
|
158
|
+
expression_statement
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
condition = nil
|
|
162
|
+
condition = expression unless check :semicolon
|
|
163
|
+
consume :semicolon, "Expect ';' after loop condition."
|
|
164
|
+
|
|
165
|
+
increment = nil
|
|
166
|
+
increment = expression unless check :right_paren
|
|
167
|
+
consume :right_paren, "Expect ')' after for clauses."
|
|
168
|
+
|
|
169
|
+
body = statement
|
|
170
|
+
|
|
171
|
+
unless increment.nil?
|
|
172
|
+
body = Lox::AST::Statement::Block.new(
|
|
173
|
+
statements: [
|
|
174
|
+
body,
|
|
175
|
+
Lox::AST::Statement::Expression.new(expression: increment)
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
condition = Lox::AST::Expression::Literal.new(value: true) if condition.nil?
|
|
181
|
+
body = Lox::AST::Statement::While.new(condition:, body:)
|
|
182
|
+
|
|
183
|
+
unless initializer.nil?
|
|
184
|
+
body = Lox::AST::Statement::Block.new(
|
|
185
|
+
statements: [
|
|
186
|
+
initializer,
|
|
187
|
+
body
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
body
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def break_statement
|
|
196
|
+
consume :semicolon, "Expect ';' after break."
|
|
197
|
+
Lox::AST::Statement::Break.new
|
|
198
|
+
end
|
|
199
|
+
|
|
67
200
|
def expression_statement
|
|
68
201
|
expr = expression_list
|
|
69
202
|
consume :semicolon, "Expect ';' after expression."
|
|
@@ -89,7 +222,7 @@ class Lox
|
|
|
89
222
|
if matches? :question
|
|
90
223
|
left_operator = previous
|
|
91
224
|
center = check(:colon) ? Lox::AST::Expression::Literal.new(value: nil) : expression_list
|
|
92
|
-
consume :colon, "Expect ':' after expression
|
|
225
|
+
consume :colon, "Expect ':' after expression: incomplete ternary operator."
|
|
93
226
|
right_operator = previous
|
|
94
227
|
right = conditional # Recurse, right-associative
|
|
95
228
|
expr = Lox::AST::Expression::Ternary.new(left: expr, left_operator:, center:, right_operator:, right:)
|
|
@@ -103,7 +236,7 @@ class Lox
|
|
|
103
236
|
end
|
|
104
237
|
|
|
105
238
|
def assignment # rubocop:disable Metrics/MethodLength
|
|
106
|
-
expr =
|
|
239
|
+
expr = logical_or
|
|
107
240
|
|
|
108
241
|
if matches? :equal
|
|
109
242
|
equals = previous
|
|
@@ -120,6 +253,30 @@ class Lox
|
|
|
120
253
|
expr
|
|
121
254
|
end
|
|
122
255
|
|
|
256
|
+
def logical_or
|
|
257
|
+
expr = logical_and
|
|
258
|
+
|
|
259
|
+
while matches? :or
|
|
260
|
+
operator = previous
|
|
261
|
+
right = logical_and
|
|
262
|
+
expr = Lox::AST::Expression::Logical.new(left: expr, operator:, right:)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
expr
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def logical_and
|
|
269
|
+
expr = equality
|
|
270
|
+
|
|
271
|
+
while matches? :and
|
|
272
|
+
operator = previous
|
|
273
|
+
right = logical_and
|
|
274
|
+
expr = Lox::AST::Expression::Logical.new(left: expr, operator:, right:)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
expr
|
|
278
|
+
end
|
|
279
|
+
|
|
123
280
|
def equality
|
|
124
281
|
expr = comparison
|
|
125
282
|
while matches?(:bang_equal, :equal_equal)
|
|
@@ -176,8 +333,31 @@ class Lox
|
|
|
176
333
|
|
|
177
334
|
Lox::AST::Expression::Unary.new(operator:, right:)
|
|
178
335
|
else
|
|
179
|
-
|
|
336
|
+
function_call
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def function_call
|
|
341
|
+
expr = primary
|
|
342
|
+
loop do
|
|
343
|
+
break unless matches? :left_paren
|
|
344
|
+
|
|
345
|
+
expr = finish_call expr
|
|
346
|
+
end
|
|
347
|
+
expr
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def finish_call(callee)
|
|
351
|
+
arguments = []
|
|
352
|
+
unless check :right_paren
|
|
353
|
+
loop do
|
|
354
|
+
error(peek, "Can't have more than 255 arguments.") if arguments.size > 255
|
|
355
|
+
arguments << expression
|
|
356
|
+
break unless matches? :comma
|
|
357
|
+
end
|
|
180
358
|
end
|
|
359
|
+
paren = consume :right_paren, "Expect ')' after arguments."
|
|
360
|
+
Lox::AST::Expression::Call.new(callee:, paren:, arguments:)
|
|
181
361
|
end
|
|
182
362
|
|
|
183
363
|
def primary # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
@@ -187,8 +367,12 @@ class Lox
|
|
|
187
367
|
Lox::AST::Expression::Literal.new(value: true)
|
|
188
368
|
elsif matches? :nil
|
|
189
369
|
Lox::AST::Expression::Literal.new(value: nil)
|
|
370
|
+
elsif matches? :fun
|
|
371
|
+
function 'inline function'
|
|
190
372
|
elsif matches? :number, :string
|
|
191
373
|
Lox::AST::Expression::Literal.new(value: previous.literal)
|
|
374
|
+
elsif matches? :break
|
|
375
|
+
raise error(previous, "Invalid 'break' not in loop.")
|
|
192
376
|
elsif matches? :identifier
|
|
193
377
|
Lox::AST::Expression::Variable.new(name: previous)
|
|
194
378
|
elsif matches? :left_paren
|
|
@@ -202,6 +386,7 @@ class Lox
|
|
|
202
386
|
raise err
|
|
203
387
|
elsif matches? :question
|
|
204
388
|
err = error(previous, 'Expect expression before ternary operator.')
|
|
389
|
+
# Parse and throw away
|
|
205
390
|
expression_list
|
|
206
391
|
consume :colon, "Expect ':' after '?' (ternary operator)."
|
|
207
392
|
conditional
|
data/lib/loxby/runner.rb
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../loxby'
|
|
4
|
+
require_relative 'config'
|
|
4
5
|
|
|
5
6
|
class Lox
|
|
6
|
-
# Lox::Runner is the interactive runner
|
|
7
|
+
# `Lox::Runner` is the interactive runner
|
|
7
8
|
# which kickstarts the interpreter.
|
|
8
9
|
# An instance is created when loxby is
|
|
9
|
-
# initialized from the command line
|
|
10
|
+
# initialized from the command line,
|
|
11
|
+
# though it can be instantiated from
|
|
12
|
+
# code as well.
|
|
10
13
|
class Runner
|
|
11
14
|
def initialize(out = $stdout, err = $stderr)
|
|
12
|
-
|
|
15
|
+
# Exit cleanly. 130 is for interrupted scripts
|
|
16
|
+
trap('INT') do
|
|
17
|
+
puts
|
|
18
|
+
exit Lox.config.exit_code.interrupt
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
@interpreter = Lox.new
|
|
14
22
|
@out = out
|
|
15
23
|
@err = err
|
|
@@ -17,8 +25,8 @@ class Lox
|
|
|
17
25
|
|
|
18
26
|
def run(args)
|
|
19
27
|
if args.size > 1
|
|
20
|
-
@out.puts 'Usage: loxby
|
|
21
|
-
exit
|
|
28
|
+
@out.puts 'Usage: loxby [script]'
|
|
29
|
+
exit Lox.config.exit_code.usage
|
|
22
30
|
elsif args.size == 1
|
|
23
31
|
@interpreter.run_file args[0]
|
|
24
32
|
else
|
data/lib/loxby/scanner.rb
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'helpers/token_type'
|
|
4
|
+
require_relative 'config'
|
|
4
5
|
|
|
5
6
|
class Lox
|
|
7
|
+
# `Lox::Scanner` converts a string to
|
|
8
|
+
# a series of tokens using a giant
|
|
9
|
+
# `case` statement.
|
|
6
10
|
class Scanner
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.to_h
|
|
11
|
+
# Custom character classes for certain tokens.
|
|
12
|
+
EXPRESSIONS = Lox.config.scanner.expressions.to_h
|
|
13
|
+
|
|
14
|
+
# Map of keywords to token types.
|
|
15
|
+
# Right now, all keywords have
|
|
16
|
+
# their own token type.
|
|
17
|
+
KEYWORDS = Lox.config.scanner.keywords.map { [_1, _1.to_sym] }.to_h
|
|
15
18
|
|
|
16
19
|
attr_accessor :line
|
|
17
20
|
|
|
@@ -25,6 +28,8 @@ class Lox
|
|
|
25
28
|
@line = 1
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
# Process text source into
|
|
32
|
+
# a list of tokens.
|
|
28
33
|
def scan_tokens
|
|
29
34
|
until end_of_source?
|
|
30
35
|
# Beginnning of next lexeme
|
|
@@ -33,38 +38,28 @@ class Lox
|
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
# Implicitly return @tokens
|
|
36
|
-
@tokens << Lox::Token.new(:eof,
|
|
41
|
+
@tokens << Lox::Token.new(:eof, '', nil, @line)
|
|
37
42
|
end
|
|
38
43
|
|
|
44
|
+
# Consume enough characters for the next token.
|
|
45
|
+
|
|
39
46
|
def scan_token # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
40
47
|
character = advance_character
|
|
41
48
|
|
|
42
49
|
case character
|
|
50
|
+
# '/' is division, '//' is comment, '/* ... */'
|
|
51
|
+
# is block comment. Needs special care.
|
|
52
|
+
when '/'
|
|
53
|
+
if match('/') # comment line
|
|
54
|
+
advance_character until peek == "\n" || end_of_source?
|
|
55
|
+
elsif match('*') # block comment
|
|
56
|
+
scan_block_comment
|
|
57
|
+
else
|
|
58
|
+
add_token :slash
|
|
59
|
+
end
|
|
43
60
|
# Single-character tokens
|
|
44
|
-
when
|
|
45
|
-
add_token
|
|
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
|
|
61
|
+
when Regexp.union(Lox::Token::SINGLE_TOKENS.keys)
|
|
62
|
+
add_token Lox::Token::SINGLE_TOKENS[character]
|
|
68
63
|
# 1-2 character tokens
|
|
69
64
|
when '!'
|
|
70
65
|
add_token match('=') ? :bang_equal : :bang
|
|
@@ -74,16 +69,6 @@ class Lox
|
|
|
74
69
|
add_token match('=') ? :less_equal : :less
|
|
75
70
|
when '>'
|
|
76
71
|
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
72
|
# Whitespace
|
|
88
73
|
when "\n"
|
|
89
74
|
@line += 1
|
|
@@ -105,7 +90,7 @@ class Lox
|
|
|
105
90
|
def scan_block_comment # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
106
91
|
advance_character until (peek == '*' && peek_next == '/') || (peek == '/' && peek_next == '*') || end_of_source?
|
|
107
92
|
|
|
108
|
-
if end_of_source? || peek_next == "\0"
|
|
93
|
+
if end_of_source? || peek_next == "\0" # If 0 or 1 characters are left
|
|
109
94
|
@interpreter.error(line, 'Unterminated block comment.')
|
|
110
95
|
return
|
|
111
96
|
elsif peek == '/' && peek_next == '*'
|
|
@@ -123,7 +108,7 @@ class Lox
|
|
|
123
108
|
|
|
124
109
|
def scan_string # rubocop:disable Metrics/MethodLength
|
|
125
110
|
until peek == '"' || end_of_source?
|
|
126
|
-
@line += 1 if peek == "\n"
|
|
111
|
+
@line += 1 if peek == "\n" # Multiline strings are valid
|
|
127
112
|
advance_character
|
|
128
113
|
end
|
|
129
114
|
|
|
@@ -159,17 +144,20 @@ class Lox
|
|
|
159
144
|
add_token(KEYWORDS[text] || :identifier)
|
|
160
145
|
end
|
|
161
146
|
|
|
147
|
+
# Move the pointer ahead one character and return it.
|
|
162
148
|
def advance_character
|
|
163
149
|
character = @source[@current]
|
|
164
150
|
@current += 1
|
|
165
151
|
character
|
|
166
152
|
end
|
|
167
153
|
|
|
154
|
+
# Emit a token.
|
|
168
155
|
def add_token(type, literal = nil)
|
|
169
156
|
text = @source[@start...@current]
|
|
170
157
|
@tokens << Lox::Token.new(type, text, literal, @line)
|
|
171
158
|
end
|
|
172
159
|
|
|
160
|
+
# Move the pointer ahead if character matches expected character; error otherwise.
|
|
173
161
|
def match(expected)
|
|
174
162
|
return false unless @source[@current] == expected || end_of_source?
|
|
175
163
|
|
data/lib/loxby/version.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
+
# This visitor prints a given AST
|
|
6
|
+
# for easier viewing and debugging.
|
|
5
7
|
class ASTPrinter < Visitor
|
|
6
8
|
def print(expression)
|
|
7
9
|
expression.accept self
|
|
@@ -34,4 +36,20 @@ class ASTPrinter < Visitor
|
|
|
34
36
|
def visit_unary_expression(expr)
|
|
35
37
|
parenthesize expr.operator.lexeme, expr.right
|
|
36
38
|
end
|
|
39
|
+
|
|
40
|
+
def visit_assign_expression(expr)
|
|
41
|
+
parenthesize 'assign', expr.name, expr.value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def visit_call_expression(expr)
|
|
45
|
+
parenthesize 'call', expr.callee, expr.arguments
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def visit_logical_expression(expr)
|
|
49
|
+
visit_binary_expression(expr)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def visit_variable_expression(expr)
|
|
53
|
+
parenthesize 'var', expr.name
|
|
54
|
+
end
|
|
37
55
|
end
|
data/lib/loxby/visitors/base.rb
CHANGED
data/lib/loxby.rb
CHANGED
|
File without changes
|