loxxy 0.1.02 → 0.1.07

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: ddfc01e822c7e68c87d515649f6ef6e2c800c926ca289dfe9f65edeff24e7015
4
- data.tar.gz: db5a6a8052c0c15f00920c6d76fcd8c6f9cfe380905c20b53f8e74cc7c6d4f84
3
+ metadata.gz: f171f0046ff95784ed21b369751ca65d25e0ff757d1b2dccc534087a4b9c4a8d
4
+ data.tar.gz: 579badf6a30436962cb18e63dfee1ed361cfd152e9fecf54f2bfd25395db086b
5
5
  SHA512:
6
- metadata.gz: f5a85f8a0a4a762f43a9dd51ab972f924a95ed15ed9e864cef0c60f4815902cbfb905fef624cc100bae9283985f8f6c7f960fbd72bd8d87e9ee3ab211eb7dc24
7
- data.tar.gz: 10522ab655b99e31007dccaa479b4ec59a9340160f6768c084c3b96825f4e89f9f1c46c1655117763d66f28004f5d8e2c193315c80a65dd49b733587bd25823a
6
+ metadata.gz: 0470f022b121ceaff279f75024e07b78a218c3baa4d44e6f9bf54f4ebddf18341b27f697fe68c727eb2d61add2c55e7c102fbf73bfd29ef4fb3965024247f747
7
+ data.tar.gz: 320dbb075351069f624103f1d93aadf903ec6a5b7bb685871f53227b496760ac67ae9fa1dae85491efd9b94cfeb90eac8f66e104b4f70eaac9b0da081dfde567
data/CHANGELOG.md CHANGED
@@ -1,8 +1,68 @@
1
+ ## [0.1.07] - 2021-03-14
2
+ - `Loxxy` now supports nested functions and closures
3
+
4
+ ### Changed
5
+ - Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
6
+ - Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
7
+ - Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
8
+ - Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
9
+ - Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
10
+
11
+ ### Fixed
12
+ - Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
13
+
14
+
15
+
16
+
17
+ ## [0.1.06] - 2021-03-06
18
+ - Parameters/arguments checks in function declaration and call
19
+
20
+ ### Changed
21
+ - Method `Ast::AstBuilder#reduce_call_arglist` raises a `Loxxy::RuntimeError` when more than 255 arguments are used.
22
+ - Method `BackEnd::Engine#after_call_expr` raises a `Loxxy::RuntimeError` when argument count doesn't match the arity of function.
23
+
24
+ - Class `BackEnd::Function` renamed to `LoxFunction`
25
+
26
+ ## [0.1.05] - 2021-03-05
27
+ - Test for Fibbonacci recursive function is now passing.
28
+
29
+ ### Fixed
30
+ - Method `BackEnd::Function#call` a call doesn't no more generate of TWO scopes
31
+
32
+ ## [0.1.04] - 2021-02-28
33
+
34
+ ### Added
35
+ - Class `Ast::LoxReturnStmt` a node that represents a return statement
36
+ - Method `Ast::ASTBuilder#reduce_return_stmt`
37
+ - Method `Ast::ASTVisitor#visit_return_stmt` for visiting an `Ast::LoxReturnStmt` node
38
+ - Method `BackEnd::Engine#after_return_stmt` to handle return statement
39
+ - Method `BackEnd::Function#!` implementing the logical negation of a function (as value).
40
+ - Test suite for logical operators (in project repository)
41
+ - Test suite for block code
42
+ - Test suite for call and function declaration (initial)
43
+
44
+ ### Changed
45
+ - Method `BackEnd::Engine#after_call_expr` now generate a `catch` and `throw` events
46
+
47
+ ## [0.1.03] - 2021-02-26
48
+ - Runtime argument checking for arithmetic and comparison operators
49
+
50
+ ### Added
51
+ - Test suite for arithmetic and comparison operators (in project repository)
52
+ - Class `BackEnd::UnaryOperator`: runtime argument validation
53
+ - Class `BackEnd::BinaryOperator`: runtime argument validation
54
+
55
+ ### Changed
56
+ - File `console` renamed to `loxxy`. Very basic command-line interface.
57
+ - Custom exception classes
58
+ - File `README.md` updated list of supported `Lox` keywords.
59
+
60
+
1
61
  ## [0.1.02] - 2021-02-21
2
62
  - Function definition and call documented in `README.md`
3
63
 
4
64
  ### Changed
5
- - File `README.md` updated todescribe function definition and function call.
65
+ - File `README.md` updated description of function definition and function call.
6
66
 
7
67
  ### Fixed
8
68
  - Method `BackEnd::Engine#after_print_stmt` now handles of empty stack or nil data.
@@ -207,11 +267,11 @@
207
267
  - The interpreter can evaluate substraction between two numbers.
208
268
 
209
269
  ### Added
210
- - Method `Datatype::Number#-` implmenting the subtraction operation
270
+ - Method `Datatype::Number#-` implementing the subtraction operation
211
271
 
212
272
  ### Changed
213
273
  - File `README.md` minor editorial changes.
214
- - File `lx_string_spec.rb` Added test for string concatentation
274
+ - File `lx_string_spec.rb` Added test for string concatenation
215
275
  - File `number_spec.rb` Added tests for addition and subtraction operations
216
276
  - File `interpreter_spec.rb` Added tests for subtraction operation
217
277
 
data/README.md CHANGED
@@ -15,7 +15,6 @@ a simple language used in Bob Nystrom's online book [Crafting Interpreters](http
15
15
  ### Current status
16
16
  The project is still in inception and the interpreter is being implemented...
17
17
  Currently it can execute all allowed __Lox__ expressions and statements except:
18
- - Closures,
19
18
  - Classes and objects.
20
19
 
21
20
  These will be implemented soon.
@@ -180,7 +179,8 @@ Loxxy supports single line C-style comments.
180
179
  ### Keywords
181
180
  Loxxy implements the following __Lox__ reserved keywords:
182
181
  ```lang-none
183
- and, false, nil, or, print, true
182
+ and, else, false, for, fun, if,
183
+ nil, or, print, true, var, while
184
184
  ```
185
185
 
186
186
  ### Datatypes
data/bin/loxxy ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'loxxy'
5
+
6
+ if ARGV[0]
7
+ lox = Loxxy::Interpreter.new
8
+ File.open(ARGV[0], 'r') do |f|
9
+ lox.evaluate(f.read)
10
+ end
11
+ end
data/lib/loxxy.rb CHANGED
@@ -6,8 +6,6 @@ require_relative 'loxxy/front_end/raw_parser'
6
6
 
7
7
  # Namespace for all classes and constants of __loxxy__ gem.
8
8
  module Loxxy
9
- class Error < StandardError; end
10
-
11
9
  # Shorthand method. Returns the sole object that represents
12
10
  # a Lox false literal.
13
11
  # @return [Loxxy::Datatype::False]
@@ -12,6 +12,7 @@ require_relative 'lox_logical_expr'
12
12
  require_relative 'lox_assign_expr'
13
13
  require_relative 'lox_block_stmt'
14
14
  require_relative 'lox_while_stmt'
15
+ require_relative 'lox_return_stmt'
15
16
  require_relative 'lox_print_stmt'
16
17
  require_relative 'lox_if_stmt'
17
18
  require_relative 'lox_for_stmt'
@@ -221,6 +221,11 @@ module Loxxy
221
221
  Ast::LoxPrintStmt.new(tokens[1].position, theChildren[1])
222
222
  end
223
223
 
224
+ # rule('returnStmt' => 'RETURN expression_opt SEMICOLON')
225
+ def reduce_return_stmt(_production, _range, tokens, theChildren)
226
+ Ast::LoxReturnStmt.new(tokens[1].position, theChildren[1])
227
+ end
228
+
224
229
  # rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement').as ''
225
230
  def reduce_while_stmt(_production, _range, tokens, theChildren)
226
231
  Ast::LoxWhileStmt.new(tokens[1].position, theChildren[2], theChildren[4])
@@ -255,20 +260,33 @@ module Loxxy
255
260
 
256
261
  # rule('call' => 'primary refinement_plus').as 'call_expr'
257
262
  def reduce_call_expr(_production, _range, _tokens, theChildren)
258
- theChildren[1].callee = theChildren[0]
259
- theChildren[1]
263
+ members = theChildren.flatten
264
+ call_expr = nil
265
+ loop do
266
+ (callee, call_expr) = members.shift(2)
267
+ call_expr.callee = callee
268
+ members.unshift(call_expr)
269
+ break if members.size == 1
270
+ end
271
+
272
+ call_expr
273
+ end
274
+
275
+ # rule('refinement_plus' => 'refinement_plus refinement')
276
+ def reduce_refinement_plus_more(_production, _range, _tokens, theChildren)
277
+ theChildren[0] << theChildren[1]
260
278
  end
261
279
 
262
280
  # rule('refinement_plus' => 'refinement').
263
281
  def reduce_refinement_plus_end(_production, _range, _tokens, theChildren)
264
- theChildren[0]
282
+ theChildren
265
283
  end
266
284
 
267
285
  # rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
268
286
  def reduce_call_arglist(_production, _range, tokens, theChildren)
269
287
  args = theChildren[1] || []
270
288
  if args.size > 255
271
- raise StandardError, "Can't have more than 255 arguments."
289
+ raise Loxxy::RuntimeError, "Can't have more than 255 arguments."
272
290
  end
273
291
 
274
292
  LoxCallExpr.new(tokens[0].position, args)
@@ -298,6 +316,10 @@ module Loxxy
298
316
  def reduce_function(_production, _range, _tokens, theChildren)
299
317
  first_child = theChildren.first
300
318
  pos = first_child.token.position
319
+ if theChildren[2] && theChildren[2].size > 255
320
+ msg = "Can't have more than 255 parameters."
321
+ raise Loxxy::SyntaxError, msg
322
+ end
301
323
  LoxFunStmt.new(pos, first_child.token.lexeme, theChildren[2], theChildren[4])
302
324
  end
303
325
 
@@ -91,6 +91,14 @@ module Loxxy
91
91
  broadcast(:after_print_stmt, aPrintStmt)
92
92
  end
93
93
 
94
+ # Visit event. The visitor is about to visit a return statement.
95
+ # @param aReturnStmt [AST::LOXReturnStmt] the return statement node to visit
96
+ def visit_return_stmt(aReturnStmt)
97
+ broadcast(:before_return_stmt, aReturnStmt)
98
+ traverse_subnodes(aReturnStmt)
99
+ broadcast(:after_return_stmt, aReturnStmt, self)
100
+ end
101
+
94
102
  # Visit event. The visitor is about to visit a while statement node.
95
103
  # @param aWhileStmt [AST::LOXWhileStmt] the while statement node to visit
96
104
  def visit_while_stmt(aWhileStmt)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lox_compound_expr'
4
+
5
+ module Loxxy
6
+ module Ast
7
+ class LoxReturnStmt < LoxCompoundExpr
8
+ # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
9
+ # @param anExpression [Ast::LoxNode] expression to return
10
+ def initialize(aPosition, anExpression)
11
+ super(aPosition, [anExpression])
12
+ end
13
+
14
+ # Part of the 'visitee' role in Visitor design pattern.
15
+ # @param visitor [Ast::ASTVisitor] the visitor
16
+ def accept(visitor)
17
+ visitor.visit_return_stmt(self)
18
+ end
19
+ end # class
20
+ end # module
21
+ end # module
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Loxxy
6
+ module BackEnd
7
+ Signature = Struct.new(:parameter_types)
8
+
9
+ # A Lox binary operator
10
+ class BinaryOperator
11
+ # @return [String] text representation of the operator
12
+ attr_reader :name
13
+
14
+ # @return [Array<Class>]
15
+ attr_reader :signatures
16
+
17
+ # @param aName [String] "name" of operator
18
+ # @param theSignatures [Array<Signature>] allowed signatures
19
+ def initialize(aName, theSignatures)
20
+ @name = aName
21
+ @signatures = theSignatures
22
+ end
23
+
24
+ # rubocop: disable Style/ClassEqualityComparison
25
+ def validate_operands(operand1, operand2)
26
+ compliant = signatures.find do |(type1, type2)|
27
+ next unless operand1.kind_of?(type1)
28
+
29
+ if type2 == :idem
30
+ (operand2.class == operand1.class)
31
+ else
32
+ operand2.kind_of?(type2)
33
+ end
34
+ end
35
+ # rubocop: enable Style/ClassEqualityComparison
36
+
37
+ unless compliant
38
+ err = Loxxy::RuntimeError
39
+ if signatures.size == 1 && signatures[0].last == :idem
40
+ raise err, "Operands must be #{datatype_name(signatures[0].first)}s."
41
+ elsif signatures.size == 2 && signatures.all? { |(_, second)| second == :idem }
42
+ type1 = datatype_name(signatures[0].first)
43
+ type2 = datatype_name(signatures[1].first)
44
+ raise err, "Operands must be two #{type1}s or two #{type2}s."
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def datatype_name(aClass)
52
+ # (?:(?:[^:](?!:|(?<=LX))))+$
53
+ aClass.name.sub(/^.+(?=::)::(?:LX)?/, '').downcase
54
+ end
55
+ end # class
56
+ end # module
57
+ end # module
@@ -2,8 +2,10 @@
2
2
 
3
3
  # Load all the classes implementing AST nodes
4
4
  require_relative '../ast/all_lox_nodes'
5
- require_relative 'function'
5
+ require_relative 'binary_operator'
6
+ require_relative 'lox_function'
6
7
  require_relative 'symbol_table'
8
+ require_relative 'unary_operator'
7
9
 
8
10
  module Loxxy
9
11
  module BackEnd
@@ -17,16 +19,26 @@ module Loxxy
17
19
  # @return [BackEnd::SymbolTable]
18
20
  attr_reader :symbol_table
19
21
 
20
- # @return [Array<Datatype::BuiltinDatatyp>] Stack for the values of expr
22
+ # @return [Array<Datatype::BuiltinDatatype>] Data stack for the expression results
21
23
  attr_reader :stack
22
24
 
25
+ # @return [Hash { Symbol => UnaryOperator}]
26
+ attr_reader :unary_operators
27
+
28
+ # @return [Hash { Symbol => BinaryOperator}]
29
+ attr_reader :binary_operators
30
+
23
31
  # @param theOptions [Hash]
24
32
  def initialize(theOptions)
25
33
  @config = theOptions
26
34
  @ostream = config.include?(:ostream) ? config[:ostream] : $stdout
27
35
  @symbol_table = SymbolTable.new
28
36
  @stack = []
37
+ @unary_operators = {}
38
+ @binary_operators = {}
29
39
 
40
+ init_unary_operators
41
+ init_binary_operators
30
42
  init_globals
31
43
  end
32
44
 
@@ -50,7 +62,7 @@ module Loxxy
50
62
  end
51
63
 
52
64
  def after_var_stmt(aVarStmt)
53
- new_var = Variable.new(aVarStmt.name, aVarStmt.subnodes[0])
65
+ new_var = Variable.new(aVarStmt.name, stack.pop)
54
66
  symbol_table.insert(new_var)
55
67
  end
56
68
 
@@ -86,6 +98,10 @@ module Loxxy
86
98
  @ostream.print tos ? tos.to_str : 'nil'
87
99
  end
88
100
 
101
+ def after_return_stmt(_returnStmt, _aVisitor)
102
+ throw(:return)
103
+ end
104
+
89
105
  def after_while_stmt(aWhileStmt, aVisitor)
90
106
  loop do
91
107
  condition = stack.pop
@@ -110,9 +126,8 @@ module Loxxy
110
126
  variable = symbol_table.lookup(var_name)
111
127
  raise StandardError, "Unknown variable #{var_name}" unless variable
112
128
 
113
- value = stack.pop
129
+ value = stack.last # ToS remains since an assignment produces a value
114
130
  variable.assign(value)
115
- stack.push value # An expression produces a value
116
131
  end
117
132
 
118
133
  def after_logical_expr(aLogicalExpr, visitor)
@@ -148,9 +163,11 @@ module Loxxy
148
163
  end
149
164
 
150
165
  def after_binary_expr(aBinaryExpr)
151
- op = aBinaryExpr.operator
152
166
  operand2 = stack.pop
153
167
  operand1 = stack.pop
168
+ op = aBinaryExpr.operator
169
+ operator = binary_operators[op]
170
+ operator.validate_operands(operand1, operand2)
154
171
  if operand1.respond_to?(op)
155
172
  stack.push operand1.send(op, operand2)
156
173
  else
@@ -160,12 +177,14 @@ module Loxxy
160
177
  end
161
178
 
162
179
  def after_unary_expr(anUnaryExpr)
163
- op = anUnaryExpr.operator
164
180
  operand = stack.pop
181
+ op = anUnaryExpr.operator
182
+ operator = unary_operators[op]
183
+ operator.validate_operand(operand)
165
184
  if operand.respond_to?(op)
166
185
  stack.push operand.send(op)
167
186
  else
168
- msg1 = "`#{op}': Unimplemented operator for a #{operand1.class}."
187
+ msg1 = "`#{op}': Unimplemented operator for a #{operand.class}."
169
188
  raise StandardError, msg1
170
189
  end
171
190
  end
@@ -176,18 +195,18 @@ module Loxxy
176
195
  callee = stack.pop
177
196
  aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
178
197
 
179
- if callee.kind_of?(NativeFunction)
198
+ case callee
199
+ when NativeFunction
180
200
  stack.push callee.call # Pass arguments
181
- else
182
- new_env = Environment.new(symbol_table.current_env)
183
- symbol_table.enter_environment(new_env)
184
- callee.parameters&.each do |param_name|
185
- local = Variable.new(param_name, stack.pop)
186
- symbol_table.insert(local)
201
+ when LoxFunction
202
+ arg_count = aCallExpr.arguments.size
203
+ if arg_count != callee.arity
204
+ msg = "Expected #{callee.arity} arguments but got #{arg_count}."
205
+ raise Loxxy::RuntimeError, msg
187
206
  end
188
- callee.call(aVisitor)
189
-
190
- symbol_table.leave_environment
207
+ callee.call(self, aVisitor)
208
+ else
209
+ raise Loxxy::RuntimeError, 'Can only call functions and classes.'
191
210
  end
192
211
  end
193
212
 
@@ -214,7 +233,7 @@ module Loxxy
214
233
  end
215
234
 
216
235
  def after_fun_stmt(aFunStmt, _visitor)
217
- function = Function.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, stack)
236
+ function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
218
237
  new_var = Variable.new(aFunStmt.name, function)
219
238
  symbol_table.insert(new_var)
220
239
  end
@@ -235,6 +254,48 @@ module Loxxy
235
254
  end
236
255
  end
237
256
 
257
+ def init_unary_operators
258
+ negate_op = UnaryOperator.new('-', [Datatype::Number])
259
+ unary_operators[:-@] = negate_op
260
+
261
+ negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype,
262
+ BackEnd::LoxFunction])
263
+ unary_operators[:!] = negation_op
264
+ end
265
+
266
+ def init_binary_operators
267
+ plus_op = BinaryOperator.new('+', [[Datatype::Number, :idem],
268
+ [Datatype::LXString, :idem]])
269
+ binary_operators[:+] = plus_op
270
+
271
+ minus_op = BinaryOperator.new('-', [[Datatype::Number, :idem]])
272
+ binary_operators[:-] = minus_op
273
+
274
+ star_op = BinaryOperator.new('*', [[Datatype::Number, :idem]])
275
+ binary_operators[:*] = star_op
276
+
277
+ slash_op = BinaryOperator.new('/', [[Datatype::Number, :idem]])
278
+ binary_operators[:/] = slash_op
279
+
280
+ equal_equal_op = BinaryOperator.new('==', [[Datatype::BuiltinDatatype, Datatype::BuiltinDatatype]])
281
+ binary_operators[:==] = equal_equal_op
282
+
283
+ not_equal_op = BinaryOperator.new('!=', [[Datatype::BuiltinDatatype, Datatype::BuiltinDatatype]])
284
+ binary_operators[:!=] = not_equal_op
285
+
286
+ less_op = BinaryOperator.new('<', [[Datatype::Number, :idem]])
287
+ binary_operators[:<] = less_op
288
+
289
+ less_equal_op = BinaryOperator.new('<=', [[Datatype::Number, :idem]])
290
+ binary_operators[:<=] = less_equal_op
291
+
292
+ greater_op = BinaryOperator.new('>', [[Datatype::Number, :idem]])
293
+ binary_operators[:>] = greater_op
294
+
295
+ greater_equal_op = BinaryOperator.new('>=', [[Datatype::Number, :idem]])
296
+ binary_operators[:>=] = greater_equal_op
297
+ end
298
+
238
299
  def init_globals
239
300
  add_native_fun('clock', native_clock)
240
301
  end
@@ -9,10 +9,17 @@ module Loxxy
9
9
  # of a relation or a relation definition.
10
10
  # It contains a map of names to the objects they name (e.g. logical var)
11
11
  class Environment
12
- # The enclosing (parent) environment.
12
+ # The enclosing (parent) environment.
13
13
  # @return [Environment, NilClass]
14
14
  attr_accessor :enclosing
15
15
 
16
+ # The previous environment in the environment chain.
17
+ # @return [Environment, NilClass]
18
+ attr_accessor :predecessor
19
+
20
+ # @return [Boolean] true if this environment is part of a closure (contains an embedded function)
21
+ attr_accessor :embedding
22
+
16
23
  # Mapping from user-defined name to related definition
17
24
  # (say, a variable object)
18
25
  # @return [Hash{String => Variable}] Pairs of the kind
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../datatype/all_datatypes'
4
+
5
+ module Loxxy
6
+ module BackEnd
7
+ # rubocop: disable Style/AccessorGrouping
8
+ # Representation of a Lox function.
9
+ # It is a named slot that can be associated with a value at the time.
10
+ class LoxFunction
11
+ # @return [String] The name of the function (if any)
12
+ attr_reader :name
13
+
14
+ # @return [Array<>] the parameters
15
+ attr_reader :parameters
16
+ attr_reader :body
17
+ attr_reader :stack
18
+ attr_reader :closure
19
+
20
+ # Create a function with given name
21
+ # @param aName [String] The name of the variable
22
+ def initialize(aName, parameterList, aBody, anEngine)
23
+ @name = aName.dup
24
+ @parameters = parameterList
25
+ @body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
26
+ @stack = anEngine.stack
27
+ @closure = anEngine.symbol_table.current_env
28
+ anEngine.symbol_table.current_env.embedding = true
29
+ end
30
+
31
+ def arity
32
+ parameters ? parameters.size : 0
33
+ end
34
+
35
+ def accept(_visitor)
36
+ stack.push self
37
+ end
38
+
39
+ def call(engine, aVisitor)
40
+ # new_env = Environment.new(engine.symbol_table.current_env)
41
+ new_env = Environment.new(closure)
42
+ engine.symbol_table.enter_environment(new_env)
43
+
44
+ parameters&.each do |param_name|
45
+ local = Variable.new(param_name, stack.pop)
46
+ engine.symbol_table.insert(local)
47
+ end
48
+
49
+ catch(:return) do
50
+ (body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
51
+ throw(:return)
52
+ end
53
+
54
+ engine.symbol_table.leave_environment
55
+ end
56
+
57
+ # Logical negation.
58
+ # As a function is a truthy thing, its negation is thus false.
59
+ # @return [Datatype::False]
60
+ def !
61
+ Datatype::False.instance
62
+ end
63
+
64
+ # Text representation of a Lox function
65
+ def to_str
66
+ "<fn #{name}>"
67
+ end
68
+ end # class
69
+ # rubocop: enable Style/AccessorGrouping
70
+ end # module
71
+ end # module
@@ -44,23 +44,33 @@ module Loxxy
44
44
  # to be a child of current environment and to be itself the new current environment.
45
45
  # @param anEnv [BackEnd::Environment] the Environment that
46
46
  def enter_environment(anEnv)
47
- anEnv.enclosing = current_env
47
+ if anEnv.enclosing && (anEnv.enclosing != current_env)
48
+ anEnv.predecessor = current_env
49
+ else
50
+ anEnv.enclosing = current_env
51
+ end
48
52
  @current_env = anEnv
49
53
  end
50
54
 
51
55
  def leave_environment
52
- current_env.defns.each_pair do |nm, _item|
53
- environments = name2envs[nm]
54
- if environments.size == 1
55
- name2envs.delete(nm)
56
- else
57
- environments.pop
58
- name2envs[nm] = environments
56
+ unless current_env.embedding
57
+ current_env.defns.each_pair do |nm, _item|
58
+ environments = name2envs[nm]
59
+ if environments.size == 1
60
+ name2envs.delete(nm)
61
+ else
62
+ environments.pop
63
+ name2envs[nm] = environments
64
+ end
59
65
  end
60
66
  end
61
67
  raise StandardError, 'Cannot remove root environment.' if current_env == root
62
68
 
63
- @current_env = current_env.enclosing
69
+ if current_env.predecessor
70
+ @current_env = current_env.predecessor
71
+ else
72
+ @current_env = current_env.enclosing
73
+ end
64
74
  end
65
75
 
66
76
  # Add an entry with given name to current environment.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Loxxy
6
+ module BackEnd
7
+ # A Lox unary operator
8
+ class UnaryOperator
9
+ # @return [String] text representation of the operator
10
+ attr_reader :name
11
+
12
+ # @return [Array<Class>]
13
+ attr_reader :signatures
14
+
15
+ # @param aName [String] "name" of operator
16
+ # @param theSignatures [Array<Class>] allowed signatures
17
+ def initialize(aName, theSignatures)
18
+ @name = aName
19
+ @signatures = theSignatures
20
+ end
21
+
22
+ def validate_operand(operand1)
23
+ compliant = signatures.find { |some_type| operand1.kind_of?(some_type) }
24
+
25
+ unless compliant
26
+ err = Loxxy::RuntimeError
27
+ # if signatures.size == 1
28
+ raise err, "Operand must be a #{datatype_name(signatures[0])}."
29
+ # end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def datatype_name(aClass)
36
+ # (?:(?:[^:](?!:|(?<=LX))))+$
37
+ aClass.name.sub(/^.+(?=::)::(?:LX)?/, '').downcase
38
+ end
39
+ end # class
40
+ end # module
41
+ end # module
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Loxxy
4
+ # Abstract class. Generalization of Loxxy error classes.
5
+ class Error < StandardError; end
6
+
7
+ # Error occurring while Loxxy executes some invalid Lox code.
8
+ class RuntimeError < Error; end
9
+
10
+ # Error occurring while Loxxy parses some invalid Lox code.
11
+ class SyntaxError < Error; end
12
+ end
@@ -74,7 +74,7 @@ module Loxxy
74
74
  rule('elsePart_opt' => [])
75
75
 
76
76
  rule('printStmt' => 'PRINT expression SEMICOLON').as 'print_stmt'
77
- rule('returnStmt' => 'RETURN expression_opt SEMICOLON')
77
+ rule('returnStmt' => 'RETURN expression_opt SEMICOLON').as 'return_stmt'
78
78
  rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement').as 'while_stmt'
79
79
  rule('block' => 'LEFT_BRACE declaration_plus RIGHT_BRACE').as 'block_stmt'
80
80
  rule('block' => 'LEFT_BRACE RIGHT_BRACE').as 'block_empty'
@@ -127,7 +127,7 @@ module Loxxy
127
127
  rule('unaryOp' => 'MINUS')
128
128
  rule('call' => 'primary')
129
129
  rule('call' => 'primary refinement_plus').as 'call_expr'
130
- rule('refinement_plus' => 'refinement_plus refinement') # .as 'refinement_plus_more'
130
+ rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
131
131
  rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
132
132
  rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
133
133
  rule('refinement' => 'DOT IDENTIFIER')
@@ -92,6 +92,7 @@ module Loxxy
92
92
 
93
93
  private
94
94
 
95
+ # rubocop: disable Lint/DuplicateBranch
95
96
  def _next_token
96
97
  skip_intertoken_spaces
97
98
  curr_ch = scanner.peek(1)
@@ -125,6 +126,7 @@ module Loxxy
125
126
 
126
127
  return token
127
128
  end
129
+ # rubocop: enable Lint/DuplicateBranch
128
130
 
129
131
  def build_token(aSymbolName, aLexeme)
130
132
  begin
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.02'
4
+ VERSION = '0.1.07'
5
5
  end
data/loxxy.gemspec CHANGED
@@ -46,8 +46,8 @@ Gem::Specification.new do |spec|
46
46
  spec.license = 'MIT'
47
47
  spec.required_ruby_version = '~> 2.4'
48
48
 
49
- spec.bindir = 'exe'
50
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
49
+ spec.bindir = 'bin'
50
+ spec.executables = ['loxxy']
51
51
  spec.require_paths = ['lib']
52
52
 
53
53
  PkgExtending.pkg_files(spec)
@@ -36,6 +36,8 @@ module Loxxy
36
36
 
37
37
 
38
38
  it "should react to 'after_var_stmt' event" do
39
+ # Precondition: value to assign is on top of stack
40
+ subject.stack.push(greeting)
39
41
  expect { subject.after_var_stmt(var_decl) }.not_to raise_error
40
42
  current_env = subject.symbol_table.current_env
41
43
  expect(current_env.defns['greeting']).to be_kind_of(Variable)
@@ -53,15 +53,6 @@ module Loxxy
53
53
  end # context
54
54
 
55
55
  context 'Evaluating Lox code:' do
56
- let(:hello_world) do
57
- lox = <<-LOX_END
58
- var greeting = "Hello"; // Declaring a variable
59
- print greeting + ", " + "world!"; // ... Playing with concatenation
60
- LOX_END
61
-
62
- lox
63
- end
64
-
65
56
  it 'should evaluate core data types' do
66
57
  result = subject.evaluate('true; // Not false')
67
58
  expect(result).to be_kind_of(Loxxy::Datatype::True)
@@ -269,9 +260,9 @@ LOX_END
269
260
 
270
261
  it 'should accept variable mention' do
271
262
  program = <<-LOX_END
272
- var foo = "bar";
273
- print foo; // => bar
274
- LOX_END
263
+ var foo = "bar";
264
+ print foo; // => bar
265
+ LOX_END
275
266
  expect { subject.evaluate(program) }.not_to raise_error
276
267
  expect(sample_cfg[:ostream].string).to eq('bar')
277
268
  end
@@ -424,8 +415,48 @@ LOX_END
424
415
  expect(sample_cfg[:ostream].string).to eq('<fn foo><native fn>')
425
416
  end
426
417
 
418
+ it 'should support return statements' do
419
+ program = <<-LOX_END
420
+ fun max(a, b) {
421
+ if (a > b) return a;
422
+
423
+ return b;
424
+ }
425
+
426
+ max(3, 2);
427
+ LOX_END
428
+ result = subject.evaluate(program)
429
+ expect(result).to eq(3)
430
+ end
431
+
432
+ # rubocop: disable Style/StringConcatenation
433
+ it 'should support local functions and closures' do
434
+ program = <<-LOX_END
435
+ fun makeCounter() {
436
+ var i = 0;
437
+ fun count() {
438
+ i = i + 1;
439
+ print i;
440
+ }
441
+
442
+ return count;
443
+ }
444
+
445
+ var counter = makeCounter();
446
+ counter(); // "1".
447
+ counter(); // "2".
448
+ LOX_END
449
+ expect { subject.evaluate(program) }.not_to raise_error
450
+ expect(sample_cfg[:ostream].string).to eq('1' + '2')
451
+ end
452
+ # rubocop: enable Style/StringConcatenation
453
+
427
454
  it 'should print the hello world message' do
428
- expect { subject.evaluate(hello_world) }.not_to raise_error
455
+ program = <<-LOX_END
456
+ var greeting = "Hello"; // Declaring a variable
457
+ print greeting + ", " + "world!"; // ... Playing with concatenation
458
+ LOX_END
459
+ expect { subject.evaluate(program) }.not_to raise_error
429
460
  expect(sample_cfg[:ostream].string).to eq('Hello, world!')
430
461
  end
431
462
  end # context
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.02
4
+ version: 0.1.07
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dimitri Geshef
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-21 00:00:00.000000000 Z
11
+ date: 2021-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -69,7 +69,8 @@ dependencies:
69
69
  description: An implementation of the Lox programming language. WIP
70
70
  email:
71
71
  - famished.tiger@yahoo.com
72
- executables: []
72
+ executables:
73
+ - loxxy
73
74
  extensions: []
74
75
  extra_rdoc_files:
75
76
  - README.md
@@ -83,6 +84,7 @@ files:
83
84
  - LICENSE.txt
84
85
  - README.md
85
86
  - Rakefile
87
+ - bin/loxxy
86
88
  - lib/loxxy.rb
87
89
  - lib/loxxy/ast/all_lox_nodes.rb
88
90
  - lib/loxxy/ast/ast_builder.rb
@@ -101,16 +103,19 @@ files:
101
103
  - lib/loxxy/ast/lox_node.rb
102
104
  - lib/loxxy/ast/lox_noop_expr.rb
103
105
  - lib/loxxy/ast/lox_print_stmt.rb
106
+ - lib/loxxy/ast/lox_return_stmt.rb
104
107
  - lib/loxxy/ast/lox_seq_decl.rb
105
108
  - lib/loxxy/ast/lox_unary_expr.rb
106
109
  - lib/loxxy/ast/lox_var_stmt.rb
107
110
  - lib/loxxy/ast/lox_variable_expr.rb
108
111
  - lib/loxxy/ast/lox_while_stmt.rb
112
+ - lib/loxxy/back_end/binary_operator.rb
109
113
  - lib/loxxy/back_end/engine.rb
110
114
  - lib/loxxy/back_end/entry.rb
111
115
  - lib/loxxy/back_end/environment.rb
112
- - lib/loxxy/back_end/function.rb
116
+ - lib/loxxy/back_end/lox_function.rb
113
117
  - lib/loxxy/back_end/symbol_table.rb
118
+ - lib/loxxy/back_end/unary_operator.rb
114
119
  - lib/loxxy/back_end/variable.rb
115
120
  - lib/loxxy/datatype/all_datatypes.rb
116
121
  - lib/loxxy/datatype/boolean.rb
@@ -120,6 +125,7 @@ files:
120
125
  - lib/loxxy/datatype/nil.rb
121
126
  - lib/loxxy/datatype/number.rb
122
127
  - lib/loxxy/datatype/true.rb
128
+ - lib/loxxy/error.rb
123
129
  - lib/loxxy/front_end/grammar.rb
124
130
  - lib/loxxy/front_end/literal.rb
125
131
  - lib/loxxy/front_end/parser.rb
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../datatype/all_datatypes'
4
-
5
- module Loxxy
6
- module BackEnd
7
- # rubocop: disable Style/AccessorGrouping
8
- # Representation of a Lox function.
9
- # It is a named slot that can be associated with a value at the time.
10
- class Function
11
- # @return [String]
12
- attr_reader :name
13
-
14
- # @return [Array<>] the parameters
15
- attr_reader :parameters
16
- attr_reader :body
17
- attr_reader :stack
18
-
19
- # Create a variable with given name and initial value
20
- # @param aName [String] The name of the variable
21
- # @param aValue [Datatype::BuiltinDatatype] the initial assigned value
22
- def initialize(aName, parameterList, aBody, aStack)
23
- @name = aName.dup
24
- @parameters = parameterList
25
- @body = aBody
26
- @stack = aStack
27
- end
28
-
29
- def accept(_visitor)
30
- stack.push self
31
- end
32
-
33
- def call(aVisitor)
34
- body.empty? ? Datatype::Nil.instance : body.accept(aVisitor)
35
- end
36
-
37
- # Text representation of a Lox function
38
- def to_str
39
- "<fn #{name}>"
40
- end
41
- end # class
42
- # rubocop: enable Style/AccessorGrouping
43
- end # module
44
- end # module