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.
@@ -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
- @environment = Lox::Environment.new
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 by definition. This does nothing.
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 the Visitor
11
- # class in visitors/base.rb.)
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?(:var)
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? :print
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 << declaration until check(:right_brace) || end_of_input?
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 (ternary operator)."
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 = equality
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
- primary
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
- trap('SIGINT') { exit } # Exit cleanly on Ctrl-C
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.rb [script]'
21
- exit 64
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
- 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
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, "", nil, @line)
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 :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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Loxby
4
- VERSION = '0.0.2'
3
+ class Lox
4
+ VERSION = '0.0.3'
5
5
  end
@@ -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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Visitable adds #accept, the only
3
+ # Visitable adds `#accept`, the only
4
4
  # method required to implement the
5
5
  # visitor pattern on a class.
6
6
  # To use the visitor pattern,
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative 'base'
4
4
 
5
+ # This visitor converts single expression
6
+ # ASTs to Reverse Polish Notation.
5
7
  class RPNConverter < Visitor
6
8
  def print(expr)
7
9
  expr.accept self
data/lib/loxby.rb CHANGED
File without changes