loxxy 0.0.20 → 0.0.21
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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +18 -4
- data/lib/loxxy/ast/all_lox_nodes.rb +1 -0
- data/lib/loxxy/ast/ast_builder.rb +13 -0
- data/lib/loxxy/ast/ast_visitor.rb +8 -0
- data/lib/loxxy/ast/lox_var_stmt.rb +28 -0
- data/lib/loxxy/back_end/engine.rb +11 -1
- data/lib/loxxy/back_end/entry.rb +41 -0
- data/lib/loxxy/back_end/environment.rb +66 -0
- data/lib/loxxy/back_end/symbol_table.rb +135 -0
- data/lib/loxxy/back_end/variable.rb +25 -0
- data/lib/loxxy/front_end/grammar.rb +2 -2
- data/lib/loxxy/interpreter.rb +1 -1
- data/lib/loxxy/version.rb +1 -1
- data/spec/back_end/engine_spec.rb +8 -0
- data/spec/back_end/environment_spec.rb +74 -0
- data/spec/back_end/symbol_table_spec.rb +142 -0
- data/spec/back_end/variable_spec.rb +79 -0
- data/spec/interpreter_spec.rb +10 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd4cbf32f4ee44a60c45630a76318425850dc35b4d9f6129274cefcbf4e91fcc
|
4
|
+
data.tar.gz: 711e0ae07d2a0b3a080c2fe408d9adf4f7b96bb070af686d66dd3f9dbae8391d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 131f6fe3b23dc0a84b063efe70744b02ecc9edb68742540799964311298f35494a0d48db75bb18a240ddeed039286ab08ba2ef99e2ae4857d2b4d5dc4403d5ba
|
7
|
+
data.tar.gz: b46c18f54147fd01f9819e3ad06564ee78fe72de5c91d6ef8a5bef5945d9f1ad622cad848c4d521ae66f00c91b7825d01e57c831b2a097da830bec0d99f2cf06
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## [0.0.21] - 2021-01-1x
|
2
|
+
- The interpreter supports the declaration global variables.
|
3
|
+
|
4
|
+
## Added
|
5
|
+
- Class `BackEnd::Entry`, mixin module for objects put in the symbol table
|
6
|
+
- Class `BackEnd::Environment` that keeps track of variables in a given context.
|
7
|
+
- Class `BackEnd::SymbolTable` that keeps track of environments.
|
8
|
+
- Class `BackEnd::Variable` internal representation of a `Lox` variable
|
9
|
+
- Method `Ast::ASTBuilder#reduce_var_declaration` and `Ast::ASTBuilder#reduce_var_declaration`
|
10
|
+
- Method `Ast::ASTVisitor#visit_var_stmt` for visiting `LoxVarStmt` nodes
|
11
|
+
- Attribute `Engine::symbol_table`for keeping track of variables
|
12
|
+
- Method `Engine::after_var_stmt` for the implementation of variable declarations
|
13
|
+
|
1
14
|
## [0.0.20] - 2021-01-15
|
2
15
|
- The interpreter supports the `if` ... `else` statement.
|
3
16
|
|
data/README.md
CHANGED
@@ -8,7 +8,7 @@ a simple language used in Bob Nystrom's online book [Crafting Interpreters](http
|
|
8
8
|
|
9
9
|
### Purpose of this project:
|
10
10
|
- To deliver an open source example of a programming language fully implemented in Ruby
|
11
|
-
(from the scanner
|
11
|
+
(from the scanner and parser to an interpreter).
|
12
12
|
- The implementation should be mature enough to run [LoxLox](https://github.com/benhoyt/loxlox),
|
13
13
|
a Lox interpreter written in Lox.
|
14
14
|
|
@@ -141,7 +141,9 @@ Here are the language features currently supported by the interpreter:
|
|
141
141
|
- [Datatypes](#datatypes)
|
142
142
|
- [Statements](#statements)
|
143
143
|
-[Expressions](#expressions)
|
144
|
-
|
144
|
+
- [Variable declarations](#var-statement)
|
145
|
+
- [If Statement](#if-statement)
|
146
|
+
- [Print Statement](#print-statement)
|
145
147
|
|
146
148
|
### Comments
|
147
149
|
|
@@ -168,13 +170,15 @@ loxxy supports all the standard __Lox__ datatypes:
|
|
168
170
|
### Statements
|
169
171
|
|
170
172
|
Loxxy supports the following statements:
|
171
|
-
-[Expressions](#expressions)
|
173
|
+
- [Expressions](#expressions)
|
172
174
|
-[Arithmetic expressions](#arithmetic-expressions)
|
173
175
|
-[String concatenation](#string-concatenation)
|
174
176
|
-[Comparison expressions](#comparison-expressions)
|
175
177
|
-[Logical expressions](#logical-expressions)
|
176
178
|
-[Grouping expressions](#grouping-expressions)
|
177
|
-
|
179
|
+
|
180
|
+
-[Variable declarations](#var-statement)
|
181
|
+
-[If Statement](#if-statement)
|
178
182
|
-[Print Statement](#print-statement)
|
179
183
|
|
180
184
|
#### Expressions
|
@@ -254,6 +258,16 @@ print 3 + 4 * 5; // => 23
|
|
254
258
|
print (3 + 4) * 5; // => 35
|
255
259
|
```
|
256
260
|
|
261
|
+
#### Variable declarations
|
262
|
+
``` javascript
|
263
|
+
var iAmAVariable = "my-initial-value";
|
264
|
+
var iAmNil; // __Lox__ initializes variables to nil by default;
|
265
|
+
```
|
266
|
+
|
267
|
+
Warning: current version cannot retrieve the value of a variable.
|
268
|
+
Expect this capability to be implemented in the coming days.
|
269
|
+
|
270
|
+
|
257
271
|
#### If statement
|
258
272
|
|
259
273
|
Based on a given condition, an if statement executes one of two statements:
|
@@ -158,6 +158,19 @@ module Loxxy
|
|
158
158
|
return_first_child(range, tokens, theChildren) # Discard the semicolon
|
159
159
|
end
|
160
160
|
|
161
|
+
# rule('varDecl' => 'VAR IDENTIFIER SEMICOLON')
|
162
|
+
def reduce_var_declaration(_production, _range, tokens, theChildren)
|
163
|
+
var_name = theChildren[1].token.lexeme.dup
|
164
|
+
Ast::LoxVarStmt.new(tokens[1].position, var_name, nil)
|
165
|
+
end
|
166
|
+
|
167
|
+
# rule('varDecl' => 'VAR IDENTIFIER EQUAL expression SEMICOLON')
|
168
|
+
def reduce_var_initialization(_production, _range, tokens, theChildren)
|
169
|
+
var_name = theChildren[1].token.lexeme.dup
|
170
|
+
Ast::LoxVarStmt.new(tokens[1].position, var_name, theChildren[3])
|
171
|
+
end
|
172
|
+
|
173
|
+
|
161
174
|
# rule('ifStmt' => 'IF ifCondition statement elsePart_opt')
|
162
175
|
def reduce_if_stmt(_production, _range, tokens, theChildren)
|
163
176
|
condition = theChildren[1]
|
@@ -51,6 +51,14 @@ module Loxxy
|
|
51
51
|
broadcast(:after_ptree, aParseTree)
|
52
52
|
end
|
53
53
|
|
54
|
+
# Visit event. The visitor is about to visit a variable declaration statement.
|
55
|
+
# @param aPrintStmt [AST::LOXVarStmt] the variable declaration node to visit
|
56
|
+
def visit_var_stmt(aVarStmt)
|
57
|
+
broadcast(:before_var_stmt, aVarStmt)
|
58
|
+
traverse_subnodes(aVarStmt)
|
59
|
+
broadcast(:after_var_stmt, aVarStmt)
|
60
|
+
end
|
61
|
+
|
54
62
|
# Visit event. The visitor is about to visit a if statement.
|
55
63
|
# @param anIfStmt [AST::LOXIfStmt] the if statement node to visit
|
56
64
|
def visit_if_stmt(anIfStmt)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_compound_expr'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
# This AST node represents a variable declaration
|
8
|
+
class LoxVarStmt < LoxCompoundExpr
|
9
|
+
# @return [String] variable name
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
13
|
+
# @param aName [String] name of the variable
|
14
|
+
# @param aValue [Loxxy::Ast::LoxNode, NilClass] initial value for the variable
|
15
|
+
def initialize(aPosition, aName, aValue)
|
16
|
+
initial_value = aValue ? [aValue] : []
|
17
|
+
super(aPosition, initial_value)
|
18
|
+
@name = aName
|
19
|
+
end
|
20
|
+
|
21
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
22
|
+
# @param visitor [Ast::ASTVisitor] the visitor
|
23
|
+
def accept(visitor)
|
24
|
+
visitor.visit_var_stmt(self)
|
25
|
+
end
|
26
|
+
end # class
|
27
|
+
end # module
|
28
|
+
end # module
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
# Load all the classes implementing AST nodes
|
4
4
|
require_relative '../ast/all_lox_nodes'
|
5
|
+
require_relative 'symbol_table'
|
5
6
|
|
6
7
|
module Loxxy
|
7
8
|
module BackEnd
|
@@ -12,13 +13,17 @@ module Loxxy
|
|
12
13
|
# @return [Hash] A set of configuration options
|
13
14
|
attr_reader :config
|
14
15
|
|
15
|
-
# @return [
|
16
|
+
# @return [BackEnd::SymbolTable]
|
17
|
+
attr_reader :symbol_table
|
18
|
+
|
19
|
+
# @return [Array<Datatype::BuiltinDatatyp>] Stack for the values of expr
|
16
20
|
attr_reader :stack
|
17
21
|
|
18
22
|
# @param theOptions [Hash]
|
19
23
|
def initialize(theOptions)
|
20
24
|
@config = theOptions
|
21
25
|
@ostream = config.include?(:ostream) ? config[:ostream] : $stdout
|
26
|
+
@symbol_table = SymbolTable.new
|
22
27
|
@stack = []
|
23
28
|
end
|
24
29
|
|
@@ -36,6 +41,11 @@ module Loxxy
|
|
36
41
|
##########################################################################
|
37
42
|
# Visit event handling
|
38
43
|
##########################################################################
|
44
|
+
def after_var_stmt(aVarStmt)
|
45
|
+
new_var = Variable.new(aVarStmt.name, aVarStmt.subnodes[0])
|
46
|
+
symbol_table.insert(new_var)
|
47
|
+
end
|
48
|
+
|
39
49
|
def after_if_stmt(anIfStmt, aVisitor)
|
40
50
|
# Retrieve the result of the condition evaluation
|
41
51
|
condition = stack.pop
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Loxxy
|
4
|
+
module BackEnd
|
5
|
+
# Mix-in module that implements the expected common behaviour of entries
|
6
|
+
# placed in the symbol table.
|
7
|
+
module Entry
|
8
|
+
# @return [String] User-defined name of the entry.
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
=begin
|
12
|
+
# @return [String] Suffix for building the internal name of the entry.
|
13
|
+
attr_accessor :suffix
|
14
|
+
=end
|
15
|
+
|
16
|
+
# Initialize the entry with given name
|
17
|
+
# @param aName [String] The name of the entry
|
18
|
+
def init_name(aName)
|
19
|
+
@name = aName.dup
|
20
|
+
@name.freeze
|
21
|
+
end
|
22
|
+
=begin
|
23
|
+
# Return the internal name of the entry
|
24
|
+
# Internal names used to disambiguate entry names.
|
25
|
+
# There might be homonyns between variable because:
|
26
|
+
# - A child Scope may have a entry with same name as one of its
|
27
|
+
# ancestor(s).
|
28
|
+
# - Multiple calls to same defrel or procedure may imply multiple creation
|
29
|
+
# of a entry given name...
|
30
|
+
# @return [String] internal name
|
31
|
+
def i_name
|
32
|
+
if suffix =~ /^_/
|
33
|
+
label + suffix
|
34
|
+
else
|
35
|
+
(suffix.nil? || suffix.empty?) ? label : suffix
|
36
|
+
end
|
37
|
+
end
|
38
|
+
=end
|
39
|
+
end # module
|
40
|
+
end # module
|
41
|
+
end # module
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'variable'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module BackEnd
|
7
|
+
# A environment is a name space that corresponds either to a specific
|
8
|
+
# delimited region in Loxxy source code or to an activation record
|
9
|
+
# of a relation or a relation definition.
|
10
|
+
# It contains a map of names to the objects they name (e.g. logical var)
|
11
|
+
class Environment
|
12
|
+
# The parent (enclosing) environment.
|
13
|
+
# @return [Environment, NilClass]
|
14
|
+
attr_accessor :parent
|
15
|
+
|
16
|
+
# Mapping from user-defined name to related definition
|
17
|
+
# (say, a variable object)
|
18
|
+
# @return [Hash{String => Variable}] Pairs of the kind
|
19
|
+
attr_reader :defns
|
20
|
+
|
21
|
+
# Construct a environment instance.
|
22
|
+
# @param aParent [Environment, NilClass] Parent environment to this one.
|
23
|
+
def initialize(aParent = nil)
|
24
|
+
@parent = aParent
|
25
|
+
@defns = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add a new variable to the environment.
|
29
|
+
# @param anEntry [BackEnd::Variable]
|
30
|
+
# @return [BackEnd::Variable] the variable
|
31
|
+
def insert(anEntry)
|
32
|
+
e = validated_entry(anEntry)
|
33
|
+
# e.suffix = default_suffix if e.kind_of?(BackEnd::Variable)
|
34
|
+
defns[e.name] = e
|
35
|
+
|
36
|
+
e
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a string with a human-readable representation of the object.
|
40
|
+
# @return [String]
|
41
|
+
def inspect
|
42
|
+
+"#<#{self.class}:#{object_id.to_s(16)}>"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def validated_entry(anEntry)
|
48
|
+
name = anEntry.name
|
49
|
+
unless name.kind_of?(String) && !name.empty?
|
50
|
+
err_msg = 'Invalid variable name argument.'
|
51
|
+
raise StandardError, err_msg
|
52
|
+
end
|
53
|
+
if defns.include?(name) && !anEntry.kind_of?(Variable)
|
54
|
+
err_msg = "Variable with name '#{name}' already exists."
|
55
|
+
raise StandardError, err_msg
|
56
|
+
end
|
57
|
+
|
58
|
+
anEntry
|
59
|
+
end
|
60
|
+
|
61
|
+
# def default_suffix
|
62
|
+
# @default_suffix ||= "_#{object_id.to_s(16)}"
|
63
|
+
# end
|
64
|
+
end # class
|
65
|
+
end # module
|
66
|
+
end # module
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'environment'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module BackEnd
|
7
|
+
# A symbol table is basically a mapping from a name onto an object
|
8
|
+
# that holds information associated with that name. It is a data structure
|
9
|
+
# that keeps track of variables and their respective environment where they are
|
10
|
+
# declared. The key requirements for the symbol are:
|
11
|
+
# - To perform fast lookup operations: given a name, retrieve the corresponding
|
12
|
+
# object.
|
13
|
+
# - To allow the efficient insertion of names and related information
|
14
|
+
# - To support the nesting of environments
|
15
|
+
# - To handle the entry environment and exit environment events,
|
16
|
+
# - To cope with variable redefinition in nested environment
|
17
|
+
# The terminology 'symbol table' comes from the compiler design
|
18
|
+
# community.
|
19
|
+
class SymbolTable
|
20
|
+
# Mapping between a name and the environment(s) where it is defined
|
21
|
+
# @return [Hash{String => Array<Environment>}]
|
22
|
+
attr_reader :name2envs
|
23
|
+
|
24
|
+
# @return [Environment] The top-level environment (= root of environment tree)
|
25
|
+
attr_reader :root
|
26
|
+
|
27
|
+
# @return [Environment] The current environment.
|
28
|
+
attr_reader :current_env
|
29
|
+
|
30
|
+
# Build symbol table with given environment as root.
|
31
|
+
# @param anEnv [BackEnd::Environment,NilClass] The top-level Environment
|
32
|
+
def initialize(anEnv = nil)
|
33
|
+
@name2envs = {}
|
34
|
+
init_root(anEnv) # Set default (global) environment
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns iff there is no entry in the symbol table
|
38
|
+
# @return [Boolean]
|
39
|
+
def empty?
|
40
|
+
name2envs.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Use this method to signal the interpreter that a given environment
|
44
|
+
# to be a child of current environment and to be itself the new current environment.
|
45
|
+
# @param anEnv [BackEnd::Environment] the Environment that
|
46
|
+
def enter_environment(anEnv)
|
47
|
+
anEnv.parent = current_env
|
48
|
+
@current_env = anEnv
|
49
|
+
end
|
50
|
+
|
51
|
+
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
|
59
|
+
end
|
60
|
+
end
|
61
|
+
raise StandardError, 'Cannot remove root environment.' if current_env == root
|
62
|
+
|
63
|
+
@current_env = current_env.parent
|
64
|
+
end
|
65
|
+
|
66
|
+
# Add an entry with given name to current environment.
|
67
|
+
# @param anEntry [Variable]
|
68
|
+
# @return [String] Internal name of the entry
|
69
|
+
def insert(anEntry)
|
70
|
+
current_env.insert(anEntry)
|
71
|
+
name = anEntry.name
|
72
|
+
if name2envs.include?(name)
|
73
|
+
name2envs[name] << current_env
|
74
|
+
else
|
75
|
+
name2envs[name] = [current_env]
|
76
|
+
end
|
77
|
+
|
78
|
+
anEntry.name # anEntry.i_name
|
79
|
+
end
|
80
|
+
|
81
|
+
# Search for the object with the given name
|
82
|
+
# @param aName [String]
|
83
|
+
# @return [BackEnd::Variable]
|
84
|
+
def lookup(aName)
|
85
|
+
environments = name2envs.fetch(aName, nil)
|
86
|
+
return nil if environments.nil?
|
87
|
+
|
88
|
+
sc = environments.last
|
89
|
+
sc.defns[aName]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Search for the object with the given i_name
|
93
|
+
# @param anIName [String]
|
94
|
+
# @return [BackEnd::Variable]
|
95
|
+
# def lookup_i_name(anIName)
|
96
|
+
# found = nil
|
97
|
+
# environment = current_env
|
98
|
+
|
99
|
+
# begin
|
100
|
+
# found = environment.defns.values.find { |e| e.i_name == anIName }
|
101
|
+
# break if found
|
102
|
+
|
103
|
+
# environment = environment.parent
|
104
|
+
# end while environment
|
105
|
+
|
106
|
+
# found
|
107
|
+
# end
|
108
|
+
|
109
|
+
# Return all variables defined in the current .. root chain.
|
110
|
+
# Variables are sorted top-down and left-to-right.
|
111
|
+
def all_variables
|
112
|
+
vars = []
|
113
|
+
skope = current_env
|
114
|
+
while skope
|
115
|
+
vars_of_environment = skope.defns.select { |_, item| item.kind_of?(Variable) }
|
116
|
+
vars = vars_of_environment.values.concat(vars)
|
117
|
+
skope = skope.parent
|
118
|
+
end
|
119
|
+
|
120
|
+
vars
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def init_root(anEnv)
|
126
|
+
@root = valid_environment(anEnv)
|
127
|
+
@current_env = @root
|
128
|
+
end
|
129
|
+
|
130
|
+
def valid_environment(anEnv)
|
131
|
+
anEnv.nil? ? Environment.new : anEnv
|
132
|
+
end
|
133
|
+
end # class
|
134
|
+
end # module
|
135
|
+
end # module
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'entry'
|
4
|
+
require_relative '../datatype/all_datatypes'
|
5
|
+
|
6
|
+
module Loxxy
|
7
|
+
module BackEnd
|
8
|
+
# Representation of a Lox variable.
|
9
|
+
# It is a named slot that can be associated with a value at the time.
|
10
|
+
class Variable
|
11
|
+
include Entry # Add expected behaviour for symbol table entries
|
12
|
+
|
13
|
+
# @return [Datatype::BuiltinDatatype] the value assigned to the variable
|
14
|
+
attr_accessor :value
|
15
|
+
|
16
|
+
# Create a variable with given name and initial value
|
17
|
+
# @param aName [String] The name of the variable
|
18
|
+
# @param aValue [Datatype::BuiltinDatatype] the initial assigned value
|
19
|
+
def initialize(aName, aValue = Datatype::Nil.instance)
|
20
|
+
init_name(aName)
|
21
|
+
@value = aValue
|
22
|
+
end
|
23
|
+
end # class
|
24
|
+
end # module
|
25
|
+
end # module
|
@@ -46,8 +46,8 @@ module Loxxy
|
|
46
46
|
|
47
47
|
rule('funDecl' => 'FUN function')
|
48
48
|
|
49
|
-
rule('varDecl' => 'VAR IDENTIFIER SEMICOLON')
|
50
|
-
rule('varDecl' => 'VAR IDENTIFIER EQUAL expression SEMICOLON')
|
49
|
+
rule('varDecl' => 'VAR IDENTIFIER SEMICOLON').as 'var_declaration'
|
50
|
+
rule('varDecl' => 'VAR IDENTIFIER EQUAL expression SEMICOLON').as 'var_initialization'
|
51
51
|
|
52
52
|
# Statements: produce side effects, but don't introduce bindings
|
53
53
|
rule('statement' => 'exprStmt')
|
data/lib/loxxy/interpreter.rb
CHANGED
@@ -31,7 +31,7 @@ module Loxxy
|
|
31
31
|
ast_tree = parser.parse(lox_input)
|
32
32
|
visitor = Ast::ASTVisitor.new(ast_tree)
|
33
33
|
|
34
|
-
# Back-end launches the tree walking &
|
34
|
+
# Back-end launches the tree walking & responds to visit events
|
35
35
|
# by executing the code determined by the visited AST node.
|
36
36
|
engine = BackEnd::Engine.new(config)
|
37
37
|
engine.execute(visitor)
|
data/lib/loxxy/version.rb
CHANGED
@@ -31,8 +31,16 @@ module Loxxy
|
|
31
31
|
context 'Listening to visitor events:' do
|
32
32
|
let(:greeting) { Datatype::LXString.new('Hello, world') }
|
33
33
|
let(:sample_pos) { double('fake-position') }
|
34
|
+
let(:var_decl) { Ast::LoxVarStmt.new(sample_pos, 'greeting', greeting) }
|
34
35
|
let(:lit_expr) { Ast::LoxLiteralExpr.new(sample_pos, greeting) }
|
35
36
|
|
37
|
+
it "should react to 'after_var_stmt' event" do
|
38
|
+
expect { subject.after_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
|
+
expect(current_env.defns['greeting'].value).to eq(greeting)
|
42
|
+
end
|
43
|
+
|
36
44
|
it "should react to 'before_literal_expr' event" do
|
37
45
|
expect { subject.before_literal_expr(lit_expr) }.not_to raise_error
|
38
46
|
expect(subject.stack.pop).to eq(greeting)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper' # Use the RSpec framework
|
4
|
+
|
5
|
+
# Load the class under test
|
6
|
+
require_relative '../../lib/loxxy/back_end/environment'
|
7
|
+
|
8
|
+
module Loxxy
|
9
|
+
module BackEnd
|
10
|
+
describe Environment do
|
11
|
+
let(:foo) { Datatype::LXString.new('foo') }
|
12
|
+
let(:bar) { Datatype::LXString.new('bar') }
|
13
|
+
let(:mother) { Environment.new }
|
14
|
+
subject { Environment.new(mother) }
|
15
|
+
|
16
|
+
# Shortand factory method.
|
17
|
+
def var(aName, aValue)
|
18
|
+
Variable.new(aName, aValue)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'Initialization:' do
|
22
|
+
it 'could be initialized without argument' do
|
23
|
+
expect { Environment.new }.not_to raise_error
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'could be initialized with a parent environment' do
|
27
|
+
expect { Environment.new(mother) }.not_to raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it "shouldn't have definitions by default" do
|
31
|
+
expect(subject.defns).to be_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should know its parent (if any)' do
|
35
|
+
expect(subject.parent).to eq(mother)
|
36
|
+
end
|
37
|
+
end # context
|
38
|
+
|
39
|
+
context 'Provided services:' do
|
40
|
+
it 'should accept the addition of a variable' do
|
41
|
+
subject.insert(var('a', foo))
|
42
|
+
expect(subject.defns).not_to be_empty
|
43
|
+
var_a = subject.defns['a']
|
44
|
+
expect(var_a).to be_kind_of(Variable)
|
45
|
+
expect(var_a.name).to eq('a')
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should accept the addition of multiple variables' do
|
49
|
+
subject.insert(var('a', foo))
|
50
|
+
expect(subject.defns).not_to be_empty
|
51
|
+
|
52
|
+
subject.insert(var('b', bar))
|
53
|
+
var_b = subject.defns['b']
|
54
|
+
expect(var_b).to be_kind_of(Variable)
|
55
|
+
expect(var_b.name).to eq('b')
|
56
|
+
end
|
57
|
+
|
58
|
+
# it 'should set the suffix of just created variable' do
|
59
|
+
# subject.insert(var('a'))
|
60
|
+
# var_a = subject.defns['a']
|
61
|
+
# expect(var_a.suffix).to eq("_#{subject.object_id.to_s(16)}")
|
62
|
+
# end
|
63
|
+
|
64
|
+
# it 'should complain when variable names collide' do
|
65
|
+
# subject.insert(var('c'))
|
66
|
+
# expect(subject.defns['c']).to be_kind_of(Datatype::Variable)
|
67
|
+
# err = StandardError
|
68
|
+
# err_msg = "Variable with name 'c' already exists."
|
69
|
+
# expect { subject.insert(var('c')) }.to raise_error(err, err_msg)
|
70
|
+
# end
|
71
|
+
end # context
|
72
|
+
end # describe
|
73
|
+
end # module
|
74
|
+
end # module
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper' # Use the RSpec framework
|
4
|
+
require_relative '../../lib/loxxy/back_end/variable'
|
5
|
+
|
6
|
+
# Load the class under test
|
7
|
+
require_relative '../../lib/loxxy/back_end/symbol_table'
|
8
|
+
|
9
|
+
module Loxxy
|
10
|
+
module BackEnd
|
11
|
+
describe SymbolTable do
|
12
|
+
subject { SymbolTable.new }
|
13
|
+
|
14
|
+
context 'Initialization:' do
|
15
|
+
it 'should be initialized without argument' do
|
16
|
+
expect { SymbolTable.new }.not_to raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should have a root BackEnd' do
|
20
|
+
expect(subject.root).not_to be_nil
|
21
|
+
expect(subject.current_env).to eq(subject.root)
|
22
|
+
expect(subject.root).to be_kind_of(BackEnd::Environment)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "shouldn't have names at initialization" do
|
26
|
+
expect(subject.name2envs).to be_empty
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should be empty at initialization' do
|
30
|
+
expect(subject).to be_empty
|
31
|
+
end
|
32
|
+
end # context
|
33
|
+
|
34
|
+
context 'Provided services:' do
|
35
|
+
def var(aName)
|
36
|
+
Variable.new(aName)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should allow the addition of a variable' do
|
40
|
+
expect { subject.insert(var('q')) }.not_to raise_error
|
41
|
+
expect(subject).not_to be_empty
|
42
|
+
expect(subject.name2envs['q']).to be_kind_of(Array)
|
43
|
+
expect(subject.name2envs['q'].size).to eq(1)
|
44
|
+
expect(subject.name2envs['q'].first).to eq(subject.current_env)
|
45
|
+
expect(subject.current_env.defns['q']).to be_kind_of(BackEnd::Variable)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should allow the addition of several labels for same env' do
|
49
|
+
i_name = subject.insert(var('q'))
|
50
|
+
#expect(i_name).to match(/^q_[0-9a-z]*$/)
|
51
|
+
|
52
|
+
expect { subject.insert(var('x')) }.not_to raise_error
|
53
|
+
expect(subject.name2envs['x']).to be_kind_of(Array)
|
54
|
+
expect(subject.name2envs['x'].first).to eq(subject.current_env)
|
55
|
+
expect(subject.current_env.defns['x']).to be_kind_of(BackEnd::Variable)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should allow the entry into a new scope' do
|
59
|
+
subject.insert(var('q'))
|
60
|
+
new_env = BackEnd::Environment.new
|
61
|
+
expect { subject.enter_environment(new_env) }.not_to raise_error
|
62
|
+
expect(subject.current_env).to eq(new_env)
|
63
|
+
expect(subject.current_env.parent).to eq(subject.root)
|
64
|
+
expect(subject.name2envs['q']).to eq([subject.root])
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should allow the addition of same name in different scopes' do
|
68
|
+
subject.insert(var('q'))
|
69
|
+
subject.enter_environment(BackEnd::Environment.new)
|
70
|
+
subject.insert(var('q'))
|
71
|
+
expect(subject.name2envs['q']).to be_kind_of(Array)
|
72
|
+
expect(subject.name2envs['q'].size).to eq(2)
|
73
|
+
expect(subject.name2envs['q'].first).to eq(subject.root)
|
74
|
+
expect(subject.name2envs['q'].last).to eq(subject.current_env)
|
75
|
+
expect(subject.current_env.defns['q']).to be_kind_of(BackEnd::Variable)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should allow the removal of a scope' do
|
79
|
+
subject.insert(var('q'))
|
80
|
+
new_env = BackEnd::Environment.new
|
81
|
+
subject.enter_environment(new_env)
|
82
|
+
subject.insert(var('q'))
|
83
|
+
expect(subject.name2envs['q'].size).to eq(2)
|
84
|
+
|
85
|
+
expect { subject.leave_environment }.not_to raise_error
|
86
|
+
expect(subject.current_env).to eq(subject.root)
|
87
|
+
expect(subject.name2envs['q'].size).to eq(1)
|
88
|
+
expect(subject.name2envs['q']).to eq([subject.root])
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should allow the search of an entry based on its name' do
|
92
|
+
subject.insert(var('q'))
|
93
|
+
subject.insert(var('x'))
|
94
|
+
subject.enter_environment(BackEnd::Environment.new)
|
95
|
+
subject.insert(var('q'))
|
96
|
+
subject.insert(var('y'))
|
97
|
+
|
98
|
+
# Search for unknown name
|
99
|
+
expect(subject.lookup('z')).to be_nil
|
100
|
+
|
101
|
+
# Search for existing unique names
|
102
|
+
expect(subject.lookup('y')).to eq(subject.current_env.defns['y'])
|
103
|
+
expect(subject.lookup('x')).to eq(subject.root.defns['x'])
|
104
|
+
|
105
|
+
# Search for redefined name
|
106
|
+
expect(subject.lookup('q')).to eq(subject.current_env.defns['q'])
|
107
|
+
end
|
108
|
+
|
109
|
+
# it 'should allow the search of an entry based on its i_name' do
|
110
|
+
# subject.insert(var('q'))
|
111
|
+
# i_name_x = subject.insert(var('x'))
|
112
|
+
# subject.enter_environment(BackEnd::Environment.new)
|
113
|
+
# i_name_q2 = subject.insert(var('q'))
|
114
|
+
# i_name_y = subject.insert(var('y'))
|
115
|
+
|
116
|
+
# # Search for unknown i_name
|
117
|
+
# expect(subject.lookup_i_name('dummy')).to be_nil
|
118
|
+
|
119
|
+
# curr_scope = subject.current_env
|
120
|
+
# # # Search for existing unique names
|
121
|
+
# expect(subject.lookup_i_name(i_name_y)).to eq(curr_scope.defns['y'])
|
122
|
+
# expect(subject.lookup_i_name(i_name_x)).to eq(subject.root.defns['x'])
|
123
|
+
|
124
|
+
# # Search for redefined name
|
125
|
+
# expect(subject.lookup_i_name(i_name_q2)).to eq(curr_scope.defns['q'])
|
126
|
+
# end
|
127
|
+
|
128
|
+
it 'should list all the variables defined in all the szcope chain' do
|
129
|
+
subject.insert(var('q'))
|
130
|
+
subject.enter_environment(BackEnd::Environment.new)
|
131
|
+
subject.insert(var('x'))
|
132
|
+
subject.enter_environment(BackEnd::Environment.new)
|
133
|
+
subject.insert(var('y'))
|
134
|
+
subject.insert(var('x'))
|
135
|
+
|
136
|
+
vars = subject.all_variables
|
137
|
+
expect(vars.map(&:name)).to eq(%w[q x y x])
|
138
|
+
end
|
139
|
+
end # context
|
140
|
+
end # describe
|
141
|
+
end # module
|
142
|
+
end # module
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper' # Use the RSpec framework
|
4
|
+
|
5
|
+
# Load the class under test
|
6
|
+
require_relative '../../lib/loxxy/back_end/variable'
|
7
|
+
|
8
|
+
|
9
|
+
module Loxxy
|
10
|
+
module BackEnd
|
11
|
+
describe Variable do
|
12
|
+
let(:sample_name) { 'iAmAVariable' }
|
13
|
+
let(:sample_value) { 'here is my value' }
|
14
|
+
subject { Variable.new(sample_name, sample_value) }
|
15
|
+
|
16
|
+
context 'Initialization:' do
|
17
|
+
it 'should be initialized with a name and a value, or...' do
|
18
|
+
expect { Variable.new(sample_name, sample_value) }.not_to raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should be initialized with just a name' do
|
22
|
+
expect { Variable.new(sample_name) }.not_to raise_error
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should know its name' do
|
26
|
+
expect(subject.name).to eq(sample_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should have a frozen name' do
|
30
|
+
expect(subject.name).to be_frozen
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should know its value (if provided)' do
|
34
|
+
expect(subject.value).to eq(sample_value)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should have a nil value otherwise' do
|
38
|
+
instance = Variable.new(sample_name)
|
39
|
+
expect(instance.value).to eq(Datatype::Nil.instance)
|
40
|
+
end
|
41
|
+
|
42
|
+
# it 'should know its default internal name' do
|
43
|
+
# # By default: internal name == label
|
44
|
+
# expect(subject.i_name).to eq(subject.label)
|
45
|
+
# end
|
46
|
+
|
47
|
+
# it 'should have a nil suffix' do
|
48
|
+
# expect(subject.suffix).to be_nil
|
49
|
+
# end
|
50
|
+
end # context
|
51
|
+
|
52
|
+
context 'Provided service:' do
|
53
|
+
let(:sample_suffix) { 'sample-suffix' }
|
54
|
+
it 'should have a label equal to its user-defined name' do
|
55
|
+
# expect(subject.label).to eq(subject.name)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should accept a suffix' do
|
59
|
+
# expect { subject.suffix = sample_suffix }.not_to raise_error
|
60
|
+
# expect(subject.suffix).to eq(sample_suffix)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should calculate its internal name' do
|
64
|
+
# # Rule: empty suffix => internal name == label
|
65
|
+
# subject.suffix = ''
|
66
|
+
# expect(subject.i_name).to eq(subject.label)
|
67
|
+
|
68
|
+
# # Rule: suffix starting underscore: internal name = label + suffix
|
69
|
+
# subject.suffix = '_10'
|
70
|
+
# expect(subject.i_name).to eq(subject.label + subject.suffix)
|
71
|
+
|
72
|
+
# # Rule: ... otherwise: internal name == suffix
|
73
|
+
# subject.suffix = sample_suffix
|
74
|
+
# expect(subject.i_name).to eq(subject.suffix)
|
75
|
+
end
|
76
|
+
end # context
|
77
|
+
end # describe
|
78
|
+
end # module
|
79
|
+
end # module
|
data/spec/interpreter_spec.rb
CHANGED
@@ -211,7 +211,7 @@ module Loxxy
|
|
211
211
|
end
|
212
212
|
end
|
213
213
|
|
214
|
-
it 'should
|
214
|
+
it 'should support expressions between parentheses' do
|
215
215
|
[
|
216
216
|
['3 + 4 * 5;', 23],
|
217
217
|
['(3 + 4) * 5;', 35],
|
@@ -250,6 +250,15 @@ module Loxxy
|
|
250
250
|
end
|
251
251
|
end
|
252
252
|
|
253
|
+
it 'should accept variable declarations' do
|
254
|
+
# Variable with initialization value
|
255
|
+
var_decl = 'var iAmAVariable = "here is my value";'
|
256
|
+
expect { subject.evaluate(var_decl) }.not_to raise_error
|
257
|
+
|
258
|
+
# Variable without initialization value
|
259
|
+
expect { subject.evaluate('var iAmNil;') }.not_to raise_error
|
260
|
+
end
|
261
|
+
|
253
262
|
it 'should print the hello world message' do
|
254
263
|
expect { subject.evaluate(hello_world) }.not_to raise_error
|
255
264
|
expect(sample_cfg[:ostream].string).to eq('Hello, world!')
|
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.0.
|
4
|
+
version: 0.0.21
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitri Geshef
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -97,7 +97,12 @@ files:
|
|
97
97
|
- lib/loxxy/ast/lox_noop_expr.rb
|
98
98
|
- lib/loxxy/ast/lox_print_stmt.rb
|
99
99
|
- lib/loxxy/ast/lox_unary_expr.rb
|
100
|
+
- lib/loxxy/ast/lox_var_stmt.rb
|
100
101
|
- lib/loxxy/back_end/engine.rb
|
102
|
+
- lib/loxxy/back_end/entry.rb
|
103
|
+
- lib/loxxy/back_end/environment.rb
|
104
|
+
- lib/loxxy/back_end/symbol_table.rb
|
105
|
+
- lib/loxxy/back_end/variable.rb
|
101
106
|
- lib/loxxy/datatype/all_datatypes.rb
|
102
107
|
- lib/loxxy/datatype/boolean.rb
|
103
108
|
- lib/loxxy/datatype/builtin_datatype.rb
|
@@ -115,6 +120,9 @@ files:
|
|
115
120
|
- lib/loxxy/version.rb
|
116
121
|
- loxxy.gemspec
|
117
122
|
- spec/back_end/engine_spec.rb
|
123
|
+
- spec/back_end/environment_spec.rb
|
124
|
+
- spec/back_end/symbol_table_spec.rb
|
125
|
+
- spec/back_end/variable_spec.rb
|
118
126
|
- spec/datatype/boolean_spec.rb
|
119
127
|
- spec/datatype/lx_string_spec.rb
|
120
128
|
- spec/datatype/nil_spec.rb
|
@@ -151,6 +159,9 @@ specification_version: 4
|
|
151
159
|
summary: An implementation of the Lox programming language. WIP
|
152
160
|
test_files:
|
153
161
|
- spec/back_end/engine_spec.rb
|
162
|
+
- spec/back_end/environment_spec.rb
|
163
|
+
- spec/back_end/symbol_table_spec.rb
|
164
|
+
- spec/back_end/variable_spec.rb
|
154
165
|
- spec/datatype/boolean_spec.rb
|
155
166
|
- spec/datatype/lx_string_spec.rb
|
156
167
|
- spec/datatype/nil_spec.rb
|