mirror of
https://github.com/safiire/n65.git
synced 2024-12-12 00:29:03 +00:00
Creating a symbol table that has scope
This commit is contained in:
parent
2dca12a519
commit
4235742c7d
126
lib/symbol_table.rb
Normal file
126
lib/symbol_table.rb
Normal file
@ -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
|
128
test/test_symbol_table.rb
Normal file
128
test/test_symbol_table.rb
Normal file
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user