loxxy 0.0.17 → 0.0.22

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.
@@ -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
@@ -28,6 +28,13 @@ module Loxxy
28
28
  true # Default implementation
29
29
  end
30
30
 
31
+ # Check for inequality of this object with another Lox object
32
+ # @param other [Datatype::BuiltinDatatype, Object]
33
+ # @return [Datatype::Boolean]
34
+ def !=(other)
35
+ !(self == other)
36
+ end
37
+
31
38
  # Negation ('not')
32
39
  # Returns a boolean with opposite truthiness value.
33
40
  # @return [Datatype::Boolean]
@@ -35,11 +42,18 @@ module Loxxy
35
42
  falsey? ? True.instance : False.instance
36
43
  end
37
44
 
38
- # Check for inequality of this object with another Lox object
39
- # @param other [Datatype::BuiltinDatatype, Object]
40
- # @return [Datatype::Boolean]
41
- def !=(other)
42
- !(self == other)
45
+ # Returns the first falsey argument (if any),
46
+ # otherwise returns the last truthy argument.
47
+ # @param operand2 [Loxxy::Datatype::BuiltinDatatype, Proc]
48
+ def and(operand2)
49
+ falsey? ? self : logical_2nd_arg(operand2)
50
+ end
51
+
52
+ # Returns the first truthy argument (if any),
53
+ # otherwise returns the last falsey argument.
54
+ # @param operand2 [Loxxy::Datatype::BuiltinDatatype, Proc]
55
+ def or(operand2)
56
+ truthy? ? self : logical_2nd_arg(operand2)
43
57
  end
44
58
 
45
59
  # Method called from Lox to obtain the text representation of the boolean.
@@ -53,6 +67,19 @@ module Loxxy
53
67
  def validated_value(aValue)
54
68
  aValue
55
69
  end
70
+
71
+ def logical_2nd_arg(operand2)
72
+ case operand2
73
+ when false
74
+ False.instance # Convert to Lox equivalent
75
+ when nil
76
+ Nil.instance # Convert to Lox equivalent
77
+ when true
78
+ True.instance # Convert to Lox equivalent
79
+ else
80
+ operand2
81
+ end
82
+ end
56
83
  end # class
57
84
  end # module
58
85
  end # module
@@ -30,8 +30,8 @@ module Loxxy
30
30
  rule('program' => 'declaration_plus EOF').as 'lox_program'
31
31
 
32
32
  # Declarations: bind an identifier to something
33
- rule('declaration_plus' => 'declaration_plus declaration')
34
- rule('declaration_plus' => 'declaration')
33
+ rule('declaration_plus' => 'declaration_plus declaration').as 'declaration_plus_more'
34
+ rule('declaration_plus' => 'declaration').as 'declaration_plus_end'
35
35
  rule('declaration' => 'classDecl')
36
36
  rule('declaration' => 'funDecl')
37
37
  rule('declaration' => 'varDecl')
@@ -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')
@@ -68,9 +68,9 @@ module Loxxy
68
68
  rule('forTest' => 'expression_opt SEMICOLON')
69
69
  rule('forUpdate' => 'expression_opt')
70
70
 
71
- rule('ifStmt' => 'IF ifCondition statement elsePart_opt')
72
- rule('ifCondition' => 'LEFT_PAREN expression RIGHT_PAREN')
73
- rule('elsePart_opt' => 'ELSE statement')
71
+ rule('ifStmt' => 'IF ifCondition statement elsePart_opt').as 'if_stmt'
72
+ rule('ifCondition' => 'LEFT_PAREN expression RIGHT_PAREN').as 'keep_symbol2'
73
+ rule('elsePart_opt' => 'ELSE statement').as 'keep_symbol2'
74
74
  rule('elsePart_opt' => [])
75
75
 
76
76
  rule('printStmt' => 'PRINT expression SEMICOLON').as 'print_stmt'
@@ -137,8 +137,8 @@ module Loxxy
137
137
  rule('primary' => 'THIS')
138
138
  rule('primary' => 'NUMBER').as 'literal_expr'
139
139
  rule('primary' => 'STRING').as 'literal_expr'
140
- rule('primary' => 'IDENTIFIER')
141
- rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN')
140
+ rule('primary' => 'IDENTIFIER').as 'variable_expr'
141
+ rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN').as 'grouping_expr'
142
142
  rule('primary' => 'SUPER DOT IDENTIFIER')
143
143
 
144
144
  # Utility rules
@@ -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 & reponds to visit events
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Loxxy
4
- VERSION = '0.0.17'
4
+ VERSION = '0.0.22'
5
5
  end
@@ -31,8 +31,17 @@ 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
+
38
+ it "should react to 'after_var_stmt' event" do
39
+ expect { subject.after_var_stmt(var_decl) }.not_to raise_error
40
+ current_env = subject.symbol_table.current_env
41
+ expect(current_env.defns['greeting']).to be_kind_of(Variable)
42
+ expect(current_env.defns['greeting'].value).to eq(greeting)
43
+ end
44
+
36
45
  it "should react to 'before_literal_expr' event" do
37
46
  expect { subject.before_literal_expr(lit_expr) }.not_to raise_error
38
47
  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