loxxy 0.1.07 → 0.1.08

Sign up to get free protection for your applications and to get access to all the features.
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