loxxy 0.2.00 → 0.2.05

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +47 -11
  3. data/CHANGELOG.md +81 -0
  4. data/README.md +188 -90
  5. data/bin/loxxy +54 -6
  6. data/lib/loxxy.rb +1 -0
  7. data/lib/loxxy/ast/all_lox_nodes.rb +0 -1
  8. data/lib/loxxy/ast/ast_builder.rb +27 -4
  9. data/lib/loxxy/ast/ast_visitee.rb +53 -0
  10. data/lib/loxxy/ast/ast_visitor.rb +2 -10
  11. data/lib/loxxy/ast/lox_assign_expr.rb +1 -5
  12. data/lib/loxxy/ast/lox_binary_expr.rb +1 -6
  13. data/lib/loxxy/ast/lox_block_stmt.rb +1 -5
  14. data/lib/loxxy/ast/lox_call_expr.rb +1 -5
  15. data/lib/loxxy/ast/lox_class_stmt.rb +1 -5
  16. data/lib/loxxy/ast/lox_fun_stmt.rb +1 -5
  17. data/lib/loxxy/ast/lox_get_expr.rb +1 -6
  18. data/lib/loxxy/ast/lox_grouping_expr.rb +1 -5
  19. data/lib/loxxy/ast/lox_if_stmt.rb +2 -6
  20. data/lib/loxxy/ast/lox_literal_expr.rb +1 -5
  21. data/lib/loxxy/ast/lox_logical_expr.rb +1 -6
  22. data/lib/loxxy/ast/lox_node.rb +9 -1
  23. data/lib/loxxy/ast/lox_print_stmt.rb +1 -5
  24. data/lib/loxxy/ast/lox_return_stmt.rb +1 -5
  25. data/lib/loxxy/ast/lox_seq_decl.rb +1 -5
  26. data/lib/loxxy/ast/lox_set_expr.rb +1 -5
  27. data/lib/loxxy/ast/lox_super_expr.rb +2 -6
  28. data/lib/loxxy/ast/lox_this_expr.rb +1 -5
  29. data/lib/loxxy/ast/lox_unary_expr.rb +1 -6
  30. data/lib/loxxy/ast/lox_var_stmt.rb +1 -5
  31. data/lib/loxxy/ast/lox_variable_expr.rb +1 -5
  32. data/lib/loxxy/ast/lox_while_stmt.rb +2 -6
  33. data/lib/loxxy/back_end/engine.rb +13 -22
  34. data/lib/loxxy/back_end/lox_instance.rb +1 -1
  35. data/lib/loxxy/back_end/resolver.rb +1 -12
  36. data/lib/loxxy/cli_parser.rb +68 -0
  37. data/lib/loxxy/error.rb +3 -0
  38. data/lib/loxxy/front_end/parser.rb +1 -1
  39. data/lib/loxxy/front_end/scanner.rb +37 -12
  40. data/lib/loxxy/version.rb +1 -1
  41. data/loxxy.gemspec +5 -1
  42. data/spec/front_end/scanner_spec.rb +41 -0
  43. data/spec/interpreter_spec.rb +23 -0
  44. metadata +8 -4
  45. data/lib/loxxy/ast/lox_for_stmt.rb +0 -41
@@ -18,11 +18,7 @@ module Loxxy
18
18
  @object = anObject
19
19
  end
20
20
 
21
- # Part of the 'visitee' role in Visitor design pattern.
22
- # @param visitor [ASTVisitor] the visitor
23
- def accept(visitor)
24
- visitor.visit_set_expr(self)
25
- end
21
+ define_accept # Add `accept` method as found in Visitor design pattern
26
22
  end # class
27
23
  end # module
28
24
  end # module
@@ -18,17 +18,13 @@ module Loxxy
18
18
  @property = aMethodName
19
19
  end
20
20
 
21
- # Part of the 'visitee' role in Visitor design pattern.
22
- # @param visitor [ASTVisitor] the visitor
23
- def accept(visitor)
24
- visitor.visit_super_expr(self)
25
- end
26
-
27
21
  # Quack like a LoxVariableExpr
28
22
  # @return [String] the `super` keyword
29
23
  def name
30
24
  'super'
31
25
  end
26
+
27
+ define_accept # Add `accept` method as found in Visitor design pattern
32
28
  alias callee= object=
33
29
  end # class
34
30
  end # module
@@ -12,11 +12,7 @@ module Loxxy
12
12
  'this'
13
13
  end
14
14
 
15
- # Part of the 'visitee' role in Visitor design pattern.
16
- # @param _visitor [LoxxyTreeVisitor] the visitor
17
- def accept(aVisitor)
18
- aVisitor.visit_this_expr(self)
19
- end
15
+ define_accept # Add `accept` method as found in Visitor design pattern
20
16
  end # class
21
17
  end # module
22
18
  end # module
@@ -15,12 +15,7 @@ module Loxxy
15
15
  @operator = anOperator
16
16
  end
17
17
 
18
- # Part of the 'visitee' role in Visitor design pattern.
19
- # @param visitor [Ast::ASTVisitor] the visitor
20
- def accept(visitor)
21
- visitor.visit_unary_expr(self)
22
- end
23
-
18
+ define_accept # Add `accept` method as found in Visitor design pattern
24
19
  alias operands subnodes
25
20
  end # class
26
21
  end # module
@@ -18,11 +18,7 @@ module Loxxy
18
18
  @name = aName
19
19
  end
20
20
 
21
- # Part of the 'visitee' role in Visitor design pattern.
22
- # @param visitor [Ast::ASTVisitor] the visitor
23
- def accept(visitor)
24
- visitor.visit_var_stmt(self)
25
- end
21
+ define_accept # Add `accept` method as found in Visitor design pattern
26
22
  end # class
27
23
  end # module
28
24
  end # module
@@ -16,11 +16,7 @@ module Loxxy
16
16
  @name = aName
17
17
  end
18
18
 
19
- # Part of the 'visitee' role in Visitor design pattern.
20
- # @param visitor [Ast::ASTVisitor] the visitor
21
- def accept(visitor)
22
- visitor.visit_variable_expr(self)
23
- end
19
+ define_accept # Add `accept` method as found in Visitor design pattern
24
20
  end # class
25
21
  end # module
26
22
  end # module
@@ -16,17 +16,13 @@ module Loxxy
16
16
  @body = theBody
17
17
  end
18
18
 
19
- # Part of the 'visitee' role in Visitor design pattern.
20
- # @param visitor [Ast::ASTVisitor] the visitor
21
- def accept(visitor)
22
- visitor.visit_while_stmt(self)
23
- end
24
-
25
19
  # Accessor to the condition expression
26
20
  # @return [LoxNode]
27
21
  def condition
28
22
  subnodes[0]
29
23
  end
24
+
25
+ define_accept # Add `accept` method as found in Visitor design pattern
30
26
  end # class
31
27
  end # module
32
28
  end # module
@@ -74,7 +74,7 @@ module Loxxy
74
74
  aClassStmt.superclass.accept(aVisitor)
75
75
  parent = stack.pop
76
76
  unless parent.kind_of?(LoxClass)
77
- raise StandardError, 'Superclass must be a class.'
77
+ raise Loxxy::RuntimeError, 'Superclass must be a class.'
78
78
  end
79
79
  else
80
80
  parent = nil
@@ -116,19 +116,6 @@ module Loxxy
116
116
  before_block_stmt(aForStmt)
117
117
  end
118
118
 
119
- def after_for_stmt(aForStmt, aVisitor)
120
- loop do
121
- aForStmt.test_expr.accept(aVisitor)
122
- condition = stack.pop
123
- break unless condition.truthy?
124
-
125
- aForStmt.body_stmt.accept(aVisitor)
126
- aForStmt.update_expr&.accept(aVisitor)
127
- stack.pop
128
- end
129
- after_block_stmt(aForStmt)
130
- end
131
-
132
119
  def after_if_stmt(anIfStmt, aVisitor)
133
120
  # Retrieve the result of the condition evaluation
134
121
  condition = stack.pop
@@ -154,7 +141,7 @@ module Loxxy
154
141
  break unless condition.truthy?
155
142
 
156
143
  aWhileStmt.body.accept(aVisitor)
157
- aWhileStmt.condition.accept(aVisitor)
144
+ aWhileStmt.condition&.accept(aVisitor)
158
145
  end
159
146
  end
160
147
 
@@ -170,22 +157,26 @@ module Loxxy
170
157
  def after_assign_expr(anAssignExpr, _visitor)
171
158
  var_name = anAssignExpr.name
172
159
  variable = variable_lookup(anAssignExpr)
173
- raise StandardError, "Unknown variable #{var_name}" unless variable
160
+ raise Loxxy::RuntimeError, "Undefined variable '#{var_name}'." unless variable
174
161
 
175
162
  value = stack.last # ToS remains since an assignment produces a value
176
163
  variable.assign(value)
177
164
  end
178
165
 
179
- def after_set_expr(aSetExpr, aVisitor)
180
- value = stack.pop
181
- # Evaluate object part
166
+ def before_set_expr(aSetExpr, aVisitor)
167
+ # Evaluate receiver object part
182
168
  aSetExpr.object.accept(aVisitor)
169
+ end
170
+
171
+ def after_set_expr(aSetExpr, _visitor)
172
+ value = stack.pop
183
173
  assignee = stack.pop
184
174
  unless assignee.kind_of?(LoxInstance)
185
- raise StandardError, 'Only instances have fields.'
175
+ raise Loxxy::RuntimeError, 'Only instances have fields.'
186
176
  end
187
177
 
188
178
  assignee.set(aSetExpr.property, value)
179
+ stack.push value
189
180
  end
190
181
 
191
182
  def after_logical_expr(aLogicalExpr, visitor)
@@ -272,7 +263,7 @@ module Loxxy
272
263
  aGetExpr.object.accept(aVisitor)
273
264
  instance = stack.pop
274
265
  unless instance.kind_of?(LoxInstance)
275
- raise StandardError, 'Only instances have properties.'
266
+ raise Loxxy::RuntimeError, 'Only instances have properties.'
276
267
  end
277
268
 
278
269
  stack.push instance.get(aGetExpr.property)
@@ -285,7 +276,7 @@ module Loxxy
285
276
  def after_variable_expr(aVarExpr, aVisitor)
286
277
  var_name = aVarExpr.name
287
278
  var = variable_lookup(aVarExpr)
288
- raise StandardError, "Undefined variable '#{var_name}'." unless var
279
+ raise Loxxy::RuntimeError, "Undefined variable '#{var_name}'." unless var
289
280
 
290
281
  var.value.accept(aVisitor) # Evaluate variable value then push on stack
291
282
  end
@@ -38,7 +38,7 @@ module Loxxy
38
38
 
39
39
  method = klass.find_method(aName)
40
40
  unless method
41
- raise StandardError, "Undefined property '#{aName}'."
41
+ raise Loxxy::RuntimeError, "Undefined property '#{aName}'."
42
42
  end
43
43
 
44
44
  method.bind(self)
@@ -88,17 +88,6 @@ module Loxxy
88
88
  @current_class = previous_class
89
89
  end
90
90
 
91
- def before_for_stmt(aForStmt)
92
- before_block_stmt(aForStmt)
93
- end
94
-
95
- def after_for_stmt(aForStmt, aVisitor)
96
- aForStmt.test_expr.accept(aVisitor)
97
- aForStmt.body_stmt.accept(aVisitor)
98
- aForStmt.update_expr&.accept(aVisitor)
99
- after_block_stmt(aForStmt)
100
- end
101
-
102
91
  def after_if_stmt(anIfStmt, aVisitor)
103
92
  anIfStmt.then_stmt.accept(aVisitor)
104
93
  anIfStmt.else_stmt&.accept(aVisitor)
@@ -123,7 +112,7 @@ module Loxxy
123
112
 
124
113
  def after_while_stmt(aWhileStmt, aVisitor)
125
114
  aWhileStmt.body.accept(aVisitor)
126
- aWhileStmt.condition.accept(aVisitor)
115
+ aWhileStmt.condition&.accept(aVisitor)
127
116
  end
128
117
 
129
118
  # A variable declaration adds a new variable to current scope
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse' # Use standard OptionParser class for command-line parsing
4
+
5
+ module Loxxy
6
+ # A command-line option parser for the Loxxy interpreter.
7
+ # It is a specialisation of the OptionParser class.
8
+ class CLIParser < OptionParser
9
+ # @return [Hash{Symbol=>String, Array}]
10
+ attr_reader(:parsed_options)
11
+
12
+ # Constructor.
13
+ def initialize(prog_name, ver)
14
+ super()
15
+ reset(prog_name, ver)
16
+
17
+ heading
18
+ separator 'Options:'
19
+ separator ''
20
+ add_tail_options
21
+ end
22
+
23
+ def parse!(args)
24
+ super
25
+ parsed_options
26
+ end
27
+
28
+ private
29
+
30
+ def reset(prog_name, ver)
31
+ @program_name = prog_name
32
+ @version = ver
33
+ @banner = "Usage: #{prog_name} LOX_FILE [options]"
34
+ @parsed_options = {}
35
+ end
36
+
37
+ def description
38
+ <<-DESCR
39
+ Description:
40
+ loxxy is a Lox interpreter, it executes the Lox file(s) given in command-line.
41
+ More on Lox Language: https://craftinginterpreters.com/the-lox-language.html
42
+
43
+ Example:
44
+ #{program_name} hello.lox
45
+ DESCR
46
+ end
47
+
48
+ def heading
49
+ banner
50
+ separator ''
51
+ separator description
52
+ separator ''
53
+ end
54
+
55
+ def add_tail_options
56
+ on_tail('--version', 'Display the program version then quit.') do
57
+ puts version
58
+ exit(0)
59
+ end
60
+
61
+ on_tail('-?', '-h', '--help', 'Display this help then quit.') do
62
+ puts help
63
+ exit(0)
64
+ end
65
+ end
66
+ end # class
67
+ end # module
68
+ # End of file
data/lib/loxxy/error.rb CHANGED
@@ -7,6 +7,9 @@ module Loxxy
7
7
  # Error occurring while Loxxy executes some invalid Lox code.
8
8
  class RuntimeError < Error; end
9
9
 
10
+ # Error occurring while Loxxy scans invalid input.
11
+ class ScanError < Error; end
12
+
10
13
  # Error occurring while Loxxy parses some invalid Lox code.
11
14
  class SyntaxError < Error; end
12
15
  end
@@ -46,7 +46,7 @@ module Loxxy
46
46
  # Stop if the parse failed...
47
47
  line1 = "Parsing failed\n"
48
48
  line2 = "Reason: #{result.failure_reason.message}"
49
- raise StandardError, line1 + line2
49
+ raise SyntaxError, line1 + line2
50
50
  end
51
51
 
52
52
  return engine.convert(result) # engine.to_ptree(result)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'strscan'
4
4
  require 'rley'
5
+ require_relative '../error'
5
6
  require_relative '../datatype/all_datatypes'
6
7
  require_relative 'literal'
7
8
 
@@ -53,15 +54,13 @@ module Loxxy
53
54
  '<=' => 'LESS_EQUAL'
54
55
  }.freeze
55
56
 
56
- # Here are all the implemented Lox keywords (in uppercase)
57
+ # Here are all the implemented Lox keywords
57
58
  # These are enumerated in section 4.2.1 Token type
58
59
  @@keywords = %w[
59
- AND CLASS ELSE FALSE FUN FOR IF NIL OR
60
- PRINT RETURN SUPER THIS TRUE VAR WHILE
60
+ and class else false fun for if nil or
61
+ print return super this true var while
61
62
  ].map { |x| [x, x] }.to_h
62
63
 
63
- class ScanError < StandardError; end
64
-
65
64
  # Constructor. Initialize a tokenizer for Lox input.
66
65
  # @param source [String] Lox text to tokenize.
67
66
  def initialize(source = nil)
@@ -114,14 +113,17 @@ module Loxxy
114
113
  elsif (lexeme = scanner.scan(/"(?:\\"|[^"])*"/))
115
114
  token = build_token('STRING', lexeme)
116
115
  elsif (lexeme = scanner.scan(/[a-zA-Z_][a-zA-Z_0-9]*/))
117
- keyw = @@keywords[lexeme.upcase]
118
- tok_type = keyw || 'IDENTIFIER'
116
+ keyw = @@keywords[lexeme]
117
+ tok_type = keyw ? keyw.upcase : 'IDENTIFIER'
119
118
  token = build_token(tok_type, lexeme)
119
+ elsif scanner.scan(/"(?:\\"|[^"])*\z/)
120
+ # Error: unterminated string...
121
+ col = scanner.pos - @line_start + 1
122
+ raise ScanError, "Error: [line #{lineno}:#{col}]: Unterminated string."
120
123
  else # Unknown token
121
- erroneous = curr_ch.nil? ? '' : scanner.scan(/./)
122
- sequel = scanner.scan(/.{1,20}/)
123
- erroneous += sequel unless sequel.nil?
124
- raise ScanError, "Unknown token #{erroneous} on line #{lineno}"
124
+ col = scanner.pos - @line_start + 1
125
+ _erroneous = curr_ch.nil? ? '' : scanner.scan(/./)
126
+ raise ScanError, "Error: [line #{lineno}:#{col}]: Unexpected character."
125
127
  end
126
128
 
127
129
  return token
@@ -156,7 +158,7 @@ module Loxxy
156
158
  when 'NUMBER'
157
159
  value = Datatype::Number.new(aLexeme)
158
160
  when 'STRING'
159
- value = Datatype::LXString.new(aLexeme)
161
+ value = Datatype::LXString.new(unescape_string(aLexeme))
160
162
  when 'TRUE'
161
163
  value = Datatype::True.instance
162
164
  else
@@ -166,6 +168,29 @@ module Loxxy
166
168
  return [value, symb]
167
169
  end
168
170
 
171
+ # Replace any sequence sequence by their "real" value.
172
+ def unescape_string(aText)
173
+ result = +''
174
+ previous = nil
175
+
176
+ aText.each_char do |ch|
177
+ if previous
178
+ if ch == ?n
179
+ result << "\n"
180
+ else
181
+ result << ch
182
+ end
183
+ previous = nil
184
+ elsif ch == '\\'
185
+ previous = ?\
186
+ else
187
+ result << ch
188
+ end
189
+ end
190
+
191
+ result
192
+ end
193
+
169
194
  # Skip non-significant whitespaces and comments.
170
195
  # Advance the scanner until something significant is found.
171
196
  def skip_intertoken_spaces
data/lib/loxxy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Loxxy
4
- VERSION = '0.2.00'
4
+ VERSION = '0.2.05'
5
5
  end
data/loxxy.gemspec CHANGED
@@ -41,7 +41,11 @@ Gem::Specification.new do |spec|
41
41
  spec.authors = ['Dimitri Geshef']
42
42
  spec.email = ['famished.tiger@yahoo.com']
43
43
  spec.summary = 'An implementation of the Lox programming language.'
44
- spec.description = 'An implementation of the Lox programming language.'
44
+ spec.description = <<-DESCR_END
45
+ A Ruby implementation of the Lox programming language. Lox is a dynamically typed,
46
+ object-oriented programming language that features first-class functions, closures,
47
+ classes, and inheritance.
48
+ DESCR_END
45
49
  spec.homepage = 'https://github.com/famished-tiger/loxxy'
46
50
  spec.license = 'MIT'
47
51
  spec.required_ruby_version = '~> 2.4'
@@ -189,6 +189,26 @@ LOX_END
189
189
  end
190
190
  end
191
191
 
192
+ it 'should recognize escaped quotes' do
193
+ embedded_quotes = %q{she said: \"Hello\"}
194
+ result = subject.send(:unescape_string, embedded_quotes)
195
+ expect(result).to eq('she said: "Hello"')
196
+ end
197
+
198
+ it 'should recognize escaped backslash' do
199
+ embedded_backslash = 'backslash>\\\\'
200
+ result = subject.send(:unescape_string, embedded_backslash)
201
+ expect(result).to eq('backslash>\\')
202
+ end
203
+
204
+ # rubocop: disable Style/StringConcatenation
205
+ it 'should recognize newline escape sequence' do
206
+ embedded_newline = 'line1\\nline2'
207
+ result = subject.send(:unescape_string, embedded_newline)
208
+ expect(result).to eq('line1' + "\n" + 'line2')
209
+ end
210
+ # rubocop: enable Style/StringConcatenation
211
+
192
212
  it 'should recognize a nil token' do
193
213
  subject.start_with('nil')
194
214
  token_nil = subject.tokens[0]
@@ -197,6 +217,13 @@ LOX_END
197
217
  expect(token_nil.lexeme).to eq('nil')
198
218
  expect(token_nil.value).to be_kind_of(Datatype::Nil)
199
219
  end
220
+
221
+ it 'should differentiate nil from variable spelled same' do
222
+ subject.start_with('Nil')
223
+ similar = subject.tokens[0]
224
+ expect(similar.terminal).to eq('IDENTIFIER')
225
+ expect(similar.lexeme).to eq('Nil')
226
+ end
200
227
  end # context
201
228
 
202
229
  context 'Handling comments:' do
@@ -237,6 +264,20 @@ LOX_END
237
264
  ]
238
265
  match_expectations(subject, expectations)
239
266
  end
267
+
268
+ it 'should complain if it finds an unterminated string' do
269
+ subject.start_with('var a = "Unfinished;')
270
+ err = Loxxy::ScanError
271
+ err_msg = 'Error: [line 1:21]: Unterminated string.'
272
+ expect { subject.tokens }.to raise_error(err, err_msg)
273
+ end
274
+
275
+ it 'should complain if it finds an unexpected character' do
276
+ subject.start_with('var a = ?1?;')
277
+ err = Loxxy::ScanError
278
+ err_msg = 'Error: [line 1:9]: Unexpected character.'
279
+ expect { subject.tokens }.to raise_error(err, err_msg)
280
+ end
240
281
  end # context
241
282
  end # describe
242
283
  end # module