diff --git a/examples/backaground.asm b/examples/backaground.asm deleted file mode 100644 index 1016894..0000000 --- a/examples/backaground.asm +++ /dev/null @@ -1,55 +0,0 @@ - ; Create an iNES header - .ines {"prog": 1, "char": 1, "mapper": 0, "mirror": 1} - - - ; Main code segment - .org $C000 -start: - SEI ; disable IRQs - CLD ; disable decimal mode - LDX #$40 - STX $4017 ; disable APU frame IRQ - LDX #$FF - TXS ; Set up stack - INX ; now X = 0 - STX $2000 ; disable NMI - STX $2001 ; disable rendering - STX $4010 ; disable DMC IRQs - -vblankwait1: ; First wait for vblank to make sure PPU is ready - BIT $2002 - BPL vblankwait1 - -clrmem: - LDA #$00 - STA $0000, X - STA $0100, X - STA $0200, X - STA $0400, X - STA $0500, X - STA $0600, X - STA $0700, X - LDA #$FE - STA $0300, X - INX - BNE clrmem - -vblankwait2: ; Second wait for vblank, PPU is ready after this - BIT $2002 - BPL vblankwait2 - - - LDA #$60 ;intensify blues - STA $2001 - -forever: - JMP forever ;jump back to Forever, infinite loop - -nmi: - RTI - - - .org $FFFA ;first of the three vectors starts here - .dw nmi ;when an NMI happens (once per frame if enabled) the processor will jump to the label NMI: - .dw start ;when the processor first turns on or is reset, it will jump to the label RESET: - .dw $0 ;external interrupt IRQ is not used in this tutorial diff --git a/examples/beep.asm b/examples/beep.asm index 7c69696..28e6251 100644 --- a/examples/beep.asm +++ b/examples/beep.asm @@ -1,9 +1,9 @@ - ; Create an iNES header - .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 1} +; Create an iNES header +.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 1} ; Here is the start of our code - .org $C000 -start: +.org $C000 +.scope main LDA #$01 ; square 1 STA $4015 LDA #$F8 ; period low @@ -12,13 +12,13 @@ start: STA $4003 LDA #$BF ; volume STA $4000 -forever: - JMP forever + forever: + JMP forever -nmi: +nothing: RTI .org $FFFA ; Here are the three interrupt vectors - .dw nmi ; VBlank non-maskable interrupt - .dw start ; When the processor is reset or powers on - .dw $0 ; External interrupt IRQ + .dw nothing ; VBlank non-maskable interrupt + .dw main ; When the processor is reset or powers on + .dw nothing ; External interrupt IRQ diff --git a/examples/demo.asm b/examples/demo.asm index 6b6bf6e..43d23ac 100644 --- a/examples/demo.asm +++ b/examples/demo.asm @@ -239,7 +239,7 @@ ;;;; ; Update the sprite, I don't exactly understand the DMA call yet. -update_sprite: +.scope update_sprite lda #>sprite sta nes.sprite.dma ; Jam page $200-$2FF into SPR-RAM, how do we get these numbers? lda sprite.x @@ -262,11 +262,12 @@ update_sprite: adc dx zp sta sprite.x rts +. ;;;; ; Read the first controller, and handle input -react_to_input: +.scope react_to_input lda #$01 ; strobe joypad sta nes.controller1 lda #$00 @@ -306,11 +307,12 @@ react_to_input: stx sprite.y not_dn: rts ; Ignore left and right +. ;;;; ; XORing with $ff toggles between 0x1 and 0xfe (-1) -reverse_dx: +.scope reverse_dx lda #$FF eor dx zp clc @@ -318,30 +320,32 @@ reverse_dx: sta dx zp jsr low_c ; Play the reverse low C note rts +. ;;;; ; Scroll the screen if we have to -scroll_screen: +.scope scroll_screen ldx #$00 ; Reset VRAM Address to $0000 stx nes.vram.address stx nes.vram.address ldx scroll zp ; Do we need to scroll at all? - beq no_scroll + beq return dex stx scroll zp lda #$00 sta nes.ppu.scroll ; Write 0 for Horiz. Scroll value stx nes.ppu.scroll ; Write the value of 'scroll' for Vert. Scroll value -no_scroll: + return: rts +. ;;;; ; Play a low C note on square 1 -low_c: +.scope low_c pha lda #$84 sta nes.apu.pulse1.control @@ -351,11 +355,12 @@ low_c: sta nes.apu.pulse1.ct pla rts +. ;;;; ; Play a high C note on square 1 -high_c: +.scope high_c pha lda #$86 sta nes.apu.pulse1.control @@ -365,15 +370,17 @@ high_c: sta nes.apu.pulse1.ct pla rts +. ;;;; ; Update everything on every vblank -vblank: +.scope vblank jsr scroll_screen jsr update_sprite jsr react_to_input rti +. ;;;; diff --git a/examples/noise.asm b/examples/noise.asm index 2062489..ccfb030 100644 --- a/examples/noise.asm +++ b/examples/noise.asm @@ -9,13 +9,6 @@ ; Create an iNES header .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} -;;;; -; Here is a good spot to associate zero page memory addresses -; with quick access variables in the program. - -.org $0200 -sprite: - ;;;; ; Setup the interrupt vectors @@ -28,7 +21,7 @@ sprite: .org $C000 ;;;; ; Here is our code entry point, which we'll call main. -main: +.scope main ; Disable interrupts and decimal flag sei cld @@ -85,15 +78,16 @@ main: cli forever: jmp forever +. ;;;; ; Update everything on every vblank -vblank: +vblank: rti ;;;; ; Don't do anything on IRQ -irq: +irq: rti diff --git a/examples/scope.asm b/examples/scope.asm deleted file mode 100644 index d44d2bd..0000000 --- a/examples/scope.asm +++ /dev/null @@ -1,30 +0,0 @@ -;;;; -; Create an iNES header -.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} - -;;;; -;; Start a prog segment number 0 -.segment prog 0 -.org $8000 - -.scope main - sei - cld - loop: - ldx $00 - inx - stx $00 - jmp loop -. - -vblank: -irq: - rti - -;;;; -;; Vector table -.org $FFFA - -.dw vblank -.dw main -.dw irq diff --git a/lib/assembler.rb b/lib/assembler.rb index 4ce9be5..998842f 100644 --- a/lib/assembler.rb +++ b/lib/assembler.rb @@ -67,6 +67,23 @@ module Assembler6502 end + #### + ## Return an object that contains the assembler's current state + def get_current_state + saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank + saved_scope = symbol_table.scope_stack.dup + OpenStruct.new(program_counter: saved_program_counter, segment: saved_segment, bank: saved_bank, scope: saved_scope) + end + + + #### + ## Set the current state from an OpenStruct + def set_current_state(struct) + @program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank + symbol_table.scope_stack = struct.scope.dup + end + + #### ## This is the main assemble method, it parses one line into an object ## which when given a reference to this assembler, controls the assembler @@ -95,6 +112,23 @@ module Assembler6502 end + #### + ## This rewinds the state of the assembler, so a promise can be + ## executed with a previous state, for example if we can't resolve + ## a symbol right now, and want to try during the second pass + def with_saved_state(&block) + ## Save the current state of the assembler + old_state = get_current_state + + lambda do + + ## Set the assembler state back to the old state and run the block like that + set_current_state(old_state) + block.call(self) + end + end + + #### ## Write to memory space. Typically, we are going to want to write ## to the location of the current PC, current segment, and current bank. diff --git a/lib/directives/dw.rb b/lib/directives/dw.rb index d1caa31..e6055bf 100644 --- a/lib/directives/dw.rb +++ b/lib/directives/dw.rb @@ -43,18 +43,13 @@ module Assembler6502 ## This is a little complicated, I admit. def exec(assembler) - ## Save these current values into the closure - pc = assembler.program_counter - segment = assembler.current_segment - bank = assembler.current_bank - - ## Create a promise, if this symbol is not defined yet. - promise = lambda do - value = assembler.symbol_table.resolve_symbol(@value) + promise = assembler.with_saved_state do |saved_assembler| + value = saved_assembler.symbol_table.resolve_symbol(@value) bytes = [value & 0xFFFF].pack('S').bytes - assembler.write_memory(bytes, pc, segment, bank) + saved_assembler.write_memory(bytes) end + ## Try to execute it now, or setup the promise to return case @value when Fixnum @@ -66,7 +61,7 @@ module Assembler6502 rescue SymbolTable::UndefinedSymbol ## Must still advance PC before returning promise, so we'll write ## a place holder value of 0xDEAD - assembler.write_memory([0xDE, 0xAD], pc, segment, bank) + assembler.write_memory([0xDE, 0xAD]) return promise end else diff --git a/lib/instruction.rb b/lib/instruction.rb index 2449936..309e379 100644 --- a/lib/instruction.rb +++ b/lib/instruction.rb @@ -147,7 +147,6 @@ module Assembler6502 if match_array.size == 4 _, op, byte_selector, arg = match_array return Instruction.new(op, arg, mode, byte_selector.to_sym) - puts "I found one with #{byte_selector} #{arg}" else _, op, arg = match_array return Instruction.new(op, arg, mode) @@ -204,22 +203,16 @@ module Assembler6502 ## promise to resolve it later. def exec(assembler) - ## Save these current values into the closure/promise - pc = assembler.program_counter - segment = assembler.current_segment - bank = assembler.current_bank - - ## Create a promise if this symbol is not defined yet. - promise = lambda do - @arg = assembler.symbol_table.resolve_symbol(@arg) + promise = assembler.with_saved_state do |saved_assembler| + @arg = saved_assembler.symbol_table.resolve_symbol(@arg) ## If the instruction uses a byte selector, we need to apply that. @arg = apply_byte_selector(@byte_selector, @arg) ## If the instruction is relative we need to work out how far away it is - @arg = @arg - pc - 2 if @mode == :relative + @arg = @arg - saved_assembler.program_counter - 2 if @mode == :relative - assembler.write_memory(emit_bytes, pc, segment, bank) + saved_assembler.write_memory(emit_bytes) end case @arg @@ -234,7 +227,7 @@ module Assembler6502 placeholder = [@hex, 0xDE, 0xAD][0...@length] ## I still have to write a placeholder instruction of the right ## length. The promise will come back and resolve the address. - assembler.write_memory(placeholder, pc, segment, bank) + assembler.write_memory(placeholder) return promise end end diff --git a/lib/symbol_table.rb b/lib/symbol_table.rb index 9ae5bd3..c4f1aab 100644 --- a/lib/symbol_table.rb +++ b/lib/symbol_table.rb @@ -1,7 +1,9 @@ +require 'pry-byebug' module Assembler6502 class SymbolTable + attr_accessor :scope_stack ##### Custom Exceptions class InvalidScope < StandardError; end @@ -54,6 +56,7 @@ module Assembler6502 end +=begin #### ## Resolve symbol to a value, for example: ## scope1.scope2.variable @@ -61,7 +64,7 @@ module Assembler6502 ## 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) + def resolve_symbol_old(name) value = if name.include?('.') path_ary = name.split('.').map(&:to_sym) @@ -85,6 +88,57 @@ module Assembler6502 end value end +=end + + + #### + ## + def resolve_symbol(name) + method = name.include?('.') ? :resolve_symbol_dot_syntax : :resolve_symbol_scoped + value = self.send(method, name) + + fail(UndefinedSymbol, name) if value.nil? + value + end + + + #### + ## Resolve symbol by working backwards through each + ## containing scope. Similarly named scopes shadow outer scopes + def resolve_symbol_scoped(name) + root = "-#{name}".to_sym + stack = @scope_stack.dup + loop do + scope = retreive_scope(stack) + + ## We see if there is a key either under this name, or root + v = scope[name.to_sym] || scope[root] + v = v.kind_of?(Hash) ? v[root] : v + + return v unless v.nil? + + ## Pop the stack so we can decend to the parent scope, if any + stack.pop + return nil if stack.empty? + end + end + + + #### + ## Dot syntax means to check an absolute path to the symbol + ## :global is ignored if it is provided as part of the path + def resolve_symbol_dot_syntax(name) + path_ary = name.split('.').map(&:to_sym) + symbol = path_ary.pop + root = "-#{symbol}".to_sym + path_ary.shift if path_ary.first == :global + + scope = retreive_scope(path_ary) + + ## We see if there is a key either under this name, or root + v = scope[symbol] + v.kind_of?(Hash) ? v[root] : v + end #### diff --git a/test/test_symbol_table.rb b/test/test_symbol_table.rb index 7b921c1..36af7e2 100644 --- a/test/test_symbol_table.rb +++ b/test/test_symbol_table.rb @@ -28,6 +28,34 @@ class TestSymbolTable < MiniTest::Test 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 @@ -153,5 +181,58 @@ class TestSymbolTable < MiniTest::Test 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