diff --git a/lib/symbol_table.rb b/lib/symbol_table.rb new file mode 100644 index 0000000..de5a07d --- /dev/null +++ b/lib/symbol_table.rb @@ -0,0 +1,126 @@ +module Assembler6502 + + class SymbolTable + + ##### Custom Exceptions + class InvalidScope < StandardError; end + class UndefinedSymbol < StandardError; end + class CantExitScope < StandardError; end + + + #### + ## Initialize a symbol table that begins in global scope + def initialize + @symbols = { + :global => {} + } + @anonymous_scope_number = 0 + @scope_stack = [:global] + end + + + #### + ## Define a new scope, which can be anonymous or named + ## and switch into that scope + def enter_scope(name = nil) + name = generate_name if name.nil? + name = name.to_sym + scope = current_scope + if scope.has_key?(name) + path_string = generate_scope_path(path_ary) + fail(InvalidScope, "Scope: #{path_string} already exists") + end + scope[name] = {} + @scope_stack.push(name) + end + + + #### + ## Exit the current scope + def exit_scope + if @scope_stack.size == 1 + fail(CantExitScope, "You cannot exit global scope") + end + @scope_stack.pop + end + + + #### + ## Define a symbol in the current scope + def define_symbol(symbol, value) + scope = current_scope + scope[symbol.to_sym] = value + end + + + #### + ## Resolve symbol to a value, for example: + ## scope1.scope2.variable + ## It is not nessessary to specify the root scope :global + ## You can just address anything by name in the current scope + ## To go backwards in scope you need to write the full path + ## like global.sprite.x or whatever + def resolve_symbol(name) + value = if name.include?('.') + path_ary = name.split('.').map(&:to_sym) + symbol = path_ary.pop + path_ary.shift if path_ary.first == :global + scope = retreive_scope(path_ary) + scope[symbol] + else + scope = current_scope + scope[name.to_sym] + end + + if value.nil? + fail(UndefinedSymbol, name) + end + value + end + + private + + #### + ## A bit more clearly states to get the current scope + def current_scope + retreive_scope + end + + + #### + ## Retrieve a reference to a scope, current scope by default + def retreive_scope(path_ary = @scope_stack) + path_ary = path_ary.dup + path_ary.unshift(:global) unless path_ary.first == :global + + path_ary.inject(@symbols) do |scope, path_component| + new_scope = scope[path_component.to_sym] + + if new_scope.nil? + path_string = generate_scope_path(path_ary) + message = "Resolving scope: #{path_string} failed at #{path_component}" + fail(InvalidScope, message) if new_scope.nil? + end + + new_scope + end + end + + + #### + ## Generate a scope path from an array + def generate_scope_path(path_ary) + path_ary.join('.') + end + + + #### + ## Generate an anonymous scope name + def generate_name + @anonymous_scope_number += 1 + "anonymous_#{@anonymous_scope_number}" + end + + end + +end diff --git a/test/test_symbol_table.rb b/test/test_symbol_table.rb new file mode 100644 index 0000000..539b35c --- /dev/null +++ b/test/test_symbol_table.rb @@ -0,0 +1,128 @@ +gem 'minitest' +require 'minitest/autorun' +require 'minitest/unit' + +require_relative '../lib/symbol_table.rb' + + +class TestAssembler < MiniTest::Test + include Assembler6502 + + #### + ## Test that we can make simple global symbols + def _test_define_global_symbols + st = SymbolTable.new + st.define_symbol('dog', 'woof') + assert_equal('woof', st.resolve_symbol('dog')) + end + + + #### + ## Test entering into a sub scope, and setting and retrieving values + def _test_enter_scope + st = SymbolTable.new + st.enter_scope('animals') + st.define_symbol('dog', 'woof') + assert_equal('woof', st.resolve_symbol('dog')) + end + + + #### + ## Test exiting a sub scope, and seeing that the variable is unavailable by simple name + def _test_exit_scope + st = SymbolTable.new + st.enter_scope('animals') + st.define_symbol('dog', 'woof') + assert_equal('woof', st.resolve_symbol('dog')) + + st.exit_scope + + assert_raises(SymbolTable::UndefinedSymbol) do + assert_equal('woof', st.resolve_symbol('dog')) + end + end + + + #### + ## Test exiting a sub scope, and being able to access a symbol through a full path + def test_exit_scope + st = SymbolTable.new + st.enter_scope('animals') + st.define_symbol('dog', 'woof') + assert_equal('woof', st.resolve_symbol('dog')) + + st.exit_scope + + assert_equal('woof', st.resolve_symbol('animals.dog')) + end + + + #### + ## Have two symbols that are the same but are in different scopes + def test_two_scopes_same_symbol + st = SymbolTable.new + st.define_symbol('dog', 'woof') + assert_equal('woof', st.resolve_symbol('dog')) + + st.enter_scope('animals') + + st.define_symbol('dog', 'woofwoof') + assert_equal('woofwoof', st.resolve_symbol('dog')) + + st.exit_scope + + assert_equal('woof', st.resolve_symbol('dog')) + assert_equal('woofwoof', st.resolve_symbol('animals.dog')) + end + + + #### + ## How do you get stuff out of the global scope when you are in + ## a sub scope? + def test_access_global_scope + st = SymbolTable.new + st.define_symbol('dog', 'woof') + assert_equal('woof', st.resolve_symbol('dog')) + + st.enter_scope('animals') + st.define_symbol('pig', 'oink') + assert_equal('oink', st.resolve_symbol('pig')) + + ## Ok, now I want to access global.dog basically from the previous scope + assert_equal('woof', st.resolve_symbol('global.dog')) + end + + + #### + ## Now I want to just test making an anonymous scope + def test_anonymous_scope + st = SymbolTable.new + st.define_symbol('dog', 'woof') + assert_equal('woof', st.resolve_symbol('dog')) + + st.enter_scope + st.define_symbol('pig', 'oink') + assert_equal('oink', st.resolve_symbol('pig')) + + ## Ok, now I want to access global.dog basically from the previous scope + assert_equal('woof', st.resolve_symbol('global.dog')) + + ## Now exit the anonymous scope and get dog + st.exit_scope + assert_equal('woof', st.resolve_symbol('global.dog')) + assert_equal('woof', st.resolve_symbol('dog')) + end + + + #### + ## Now I want to test that I cannot exist the outer-most + ## global scope by mistake + def test_cant_exit_global + st = SymbolTable.new + assert_raises(SymbolTable::CantExitScope) do + st.exit_scope + end + end + +end +