diff --git a/spec/lib/n65/symbol_table_spec.rb b/spec/lib/n65/symbol_table_spec.rb new file mode 100644 index 0000000..99ac6b4 --- /dev/null +++ b/spec/lib/n65/symbol_table_spec.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +require_relative '../../../lib/n65/symbol_table' +require_relative '../../../lib/n65' + +RSpec.describe(N65::SymbolTable) do + subject { described_class.new } + + context 'when defining a global symbol' do + before { subject.define_symbol('dog', 'woof') } + + it 'can resolve the value' do + expect(subject.resolve_symbol('dog')).to eq('woof') + end + end + + context 'when entering a sub-scope' do + before do + subject.enter_scope('animals') + subject.define_symbol('dog', 'woof') + end + + it 'can resolve the value' do + expect(subject.resolve_symbol('dog')).to eq('woof') + end + + it 'can resolve the value with full dot syntax' do + expect(subject.resolve_symbol('animals.dog')).to eq('woof') + end + end + + context 'when accessing a symbol at higher scope' do + before do + subject.enter_scope('outer') + subject.define_symbol('dog', 'woof') + subject.enter_scope('inner') + subject.define_symbol('pig', 'oink') + end + + it 'can resolve the outer value without dot syntax' do + expect(subject.resolve_symbol('dog')).to eq('woof') + end + + it 'can resolve the value in current scope' do + expect(subject.resolve_symbol('pig')).to eq('oink') + end + end + + context 'when a symbol from an outer scope is shadowed' do + before do + subject.enter_scope('outer') + subject.define_symbol('dog', 'woof') + subject.enter_scope('inner') + subject.define_symbol('dog', 'bark') + end + + it 'the inner scope shadows the outer' do + expect(subject.resolve_symbol('dog')).to eq('bark') + end + + it 'does not shadow it when we leave the inner scope' do + subject.exit_scope + expect(subject.resolve_symbol('dog')).to eq('woof') + end + + it 'can access inner via dot syntax if we exit both scopes' do + subject.exit_scope + subject.exit_scope + expect(subject.resolve_symbol('outer.inner.dog')).to eq('bark') + end + end + + context 'when trying to access a symbol not in scope' do + before do + subject.enter_scope('animals') + subject.define_symbol('dog', 'woof') + subject.exit_scope + end + + it 'is undefined' do + expect { subject.resolve_symbol('dog') }.to raise_error(described_class::UndefinedSymbol) + end + end + + context 'when trying to access a symbol not in scope' do + before do + subject.enter_scope('animals') + subject.define_symbol('dog', 'woof') + subject.exit_scope + end + + it 'can be accessed by full path' do + expect(subject.resolve_symbol('animals.dog')).to eq('woof') + end + end + + context 'when we have two symbols with the same name in different scopes' do + before do + subject.define_symbol('dog', 'woof') + subject.enter_scope('animals') + subject.define_symbol('dog', 'woof woof') + subject.exit_scope + end + + it 'can access each by full path' do + expect(subject.resolve_symbol('dog')).to eq('woof') + end + + it 'can access each by full path' do + expect(subject.resolve_symbol('animals.dog')).to eq('woof woof') + end + end + + context 'when trying to access symbols at top scope' do + before do + subject.define_symbol('dog', 'woof') + subject.enter_scope('animals') + subject.define_symbol('dog', 'woof woof') + end + + it 'can use the global prefix' do + expect(subject.resolve_symbol('global.dog')).to eq('woof') + end + end + + context 'when creating an anonymous scope' do + before do + subject.define_symbol('dog', 'woof') + subject.enter_scope + subject.define_symbol('dog', 'woof woof') + end + + it 'gets the value in the current anonymous scope' do + expect(subject.resolve_symbol('dog')).to eq('woof woof') + end + + it 'can get the outer dog by dot syntax' do + expect(subject.resolve_symbol('global.dog')).to eq('woof') + end + + it 'can get the outer dog by exiting anonymous scope and resolving' do + subject.exit_scope + expect(subject.resolve_symbol('dog')).to eq('woof') + end + end + + context 'when trying to exit the top most scope' do + it 'raises an error' do + expect { subject.exit_scope }.to raise_error(described_class::CantExitScope) + end + end + + context 'when checking the address value of a scope' do + let(:assembler) { N65::Assembler.new } + let(:program) do + <<~'ASM' + .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} + .org $8000 + .segment prog 0 + .scope main + sei + cld + lda #$00 + jmp main + . + jmp main + jmp global.main + ASM + end + + before do + program.split(/\n/).each { |line| assembler.assemble_one_line(line) } + assembler.fulfill_promises + end + + it 'assigns the value of the scope main to the program counter value' do + expect(assembler.symbol_table.resolve_symbol('global.main')).to eq(0x8000) + end + end + + context 'when we try to jump to a forward declared symbol' do + let(:assembler) { N65::Assembler.new } + let(:program) do + <<~'ASM' + .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} + .org $8000 + .scope main + sei + cld + lda #$00 + bne forward_symbol + nop + nop + nop + forward_symbol: + rts + . + ASM + end + let(:correct_binary) do + [ + 0x4e, 0x45, 0x53, 0x1a, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x78, # SEI + 0xd8, # CLD + 0xa9, 0x0, # LDA immediate 0 + 0xd0, 0x3, # BNE +3 + 0xea, # NOP + 0xea, # NOP + 0xea, # NOP + 0x60 # RTS forward_symbol + ] + end + let(:emitted_rom) { assembler.emit_binary_rom.bytes[0...26] } + + before do + program.split(/\n/).each { |line| assembler.assemble_one_line(line) } + assembler.fulfill_promises + end + + it 'assembles the branch to forward_symbol correctly' do + expect(emitted_rom).to eq(correct_binary) + end + end +end diff --git a/test/test_memory_space.rb b/test/test_memory_space.rb deleted file mode 100644 index 53dcdf9..0000000 --- a/test/test_memory_space.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -gem 'minitest' -require 'minitest/autorun' -require 'minitest/unit' - -require_relative '../lib/n65/memory_space' - -class TestMemorySpace < MiniTest::Test - include N65 - - def test_create_prog_rom - # First just try to read alll of it - space = MemorySpace.create_prog_rom - contents = space.read(0x8000, 0x4000) - assert_equal(contents.size, 0x4000) - assert(contents.all?(&:zero?)) - - # It is mirrored so this should also work - space = MemorySpace.create_prog_rom - contents = space.read(0xC000, 0x4000) - assert_equal(contents.size, 0x4000) - assert(contents.all?(&:zero?)) - end - - def test_writing - # Write some bytes into prog 2 area - space = MemorySpace.create_prog_rom - space.write(0xC000, 'hi there'.bytes) - - # Read them back.. - contents = space.read(0xC000, 8) - assert_equal('hi there', contents.pack('C*')) - - # Should be mirrored in prog 1 - contents = space.read(0x8000, 8) - assert_equal('hi there', contents.pack('C*')) - end - - def test_reading_out_of_bounds - space = MemorySpace.create_prog_rom - assert_raises(MemorySpace::AccessOutsideProgRom) do - space.read(0x200, 10) - end - - # But that is valid char rom area, so no explody - space = MemorySpace.create_char_rom - space.read(0x200, 10) - - # But something like this should explode - space = MemorySpace.create_char_rom - assert_raises(MemorySpace::AccessOutsideCharRom) do - space.read(0x8001, 10) - end - end - - # There seem to be problems writing bytes right to - # the end of the memory map, specifically where the - # vector table is in prog rom, so let's test that. - def test_writing_to_end - space = MemorySpace.create_prog_rom - bytes = [0xDE, 0xAD] - - # Write the NMI address to FFFA - space.write(0xFFFA, bytes) - - # Write the entry point to FFFC - space.write(0xFFFC, bytes) - - # Write the irq to FFFE, and this fails, saying - # I'm trying to write to $10000 for some reason. - space.write(0xFFFE, bytes) - - # Write to the very first - space.write(0x8000, bytes) - end -end diff --git a/test/test_symbol_table.rb b/test/test_symbol_table.rb deleted file mode 100644 index 4455b56..0000000 --- a/test/test_symbol_table.rb +++ /dev/null @@ -1,214 +0,0 @@ -# frozen_string_literal: true - -gem 'minitest' -require 'minitest/autorun' -require 'minitest/unit' - -require_relative '../lib/n65/symbol_table' -require_relative '../lib/n65' - -class TestSymbolTable < MiniTest::Test - include N65 - - # 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 - - # Access something from an outer scope without dot syntax - def test_outer_scope - st = SymbolTable.new - st.enter_scope('outer') - st.define_symbol('dog', 'woof') - st.enter_scope('inner') - st.define_symbol('pig', 'oink') - assert_equal('woof', st.resolve_symbol('dog')) - end - - # Access something from an outer scope without dot syntax - def test_shadow - st = SymbolTable.new - st.enter_scope('outer') - st.define_symbol('dog', 'woof') - st.enter_scope('inner') - st.define_symbol('dog', 'bark') - assert_equal('bark', st.resolve_symbol('dog')) - assert_equal('woof', st.resolve_symbol('outer.dog')) - st.exit_scope - st.exit_scope - assert_equal('bark', st.resolve_symbol('outer.inner.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_full_path - 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 - - # I would like the name of the scope to take on the - # value of the program counter at that location. - def test_scope_as_symbol - program = <<-ASM - .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} - .org $8000 - .segment prog 0 - .scope main - sei - cld - lda \#$00 - jmp main - . - jmp main - jmp global.main - ASM - - # There really should be an evaluate string method - assembler = Assembler.new - program.split(/\n/).each do |line| - assembler.assemble_one_line(line) - end - assembler.fulfill_promises - assert_equal(0x8000, assembler.symbol_table.resolve_symbol('global.main')) - end - - # Fix a bug where we can't see a forward declared symbol in a scope - def test_foward_declaration_in_scope - program = <<-ASM - ;;;; - ; Create an iNES header - .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} - - ;;;; - ; Try to expose a problem we have with scopes - ; We don't seem to be able to branch to a forward - ; declared symbol within a scope - .org $8000 - .scope main - sei - cld - lda #\$00 - bne forward_symbol - nop - nop - nop - forward_symbol: - rts - . - ASM - - # There really should be an evaluate string method - assembler = Assembler.new - program.split(/\n/).each do |line| - assembler.assemble_one_line(line) - end - puts YAML.dump(assembler.symbol_table) - assembler.fulfill_promises - - # The forward symbol should have been resolved to +3, and the ROM should look like this: - correct_rom = [ - 0x4e, 0x45, 0x53, 0x1a, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x78, # SEI - 0xd8, # CLD - 0xa9, 0x0, # LDA immediate 0 - 0xd0, 0x3, # BNE +3 - 0xea, # NOP - 0xea, # NOP - 0xea, # NOP - 0x60 # RTS forward_symbol - ] - - # Grab the first 26 bytes of the rom and make sure they assemble to the above - emitted_rom = assembler.emit_binary_rom.bytes[0...26] - assert_equal(correct_rom, emitted_rom) - # Yup it is fixed now. - end -end