loxxy 0.1.07 → 0.1.08

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f171f0046ff95784ed21b369751ca65d25e0ff757d1b2dccc534087a4b9c4a8d
4
- data.tar.gz: 579badf6a30436962cb18e63dfee1ed361cfd152e9fecf54f2bfd25395db086b
3
+ metadata.gz: 1724fce16b30fe2328ba31a725e92cc03f43ac7dee60fd54c68642c5a5639471
4
+ data.tar.gz: 0f045add2ebf55375ee63e94f3a7316984588d0133200f3f4ce45de3ae118157
5
5
  SHA512:
6
- metadata.gz: 0470f022b121ceaff279f75024e07b78a218c3baa4d44e6f9bf54f4ebddf18341b27f697fe68c727eb2d61add2c55e7c102fbf73bfd29ef4fb3965024247f747
7
- data.tar.gz: 320dbb075351069f624103f1d93aadf903ec6a5b7bb685871f53227b496760ac67ae9fa1dae85491efd9b94cfeb90eac8f66e104b4f70eaac9b0da081dfde567
6
+ metadata.gz: 5e4a67161f2d6f6a89b237bdda4e1d096df1183eeb3a9b8ff4f1506d8b053bae265de73857faf82a225b77fad5d32faddfe07873d74e0e71a2839f113659c61b
7
+ data.tar.gz: 29d09adcb56231c4f7b7c911c16b978e4ec3e0608f754e8b80b971293b99ea96fcf2bb6cd55dec62a8acac00bde0fb9b93e86cc37ee11024faea8e8eb0a70d3f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.1.08] - 2021-03-27
2
+ - `Loxxy` implements variable resolving and binding as described in Chapter 11 of "Crafting Interpreters" book.
3
+
4
+ ### New
5
+ - Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
6
+
7
+ ### Changed
8
+ - Class `Ast::Visitor` changes in some method signatures
9
+ - Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
10
+ - Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
11
+
1
12
  ## [0.1.07] - 2021-03-14
2
13
  - `Loxxy` now supports nested functions and closures
3
14
 
@@ -12,8 +23,6 @@
12
23
  - Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
13
24
 
14
25
 
15
-
16
-
17
26
  ## [0.1.06] - 2021-03-06
18
27
  - Parameters/arguments checks in function declaration and call
19
28
 
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### What is loxxy?
6
6
  A Ruby implementation of the [Lox programming language](https://craftinginterpreters.com/the-lox-language.html ),
7
- a simple language used in Bob Nystrom's online book [Crafting Interpreters](https://craftinginterpreters.com/ ).
7
+ a simple language defined in Bob Nystrom's online book [Crafting Interpreters](https://craftinginterpreters.com/ ).
8
8
 
9
9
  ### Purpose of this project:
10
10
  - To deliver an open source example of a programming language fully implemented in Ruby
@@ -13,11 +13,10 @@ a simple language used in Bob Nystrom's online book [Crafting Interpreters](http
13
13
  a Lox interpreter written in Lox.
14
14
 
15
15
  ### Current status
16
- The project is still in inception and the interpreter is being implemented...
17
- Currently it can execute all allowed __Lox__ expressions and statements except:
18
- - Classes and objects.
16
+ The interpreter currently it can execute all allowed __Lox__ expressions and statements except
17
+ object-oriented feaures (classes and objects).
19
18
 
20
- These will be implemented soon.
19
+ Our intent is implement to these missing features in Q2 2021.
21
20
 
22
21
 
23
22
  ## What's the fuss about Lox?
@@ -120,7 +120,7 @@ module Loxxy
120
120
  def visit_assign_expr(anAssignExpr)
121
121
  broadcast(:before_assign_expr, anAssignExpr)
122
122
  traverse_subnodes(anAssignExpr)
123
- broadcast(:after_assign_expr, anAssignExpr)
123
+ broadcast(:after_assign_expr, anAssignExpr, self)
124
124
  end
125
125
 
126
126
  # Visit event. The visitor is about to visit a logical expression.
@@ -194,7 +194,7 @@ module Loxxy
194
194
  # Visit event. The visitor is about to visit a function statement node.
195
195
  # @param aFunStmt [AST::LoxFunStmt] function declaration to visit
196
196
  def visit_fun_stmt(aFunStmt)
197
- broadcast(:before_fun_stmt, aFunStmt)
197
+ broadcast(:before_fun_stmt, aFunStmt, self)
198
198
  traverse_subnodes(aFunStmt)
199
199
  broadcast(:after_fun_stmt, aFunStmt, self)
200
200
  end
@@ -20,8 +20,6 @@ module Loxxy
20
20
  def accept(visitor)
21
21
  visitor.visit_block_stmt(self)
22
22
  end
23
-
24
- alias operands subnodes
25
23
  end # class
26
24
  end # module
27
25
  end # module
@@ -26,8 +26,6 @@ module Loxxy
26
26
  def accept(visitor)
27
27
  visitor.visit_fun_stmt(self)
28
28
  end
29
-
30
- alias operands subnodes
31
29
  end # class
32
30
  # rubocop: enable Style/AccessorGrouping
33
31
  end # module
@@ -16,8 +16,6 @@ module Loxxy
16
16
  def accept(visitor)
17
17
  visitor.visit_grouping_expr(self)
18
18
  end
19
-
20
- alias operands subnodes
21
19
  end # class
22
20
  end # module
23
21
  end # module
@@ -10,8 +10,6 @@ module Loxxy
10
10
  def accept(visitor)
11
11
  visitor.visit_seq_decl(self)
12
12
  end
13
-
14
- alias operands subnodes
15
13
  end # class
16
14
  end # module
17
15
  end # module
@@ -13,8 +13,8 @@ module Loxxy
13
13
  # @param aName [String] name of the variable
14
14
  # @param aValue [Loxxy::Ast::LoxNode, NilClass] initial value for the variable
15
15
  def initialize(aPosition, aName, aValue)
16
- initial_value = aValue ? [aValue] : [Datatype::Nil.instance]
17
- super(aPosition, initial_value)
16
+ initial_value = aValue || Datatype::Nil.instance
17
+ super(aPosition, [initial_value])
18
18
  @name = aName
19
19
  end
20
20
 
@@ -4,6 +4,7 @@
4
4
  require_relative '../ast/all_lox_nodes'
5
5
  require_relative 'binary_operator'
6
6
  require_relative 'lox_function'
7
+ require_relative 'resolver'
7
8
  require_relative 'symbol_table'
8
9
  require_relative 'unary_operator'
9
10
 
@@ -11,7 +12,6 @@ module Loxxy
11
12
  module BackEnd
12
13
  # An instance of this class executes the statements as when they
13
14
  # occur during the abstract syntax tree walking.
14
- # @note WIP: very crude implementation.
15
15
  class Engine
16
16
  # @return [Hash] A set of configuration options
17
17
  attr_reader :config
@@ -28,6 +28,9 @@ module Loxxy
28
28
  # @return [Hash { Symbol => BinaryOperator}]
29
29
  attr_reader :binary_operators
30
30
 
31
+ # @return [BackEnd::Resolver]
32
+ attr_reader :resolver
33
+
31
34
  # @param theOptions [Hash]
32
35
  def initialize(theOptions)
33
36
  @config = theOptions
@@ -47,6 +50,10 @@ module Loxxy
47
50
  # @param aVisitor [AST::ASTVisitor]
48
51
  # @return [Loxxy::Datatype::BuiltinDatatype]
49
52
  def execute(aVisitor)
53
+ # Do variable resolution pass first
54
+ @resolver = BackEnd::Resolver.new
55
+ resolver.analyze(aVisitor)
56
+
50
57
  aVisitor.subscribe(self)
51
58
  aVisitor.start
52
59
  aVisitor.unsubscribe(self)
@@ -61,11 +68,20 @@ module Loxxy
61
68
  # Do nothing, subnodes were already evaluated
62
69
  end
63
70
 
64
- def after_var_stmt(aVarStmt)
65
- new_var = Variable.new(aVarStmt.name, stack.pop)
71
+ def before_var_stmt(aVarStmt)
72
+ new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
66
73
  symbol_table.insert(new_var)
67
74
  end
68
75
 
76
+ def after_var_stmt(aVarStmt)
77
+ var_name = aVarStmt.name
78
+ variable = symbol_table.lookup(var_name)
79
+ raise StandardError, "Unknown variable #{var_name}" unless variable
80
+
81
+ value = stack.pop
82
+ variable.assign(value)
83
+ end
84
+
69
85
  def before_for_stmt(aForStmt)
70
86
  before_block_stmt(aForStmt)
71
87
  end
@@ -121,9 +137,9 @@ module Loxxy
121
137
  symbol_table.leave_environment
122
138
  end
123
139
 
124
- def after_assign_expr(anAssignExpr)
140
+ def after_assign_expr(anAssignExpr, _visitor)
125
141
  var_name = anAssignExpr.name
126
- variable = symbol_table.lookup(var_name)
142
+ variable = variable_lookup(anAssignExpr)
127
143
  raise StandardError, "Unknown variable #{var_name}" unless variable
128
144
 
129
145
  value = stack.last # ToS remains since an assignment produces a value
@@ -216,10 +232,10 @@ module Loxxy
216
232
 
217
233
  def after_variable_expr(aVarExpr, aVisitor)
218
234
  var_name = aVarExpr.name
219
- var = symbol_table.lookup(var_name)
235
+ var = variable_lookup(aVarExpr)
220
236
  raise StandardError, "Unknown variable #{var_name}" unless var
221
237
 
222
- var.value.accept(aVisitor) # Evaluate the variable value
238
+ var.value.accept(aVisitor) # Evaluate variable value then push on stack
223
239
  end
224
240
 
225
241
  # @param literalExpr [Ast::LoxLiteralExpr]
@@ -240,6 +256,19 @@ module Loxxy
240
256
 
241
257
  private
242
258
 
259
+ def variable_lookup(aVarNode)
260
+ env = nil
261
+ offset = resolver.locals[aVarNode]
262
+ if offset.nil?
263
+ env = symbol_table.root
264
+ else
265
+ env = symbol_table.current_env
266
+ offset.times { env = env.enclosing }
267
+ end
268
+
269
+ env.defns[aVarNode.name]
270
+ end
271
+
243
272
  NativeFunction = Struct.new(:callable, :interp) do
244
273
  def accept(_visitor)
245
274
  interp.stack.push self
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load all the classes implementing AST nodes
4
+ require_relative '../ast/all_lox_nodes'
5
+ require_relative 'binary_operator'
6
+ require_relative 'lox_function'
7
+ require_relative 'symbol_table'
8
+ require_relative 'unary_operator'
9
+
10
+ module Loxxy
11
+ module BackEnd
12
+ # A class aimed to perform variable resolution when it visits the parse tree.
13
+ # Resolving means retrieve the declaration of a variable/function everywhere it
14
+ # is referenced.
15
+ class Resolver
16
+ # A stack of Hashes of the form String => Boolean
17
+ # @return [Array<Hash{String => Boolean}>]
18
+ attr_reader :scopes
19
+
20
+ # A map from a LoxNode involving a variable and the number of enclosing scopes
21
+ # where it is declared.
22
+ # @return [Hash {LoxNode => Integer}]
23
+ attr_reader :locals
24
+
25
+ def initialize
26
+ @scopes = []
27
+ @locals = {}
28
+ end
29
+
30
+ # Given an abstract syntax parse tree visitor, launch the visit
31
+ # and execute the visit events in the output stream.
32
+ # @param aVisitor [AST::ASTVisitor]
33
+ # @return [Loxxy::Datatype::BuiltinDatatype]
34
+ def analyze(aVisitor)
35
+ begin_scope
36
+ aVisitor.subscribe(self)
37
+ aVisitor.start
38
+ aVisitor.unsubscribe(self)
39
+ end_scope
40
+ end
41
+
42
+ # block statement introduces a new scope
43
+ def before_block_stmt(_aBlockStmt)
44
+ begin_scope
45
+ end
46
+
47
+ def after_block_stmt(_aBlockStmt)
48
+ end_scope
49
+ end
50
+
51
+ def before_for_stmt(aForStmt)
52
+ before_block_stmt(aForStmt)
53
+ end
54
+
55
+ def after_for_stmt(aForStmt, aVisitor)
56
+ aForStmt.test_expr.accept(aVisitor)
57
+ aForStmt.body_stmt.accept(aVisitor)
58
+ aForStmt.update_expr&.accept(aVisitor)
59
+ after_block_stmt(aForStmt)
60
+ end
61
+
62
+ def after_if_stmt(anIfStmt, aVisitor)
63
+ anIfStmt.then_stmt.accept(aVisitor)
64
+ anIfStmt.else_stmt&.accept(aVisitor)
65
+ end
66
+
67
+ def after_while_stmt(aWhileStmt, aVisitor)
68
+ aWhileStmt.body.accept(aVisitor)
69
+ aWhileStmt.condition.accept(aVisitor)
70
+ end
71
+
72
+ # A variable declaration adds a new variable to current scope
73
+ def before_var_stmt(aVarStmt)
74
+ declare(aVarStmt.name)
75
+ end
76
+
77
+ def after_var_stmt(aVarStmt)
78
+ define(aVarStmt.name)
79
+ end
80
+
81
+ # Assignment expressions require their variables resolved
82
+ def after_assign_expr(anAssignExpr, aVisitor)
83
+ resolve_local(anAssignExpr, aVisitor)
84
+ end
85
+
86
+ # Variable expressions require their variables resolved
87
+ def before_variable_expr(aVarExpr)
88
+ var_name = aVarExpr.name
89
+ if !scopes.empty? && (scopes.last[var_name] == false)
90
+ raise StandardError, "Can't read variable #{var_name} in its own initializer"
91
+ end
92
+ end
93
+
94
+ def after_variable_expr(aVarExpr, aVisitor)
95
+ resolve_local(aVarExpr, aVisitor)
96
+ end
97
+
98
+ def after_call_expr(aCallExpr, aVisitor)
99
+ # Evaluate callee part
100
+ aCallExpr.callee.accept(aVisitor)
101
+ aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
102
+ end
103
+
104
+ # function declaration creates a new scope for its body & binds its parameters for that scope
105
+ def before_fun_stmt(aFunStmt, aVisitor)
106
+ declare(aFunStmt.name)
107
+ define(aFunStmt.name)
108
+ resolve_function(aFunStmt, aVisitor)
109
+ end
110
+
111
+ private
112
+
113
+ def begin_scope
114
+ scopes.push({})
115
+ end
116
+
117
+ def end_scope
118
+ scopes.pop
119
+ end
120
+
121
+ def declare(aVarName)
122
+ return if scopes.empty?
123
+
124
+ curr_scope = scopes.last
125
+
126
+ # The initializer is not yet processed.
127
+ # Mark the variable as 'not yet ready' = exists but may not be referenced yet
128
+ curr_scope[aVarName] = false
129
+ end
130
+
131
+ def define(aVarName)
132
+ return if scopes.empty?
133
+
134
+ curr_scope = scopes.last
135
+
136
+ # The initializer (if any) was processed.
137
+ # Mark the variable as alive (= can be referenced in an expression)
138
+ curr_scope[aVarName] = true
139
+ end
140
+
141
+ def resolve_local(aVarExpr, _visitor)
142
+ max_i = i = scopes.size - 1
143
+ scopes.reverse_each do |scp|
144
+ if scp.include?(aVarExpr.name)
145
+ # Keep track of the difference of nesting levels between current scope
146
+ # and the scope where the variable is declared
147
+ @locals[aVarExpr] = max_i - i
148
+ break
149
+ end
150
+ i -= 1
151
+ end
152
+ end
153
+
154
+ def resolve_function(aFunStmt, aVisitor)
155
+ begin_scope
156
+
157
+ aFunStmt.params&.each do |param_name|
158
+ declare(param_name)
159
+ define(param_name)
160
+ end
161
+
162
+ body = aFunStmt.body
163
+ unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
164
+ body.subnodes.first&.accept(aVisitor)
165
+ end
166
+
167
+ end_scope
168
+ end
169
+ end # class
170
+ end # mmodule
171
+ 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.07'
4
+ VERSION = '0.1.08'
5
5
  end
@@ -34,10 +34,18 @@ 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
37
42
 
38
43
  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
39
46
  # Precondition: value to assign is on top of stack
40
47
  subject.stack.push(greeting)
48
+
41
49
  expect { subject.after_var_stmt(var_decl) }.not_to raise_error
42
50
  current_env = subject.symbol_table.current_env
43
51
  expect(current_env.defns['greeting']).to be_kind_of(Variable)
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.07
4
+ version: 0.1.08
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-03-14 00:00:00.000000000 Z
11
+ date: 2021-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -114,6 +114,7 @@ files:
114
114
  - lib/loxxy/back_end/entry.rb
115
115
  - lib/loxxy/back_end/environment.rb
116
116
  - lib/loxxy/back_end/lox_function.rb
117
+ - lib/loxxy/back_end/resolver.rb
117
118
  - lib/loxxy/back_end/symbol_table.rb
118
119
  - lib/loxxy/back_end/unary_operator.rb
119
120
  - lib/loxxy/back_end/variable.rb