Fixed an issue with referencing outer scoped symbols

This commit is contained in:
Safiire 2015-03-21 23:42:21 -07:00
parent 411e8b9873
commit a48d10474e
10 changed files with 210 additions and 137 deletions

View File

@ -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

View File

@ -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

View File

@ -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
.
;;;;

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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
####

View File

@ -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