loxxy 0.2.00 → 0.2.05
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +47 -11
- data/CHANGELOG.md +81 -0
- data/README.md +188 -90
- data/bin/loxxy +54 -6
- data/lib/loxxy.rb +1 -0
- data/lib/loxxy/ast/all_lox_nodes.rb +0 -1
- data/lib/loxxy/ast/ast_builder.rb +27 -4
- data/lib/loxxy/ast/ast_visitee.rb +53 -0
- data/lib/loxxy/ast/ast_visitor.rb +2 -10
- data/lib/loxxy/ast/lox_assign_expr.rb +1 -5
- data/lib/loxxy/ast/lox_binary_expr.rb +1 -6
- data/lib/loxxy/ast/lox_block_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_call_expr.rb +1 -5
- data/lib/loxxy/ast/lox_class_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_fun_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_get_expr.rb +1 -6
- data/lib/loxxy/ast/lox_grouping_expr.rb +1 -5
- data/lib/loxxy/ast/lox_if_stmt.rb +2 -6
- data/lib/loxxy/ast/lox_literal_expr.rb +1 -5
- data/lib/loxxy/ast/lox_logical_expr.rb +1 -6
- data/lib/loxxy/ast/lox_node.rb +9 -1
- data/lib/loxxy/ast/lox_print_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_return_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_seq_decl.rb +1 -5
- data/lib/loxxy/ast/lox_set_expr.rb +1 -5
- data/lib/loxxy/ast/lox_super_expr.rb +2 -6
- data/lib/loxxy/ast/lox_this_expr.rb +1 -5
- data/lib/loxxy/ast/lox_unary_expr.rb +1 -6
- data/lib/loxxy/ast/lox_var_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_variable_expr.rb +1 -5
- data/lib/loxxy/ast/lox_while_stmt.rb +2 -6
- data/lib/loxxy/back_end/engine.rb +13 -22
- data/lib/loxxy/back_end/lox_instance.rb +1 -1
- data/lib/loxxy/back_end/resolver.rb +1 -12
- data/lib/loxxy/cli_parser.rb +68 -0
- data/lib/loxxy/error.rb +3 -0
- data/lib/loxxy/front_end/parser.rb +1 -1
- data/lib/loxxy/front_end/scanner.rb +37 -12
- data/lib/loxxy/version.rb +1 -1
- data/loxxy.gemspec +5 -1
- data/spec/front_end/scanner_spec.rb +41 -0
- data/spec/interpreter_spec.rb +23 -0
- metadata +8 -4
- 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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
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
|
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
|
180
|
-
|
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
|
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
|
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
|
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
|
@@ -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
|
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
|
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
|
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
|
-
|
60
|
-
|
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
|
118
|
-
tok_type = keyw
|
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
|
-
|
122
|
-
|
123
|
-
|
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
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 =
|
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
|