loxxy 0.1.15 → 0.2.02

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.
@@ -85,7 +85,7 @@ module Loxxy
85
85
  name2envs[name] = [current_env]
86
86
  end
87
87
 
88
- anEntry.name # anEntry.i_name
88
+ anEntry.name
89
89
  end
90
90
 
91
91
  # Search for the object with the given name
@@ -99,23 +99,6 @@ module Loxxy
99
99
  sc.defns[aName]
100
100
  end
101
101
 
102
- # Search for the object with the given i_name
103
- # @param anIName [String]
104
- # @return [BackEnd::Variable]
105
- # def lookup_i_name(anIName)
106
- # found = nil
107
- # environment = current_env
108
-
109
- # begin
110
- # found = environment.defns.values.find { |e| e.i_name == anIName }
111
- # break if found
112
-
113
- # environment = environment.parent
114
- # end while environment
115
-
116
- # found
117
- # end
118
-
119
102
  # Return all variables defined in the current .. root chain.
120
103
  # Variables are sorted top-down and left-to-right.
121
104
  def all_variables
@@ -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
@@ -38,7 +38,7 @@ module Loxxy
38
38
  rule('declaration' => 'stmt')
39
39
 
40
40
  rule('classDecl' => 'CLASS classNaming class_body').as 'class_decl'
41
- rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
41
+ rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER').as 'class_subclassing'
42
42
  rule('classNaming' => 'IDENTIFIER').as 'class_name'
43
43
  rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE').as 'class_body'
44
44
  rule('methods_opt' => 'method_plus')
@@ -143,7 +143,7 @@ module Loxxy
143
143
  rule('primary' => 'STRING').as 'literal_expr'
144
144
  rule('primary' => 'IDENTIFIER').as 'variable_expr'
145
145
  rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN').as 'grouping_expr'
146
- rule('primary' => 'SUPER DOT IDENTIFIER')
146
+ rule('primary' => 'SUPER DOT IDENTIFIER').as 'super_expr'
147
147
 
148
148
  # Utility rules
149
149
  rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
@@ -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
 
@@ -60,8 +61,6 @@ module Loxxy
60
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)
@@ -117,11 +116,14 @@ module Loxxy
117
116
  keyw = @@keywords[lexeme.upcase]
118
117
  tok_type = keyw || '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
@@ -24,6 +24,15 @@ module Loxxy
24
24
  # @param lox_input [String] Lox program to evaluate
25
25
  # @return [Loxxy::Datatype::BuiltinDatatype]
26
26
  def evaluate(lox_input)
27
+ raw_evaluate(lox_input).first
28
+ end
29
+
30
+ # Evaluate the given Lox program.
31
+ # Return the pair [result, a BackEnd::Engine instance]
32
+ # where result is the value of the last executed expression (if any)
33
+ # @param lox_input [String] Lox program to evaluate
34
+ # @return Loxxy::Datatype::BuiltinDatatype, Loxxy::BackEnd::Engine]
35
+ def raw_evaluate(lox_input)
27
36
  # Front-end scans, parses the input and blurps an AST...
28
37
  parser = FrontEnd::Parser.new
29
38
 
@@ -34,7 +43,9 @@ module Loxxy
34
43
  # Back-end launches the tree walking & responds to visit events
35
44
  # by executing the code determined by the visited AST node.
36
45
  engine = BackEnd::Engine.new(config)
37
- engine.execute(visitor)
46
+ result = engine.execute(visitor)
47
+
48
+ [result, engine]
38
49
  end
39
50
  end # class
40
51
  end # module
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.1.15'
4
+ VERSION = '0.2.02'
5
5
  end
data/loxxy.gemspec CHANGED
@@ -40,8 +40,12 @@ Gem::Specification.new do |spec|
40
40
  spec.version = Loxxy::VERSION
41
41
  spec.authors = ['Dimitri Geshef']
42
42
  spec.email = ['famished.tiger@yahoo.com']
43
- spec.summary = 'An implementation of the Lox programming language. WIP'
44
- spec.description = 'An implementation of the Lox programming language. WIP'
43
+ spec.summary = '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'
@@ -34,15 +34,7 @@ module Loxxy
34
34
  let(:var_decl) { Ast::LoxVarStmt.new(sample_pos, 'greeting', greeting) }
35
35
  let(:lit_expr) { Ast::LoxLiteralExpr.new(sample_pos, greeting) }
36
36
 
37
- it "should react to 'before_var_stmt' event" do
38
- expect { subject.before_var_stmt(var_decl) }.not_to raise_error
39
- current_env = subject.symbol_table.current_env
40
- expect(current_env.defns['greeting']).to be_kind_of(Variable)
41
- end
42
-
43
37
  it "should react to 'after_var_stmt' event" do
44
- # Precondition: `before_var_stmt` is called...
45
- expect { subject.before_var_stmt(var_decl) }.not_to raise_error
46
38
  # Precondition: value to assign is on top of stack
47
39
  subject.stack.push(greeting)
48
40
 
@@ -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]
@@ -237,6 +257,20 @@ LOX_END
237
257
  ]
238
258
  match_expectations(subject, expectations)
239
259
  end
260
+
261
+ it 'should complain if it finds an unterminated string' do
262
+ subject.start_with('var a = "Unfinished;')
263
+ err = Loxxy::ScanError
264
+ err_msg = 'Error: [line 1:21]: Unterminated string.'
265
+ expect { subject.tokens }.to raise_error(err, err_msg)
266
+ end
267
+
268
+ it 'should complain if it finds an unexpected character' do
269
+ subject.start_with('var a = ?1?;')
270
+ err = Loxxy::ScanError
271
+ err_msg = 'Error: [line 1:9]: Unexpected character.'
272
+ expect { subject.tokens }.to raise_error(err, err_msg)
273
+ end
240
274
  end # context
241
275
  end # describe
242
276
  end # module
@@ -6,6 +6,7 @@ require 'stringio'
6
6
  # Load the class under test
7
7
  require_relative '../lib/loxxy/interpreter'
8
8
 
9
+ # rubocop: disable Metrics/ModuleLength
9
10
  module Loxxy
10
11
  # This spec contains the bare bones test for the Interpreter class.
11
12
  # The execution of Lox code is tested elsewhere.
@@ -525,7 +526,100 @@ LOX_END
525
526
  expect { subject.evaluate(program) }.not_to raise_error
526
527
  expect(sample_cfg[:ostream].string).to eq('Egotist instance')
527
528
  end
529
+
530
+ it 'should support a closure nested in a method' do
531
+ lox_snippet = <<-LOX_END
532
+ class Foo {
533
+ getClosure() {
534
+ fun closure() {
535
+ return this.toString();
536
+ }
537
+ return closure;
538
+ }
539
+
540
+ toString() { return "foo"; }
541
+ }
542
+
543
+ var closure = Foo().getClosure();
544
+ closure;
545
+ LOX_END
546
+ # Expected result: Backend::LoxFunction('closure')
547
+ # Expected function's closure (environment layout):
548
+ # Environment('global')
549
+ # defns
550
+ # +- ['clock'] => BackEnd::Engine::NativeFunction
551
+ # *- ['Foo'] => BackEnd::LoxClass
552
+ # Environment
553
+ # defns
554
+ # ['this'] => BackEnd::LoxInstance
555
+ # Environment
556
+ # defns
557
+ # +- ['closure'] => Backend::LoxFunction
558
+ result = subject.evaluate(lox_snippet)
559
+ expect(result).to be_kind_of(BackEnd::LoxFunction)
560
+ expect(result.name).to eq('closure')
561
+ closure = result.closure
562
+ expect(closure).to be_kind_of(Loxxy::BackEnd::Environment)
563
+ expect(closure.defns['closure'].value).to eq(result)
564
+ expect(closure.enclosing).to be_kind_of(Loxxy::BackEnd::Environment)
565
+ expect(closure.enclosing.defns['this'].value).to be_kind_of(Loxxy::BackEnd::LoxInstance)
566
+ global_env = closure.enclosing.enclosing
567
+ expect(global_env).to be_kind_of(Loxxy::BackEnd::Environment)
568
+ expect(global_env.defns['clock'].value).to be_kind_of(BackEnd::Engine::NativeFunction)
569
+ expect(global_env.defns['Foo'].value).to be_kind_of(BackEnd::LoxClass)
570
+ end
571
+
572
+ it 'should support custom initializer' do
573
+ lox_snippet = <<-LOX_END
574
+ // From section 3.9.5
575
+ class Breakfast {
576
+ init(meat, bread) {
577
+ this.meat = meat;
578
+ this.bread = bread;
579
+ }
580
+
581
+ serve(who) {
582
+ print "Enjoy your " + this.meat + " and " +
583
+ this.bread + ", " + who + ".";
584
+ }
585
+ }
586
+
587
+ var baconAndToast = Breakfast("bacon", "toast");
588
+ baconAndToast.serve("Dear Reader");
589
+ // Output: "Enjoy your bacon and toast, Dear Reader."
590
+ LOX_END
591
+ expect { subject.evaluate(lox_snippet) }.not_to raise_error
592
+ predicted = 'Enjoy your bacon and toast, Dear Reader.'
593
+ expect(sample_cfg[:ostream].string).to eq(predicted)
594
+ end
595
+
596
+ it 'should support class inheritance and super keyword' do
597
+ lox_snippet = <<-LOX_END
598
+ class A {
599
+ method() {
600
+ print "A method";
601
+ }
602
+ }
603
+
604
+ class B < A {
605
+ method() {
606
+ print "B method";
607
+ }
608
+
609
+ test() {
610
+ super.method();
611
+ }
612
+ }
613
+
614
+ class C < B {}
615
+
616
+ C().test();
617
+ LOX_END
618
+ expect { subject.evaluate(lox_snippet) }.not_to raise_error
619
+ expect(sample_cfg[:ostream].string).to eq('A method')
620
+ end
528
621
  end # context
529
622
  end # describe
530
623
  # rubocop: enable Metrics/BlockLength
531
624
  end # module
625
+ # rubocop: enable Metrics/ModuleLength
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loxxy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.15
4
+ version: 0.2.02
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dimitri Geshef
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-08 00:00:00.000000000 Z
11
+ date: 2021-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -66,7 +66,10 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.0'
69
- description: An implementation of the Lox programming language. WIP
69
+ description: |2
70
+ A Ruby implementation of the Lox programming language. Lox is a dynamically typed,
71
+ object-oriented programming language that features first-class functions, closures,
72
+ classes, and inheritance.
70
73
  email:
71
74
  - famished.tiger@yahoo.com
72
75
  executables:
@@ -108,6 +111,7 @@ files:
108
111
  - lib/loxxy/ast/lox_return_stmt.rb
109
112
  - lib/loxxy/ast/lox_seq_decl.rb
110
113
  - lib/loxxy/ast/lox_set_expr.rb
114
+ - lib/loxxy/ast/lox_super_expr.rb
111
115
  - lib/loxxy/ast/lox_this_expr.rb
112
116
  - lib/loxxy/ast/lox_unary_expr.rb
113
117
  - lib/loxxy/ast/lox_var_stmt.rb
@@ -124,6 +128,7 @@ files:
124
128
  - lib/loxxy/back_end/symbol_table.rb
125
129
  - lib/loxxy/back_end/unary_operator.rb
126
130
  - lib/loxxy/back_end/variable.rb
131
+ - lib/loxxy/cli_parser.rb
127
132
  - lib/loxxy/datatype/all_datatypes.rb
128
133
  - lib/loxxy/datatype/boolean.rb
129
134
  - lib/loxxy/datatype/builtin_datatype.rb
@@ -178,7 +183,7 @@ requirements: []
178
183
  rubygems_version: 3.1.4
179
184
  signing_key:
180
185
  specification_version: 4
181
- summary: An implementation of the Lox programming language. WIP
186
+ summary: An implementation of the Lox programming language.
182
187
  test_files:
183
188
  - spec/back_end/engine_spec.rb
184
189
  - spec/back_end/environment_spec.rb