loxxy 0.1.02 → 0.1.07

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: 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