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