mos6502-workbench 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'flags'
4
+ require_relative 'bus'
5
+ require_relative 'device'
6
+ require_relative 'memory'
7
+ require_relative 'registers'
8
+
9
+ module MOS6502
10
+ # Raised when the CPU encounters an opcode that is not present in the decode table.
11
+ class UnsupportedOpcode < StandardError; end
12
+
13
+ # A MOS 6502 CPU core with memory, registers, flags, and opcode dispatch.
14
+ #
15
+ # The implementation is intentionally stateful so it can be stepped instruction
16
+ # by instruction, which makes it a good fit for future tracing, disassembly,
17
+ # assembler integration, and TUI visualisation.
18
+ class CPU
19
+ include Registers
20
+ include Flags
21
+ include Memory
22
+
23
+ # Default address used for raw program loading when no reset vector is supplied.
24
+ DEFAULT_LOAD_ADDRESS = 0x0600
25
+ # Base address of the 6502 hardware stack.
26
+ STACK_BASE = 0x0100
27
+ # Vector used for non-maskable interrupts.
28
+ NMI_VECTOR = 0xfffa
29
+ # Vector read on reset.
30
+ RESET_VECTOR = 0xfffc
31
+ # Vector used for IRQ and BRK.
32
+ IRQ_VECTOR = 0xfffe
33
+
34
+ # Official opcode table mapping each supported byte to a mnemonic and
35
+ # addressing mode.
36
+ #
37
+ # @return [Hash{Integer => Array<Symbol>}]
38
+ OPCODES = {
39
+ 0x00 => %i[brk implied],
40
+ 0x01 => %i[ora indirect_x],
41
+ 0x05 => %i[ora zero_page],
42
+ 0x06 => %i[asl zero_page],
43
+ 0x08 => %i[php implied],
44
+ 0x09 => %i[ora immediate],
45
+ 0x0a => %i[asl accumulator],
46
+ 0x0d => %i[ora absolute],
47
+ 0x0e => %i[asl absolute],
48
+ 0x10 => %i[bpl relative],
49
+ 0x11 => %i[ora indirect_y],
50
+ 0x15 => %i[ora zero_page_x],
51
+ 0x16 => %i[asl zero_page_x],
52
+ 0x18 => %i[clc implied],
53
+ 0x19 => %i[ora absolute_y],
54
+ 0x1d => %i[ora absolute_x],
55
+ 0x1e => %i[asl absolute_x],
56
+ 0x20 => %i[jsr absolute],
57
+ 0x21 => %i[and indirect_x],
58
+ 0x24 => %i[bit zero_page],
59
+ 0x25 => %i[and zero_page],
60
+ 0x26 => %i[rol zero_page],
61
+ 0x28 => %i[plp implied],
62
+ 0x29 => %i[and immediate],
63
+ 0x2a => %i[rol accumulator],
64
+ 0x2c => %i[bit absolute],
65
+ 0x2d => %i[and absolute],
66
+ 0x2e => %i[rol absolute],
67
+ 0x30 => %i[bmi relative],
68
+ 0x31 => %i[and indirect_y],
69
+ 0x35 => %i[and zero_page_x],
70
+ 0x36 => %i[rol zero_page_x],
71
+ 0x38 => %i[sec implied],
72
+ 0x39 => %i[and absolute_y],
73
+ 0x3d => %i[and absolute_x],
74
+ 0x3e => %i[rol absolute_x],
75
+ 0x40 => %i[rti implied],
76
+ 0x41 => %i[eor indirect_x],
77
+ 0x45 => %i[eor zero_page],
78
+ 0x46 => %i[lsr zero_page],
79
+ 0x48 => %i[pha implied],
80
+ 0x49 => %i[eor immediate],
81
+ 0x4a => %i[lsr accumulator],
82
+ 0x4c => %i[jmp absolute],
83
+ 0x4d => %i[eor absolute],
84
+ 0x4e => %i[lsr absolute],
85
+ 0x50 => %i[bvc relative],
86
+ 0x51 => %i[eor indirect_y],
87
+ 0x55 => %i[eor zero_page_x],
88
+ 0x56 => %i[lsr zero_page_x],
89
+ 0x58 => %i[cli implied],
90
+ 0x59 => %i[eor absolute_y],
91
+ 0x5d => %i[eor absolute_x],
92
+ 0x5e => %i[lsr absolute_x],
93
+ 0x60 => %i[rts implied],
94
+ 0x61 => %i[adc indirect_x],
95
+ 0x65 => %i[adc zero_page],
96
+ 0x66 => %i[ror zero_page],
97
+ 0x68 => %i[pla implied],
98
+ 0x69 => %i[adc immediate],
99
+ 0x6a => %i[ror accumulator],
100
+ 0x6c => %i[jmp indirect],
101
+ 0x6d => %i[adc absolute],
102
+ 0x6e => %i[ror absolute],
103
+ 0x70 => %i[bvs relative],
104
+ 0x71 => %i[adc indirect_y],
105
+ 0x75 => %i[adc zero_page_x],
106
+ 0x76 => %i[ror zero_page_x],
107
+ 0x78 => %i[sei implied],
108
+ 0x79 => %i[adc absolute_y],
109
+ 0x7d => %i[adc absolute_x],
110
+ 0x7e => %i[ror absolute_x],
111
+ 0x81 => %i[sta indirect_x],
112
+ 0x84 => %i[sty zero_page],
113
+ 0x85 => %i[sta zero_page],
114
+ 0x86 => %i[stx zero_page],
115
+ 0x88 => %i[dey implied],
116
+ 0x8a => %i[txa implied],
117
+ 0x8c => %i[sty absolute],
118
+ 0x8d => %i[sta absolute],
119
+ 0x8e => %i[stx absolute],
120
+ 0x90 => %i[bcc relative],
121
+ 0x91 => %i[sta indirect_y],
122
+ 0x94 => %i[sty zero_page_x],
123
+ 0x95 => %i[sta zero_page_x],
124
+ 0x96 => %i[stx zero_page_y],
125
+ 0x98 => %i[tya implied],
126
+ 0x99 => %i[sta absolute_y],
127
+ 0x9a => %i[txs implied],
128
+ 0x9d => %i[sta absolute_x],
129
+ 0xa0 => %i[ldy immediate],
130
+ 0xa1 => %i[lda indirect_x],
131
+ 0xa2 => %i[ldx immediate],
132
+ 0xa4 => %i[ldy zero_page],
133
+ 0xa5 => %i[lda zero_page],
134
+ 0xa6 => %i[ldx zero_page],
135
+ 0xa8 => %i[tay implied],
136
+ 0xa9 => %i[lda immediate],
137
+ 0xaa => %i[tax implied],
138
+ 0xac => %i[ldy absolute],
139
+ 0xad => %i[lda absolute],
140
+ 0xae => %i[ldx absolute],
141
+ 0xb0 => %i[bcs relative],
142
+ 0xb1 => %i[lda indirect_y],
143
+ 0xb4 => %i[ldy zero_page_x],
144
+ 0xb5 => %i[lda zero_page_x],
145
+ 0xb6 => %i[ldx zero_page_y],
146
+ 0xb8 => %i[clv implied],
147
+ 0xb9 => %i[lda absolute_y],
148
+ 0xba => %i[tsx implied],
149
+ 0xbc => %i[ldy absolute_x],
150
+ 0xbd => %i[lda absolute_x],
151
+ 0xbe => %i[ldx absolute_y],
152
+ 0xc0 => %i[cpy immediate],
153
+ 0xc1 => %i[cmp indirect_x],
154
+ 0xc4 => %i[cpy zero_page],
155
+ 0xc5 => %i[cmp zero_page],
156
+ 0xc6 => %i[dec zero_page],
157
+ 0xc8 => %i[iny implied],
158
+ 0xc9 => %i[cmp immediate],
159
+ 0xca => %i[dex implied],
160
+ 0xcc => %i[cpy absolute],
161
+ 0xcd => %i[cmp absolute],
162
+ 0xce => %i[dec absolute],
163
+ 0xd0 => %i[bne relative],
164
+ 0xd1 => %i[cmp indirect_y],
165
+ 0xd5 => %i[cmp zero_page_x],
166
+ 0xd6 => %i[dec zero_page_x],
167
+ 0xd8 => %i[cld implied],
168
+ 0xd9 => %i[cmp absolute_y],
169
+ 0xdd => %i[cmp absolute_x],
170
+ 0xde => %i[dec absolute_x],
171
+ 0xe0 => %i[cpx immediate],
172
+ 0xe1 => %i[sbc indirect_x],
173
+ 0xe4 => %i[cpx zero_page],
174
+ 0xe5 => %i[sbc zero_page],
175
+ 0xe6 => %i[inc zero_page],
176
+ 0xe8 => %i[inx implied],
177
+ 0xe9 => %i[sbc immediate],
178
+ 0xea => %i[nop implied],
179
+ 0xec => %i[cpx absolute],
180
+ 0xed => %i[sbc absolute],
181
+ 0xee => %i[inc absolute],
182
+ 0xf0 => %i[beq relative],
183
+ 0xf1 => %i[sbc indirect_y],
184
+ 0xf5 => %i[sbc zero_page_x],
185
+ 0xf6 => %i[inc zero_page_x],
186
+ 0xf8 => %i[sed implied],
187
+ 0xf9 => %i[sbc absolute_y],
188
+ 0xfd => %i[sbc absolute_x],
189
+ 0xfe => %i[inc absolute_x]
190
+ }.freeze
191
+
192
+ # @return [Integer] the number of instructions executed since the last reset
193
+ attr_reader :instruction_count
194
+ # @return [Integer] the number of base CPU cycles observed since the last reset
195
+ attr_reader :cycle_count
196
+ # @return [Integer, nil] the most recently executed opcode byte
197
+ attr_reader :last_opcode
198
+ # @return [Integer] the base cycle cost of the most recent instruction
199
+ attr_reader :last_cycles
200
+ # @return [Integer] the current program counter
201
+ attr_reader :program_counter
202
+ # @return [Integer] the current stack pointer offset within page `0x0100`
203
+ attr_reader :stack_pointer
204
+ # @return [Bus] the address bus used for all memory access
205
+ attr_reader :bus
206
+
207
+ BRANCH_MNEMONICS = %i[bpl bmi bvc bvs bcc bcs bne beq].freeze
208
+ TWO_CYCLE_IMPLIED_MNEMONICS = %i[
209
+ clc cld cli clv
210
+ sec sed sei
211
+ dex dey inx iny
212
+ nop
213
+ tax tay tsx txa txs tya
214
+ ].freeze
215
+
216
+ # Creates a new CPU instance and powers it on.
217
+ #
218
+ # When no bus is supplied, the CPU installs a default standalone memory map
219
+ # consisting of 64 KiB of RAM and a synthetic reset vector pointing at
220
+ # {DEFAULT_LOAD_ADDRESS}. When a custom bus is supplied, callers can disable
221
+ # that convenience vector so a ROM-mapped machine can boot from real vectors.
222
+ #
223
+ # @param bus [Bus, nil] the address bus used by the CPU
224
+ # @param install_default_reset_vector [Boolean, nil] whether to install the standalone reset vector
225
+ # @return [void]
226
+ def initialize(bus: nil, install_default_reset_vector: nil)
227
+ @bus = bus || Bus.default
228
+ @step_subscribers = []
229
+ install_default_reset_vector = bus.nil? if install_default_reset_vector.nil?
230
+ power_on(install_default_reset_vector:)
231
+ end
232
+
233
+ # Performs a full power-on sequence.
234
+ #
235
+ # Unlike {#reset}, this also clears RAM and installs the default reset vector.
236
+ #
237
+ # @param install_default_reset_vector [Boolean] whether to install the standalone reset vector
238
+ # @return [CPU] the CPU instance
239
+ def power_on(install_default_reset_vector: true)
240
+ memory_reset
241
+ write_word(RESET_VECTOR, DEFAULT_LOAD_ADDRESS) if install_default_reset_vector
242
+ reset
243
+ end
244
+
245
+ # Resets the CPU registers and control state without clearing memory.
246
+ #
247
+ # This mirrors real 6502 usage more closely than erasing RAM on every reset,
248
+ # which is important once programs are loaded from binaries or an assembler.
249
+ #
250
+ # @param program_counter [Integer, nil] an optional explicit reset address
251
+ # @return [CPU] the CPU instance
252
+ def reset(program_counter: nil)
253
+ registers_reset
254
+ flags_reset
255
+ @stack_pointer = 0xfd
256
+ @instruction_count = 0
257
+ @cycle_count = 0
258
+ @last_opcode = nil
259
+ @last_cycles = 0
260
+ self.interrupt = true
261
+ @program_counter = program_counter || read_word(RESET_VECTOR)
262
+ @program_counter &= 0xffff
263
+ self
264
+ end
265
+
266
+ # Loads program bytes into memory.
267
+ #
268
+ # @param program [String, #to_a] the raw bytes to write
269
+ # @param start_address [Integer] where to place the first byte
270
+ # @param set_reset_vector [Boolean] whether to point the reset vector at the program
271
+ # @return [Integer] the start address used for the load
272
+ def load(program, start_address: DEFAULT_LOAD_ADDRESS, set_reset_vector: true)
273
+ bytes = program.is_a?(String) ? program.bytes : program.to_a
274
+ bytes.each_with_index do |byte, offset|
275
+ write_byte(start_address + offset, byte)
276
+ end
277
+ write_word(RESET_VECTOR, start_address) if set_reset_vector
278
+ start_address
279
+ end
280
+
281
+ # Loads an Intel HEX document directly into memory.
282
+ #
283
+ # The reset vector is pointed at the lowest loaded address by default, which
284
+ # matches the most common "load and run" workflow for assembled programs.
285
+ #
286
+ # @param source [String] the Intel HEX text to parse
287
+ # @param set_reset_vector [Boolean] whether to point the reset vector at the first record
288
+ # @return [Integer, nil] the lowest loaded address
289
+ # @raise [IntelHexError] if the document is malformed or unsupported
290
+ def load_intel_hex(source, set_reset_vector: true)
291
+ IntelHex.new.parse(source).load_into(self, set_reset_vector:)
292
+ end
293
+
294
+ # Fetches, decodes, and executes a single instruction.
295
+ #
296
+ # @return [Integer] the executed opcode byte
297
+ # @raise [UnsupportedOpcode] if the opcode is not present in {OPCODES}
298
+ def step
299
+ instruction_address = @program_counter
300
+ @last_opcode = fetch_byte
301
+ instruction = OPCODES[@last_opcode]
302
+ raise UnsupportedOpcode, format('Opcode 0x%02X is not implemented', @last_opcode) unless instruction
303
+
304
+ mnemonic, mode = instruction
305
+ send(mnemonic, mode)
306
+ @instruction_count += 1
307
+ @last_cycles = base_cycles_for(mnemonic, mode)
308
+ @cycle_count += @last_cycles
309
+ emit_step(
310
+ address: instruction_address,
311
+ opcode: @last_opcode,
312
+ mnemonic:,
313
+ mode:,
314
+ cycles: @last_cycles,
315
+ snapshot: snapshot
316
+ )
317
+ @last_opcode
318
+ end
319
+
320
+ # Executes a fixed number of instructions.
321
+ #
322
+ # @param max_instructions [Integer] how many instructions to execute
323
+ # @return [CPU] the CPU instance
324
+ def run(max_instructions:)
325
+ max_instructions.times { step }
326
+ self
327
+ end
328
+
329
+ # Services a maskable interrupt request.
330
+ #
331
+ # If the interrupt-disable flag is set, the IRQ is ignored.
332
+ #
333
+ # @return [Boolean] true when the IRQ was taken, false when it was masked
334
+ def irq
335
+ return false if interrupt?
336
+
337
+ interrupt_sequence(vector: IRQ_VECTOR, break_flag: false)
338
+ @cycle_count += 7
339
+ true
340
+ end
341
+
342
+ # Services a non-maskable interrupt.
343
+ #
344
+ # @return [Boolean] always true
345
+ def nmi
346
+ interrupt_sequence(vector: NMI_VECTOR, break_flag: false)
347
+ @cycle_count += 7
348
+ true
349
+ end
350
+
351
+ # Captures the externally visible CPU state as a hash.
352
+ #
353
+ # This is intended as a convenient integration point for tracers, tests,
354
+ # and future UI layers.
355
+ #
356
+ # @return [Hash<Symbol, Integer, nil>] a snapshot of the current CPU state
357
+ def snapshot
358
+ {
359
+ accumulator: accumulator,
360
+ register_x: register_x,
361
+ register_y: register_y,
362
+ stack_pointer: stack_pointer,
363
+ program_counter: program_counter,
364
+ status: flags_encode,
365
+ instruction_count: instruction_count,
366
+ cycle_count: cycle_count,
367
+ last_cycles: last_cycles,
368
+ last_opcode: last_opcode
369
+ }
370
+ end
371
+
372
+ # Registers a callback for each executed instruction.
373
+ #
374
+ # Subscribers receive a hash with the executed address, opcode, mnemonic,
375
+ # addressing mode, base cycles, and a post-instruction snapshot.
376
+ #
377
+ # @yieldparam event [Hash] the instruction event
378
+ # @return [Proc] the registered callback
379
+ def subscribe_steps(&block)
380
+ raise ArgumentError, 'A block is required' unless block
381
+
382
+ @step_subscribers << block
383
+ block
384
+ end
385
+
386
+ # Removes a previously registered step callback.
387
+ #
388
+ # @param subscriber [Proc] the callback returned by {#subscribe_steps}
389
+ # @return [Proc, nil] the removed callback
390
+ def unsubscribe_steps(subscriber)
391
+ @step_subscribers.delete(subscriber)
392
+ end
393
+
394
+ private
395
+
396
+ # Returns the base cycle cost for an opcode without dynamic penalties.
397
+ #
398
+ # These values intentionally exclude page-crossing and taken-branch penalties.
399
+ # They are useful for coarse timing and device ticking, but they are not yet a
400
+ # full cycle-accurate timing model.
401
+ #
402
+ # @param mnemonic [Symbol] the decoded mnemonic
403
+ # @param mode [Symbol] the decoded addressing mode
404
+ # @return [Integer] the base cycle count
405
+ def base_cycles_for(mnemonic, mode)
406
+ return 7 if mnemonic == :brk
407
+ return 6 if %i[jsr rts rti].include?(mnemonic)
408
+ return { absolute: 3, indirect: 5 }.fetch(mode) if mnemonic == :jmp
409
+ return 3 if %i[pha php].include?(mnemonic)
410
+ return 4 if %i[pla plp].include?(mnemonic)
411
+ return shift_cycles(mode) if %i[asl lsr rol ror].include?(mnemonic)
412
+ return inc_dec_cycles(mode) if %i[inc dec].include?(mnemonic)
413
+ return store_cycles(mnemonic, mode) if %i[sta stx sty].include?(mnemonic)
414
+ return bit_cycles(mode) if mnemonic == :bit
415
+ return ldy_cycles(mode) if mnemonic == :ldy
416
+ return ldx_cycles(mode) if mnemonic == :ldx
417
+ return compare_register_cycles(mode) if %i[cpy cpx].include?(mnemonic)
418
+ return 2 if BRANCH_MNEMONICS.include?(mnemonic)
419
+ return 2 if TWO_CYCLE_IMPLIED_MNEMONICS.include?(mnemonic)
420
+
421
+ default_mode_cycles(mnemonic, mode)
422
+ end
423
+
424
+ # Broadcasts an executed-instruction event to registered subscribers.
425
+ #
426
+ # @param event [Hash] the instruction event payload
427
+ # @return [void]
428
+ def emit_step(event)
429
+ @step_subscribers.each { |subscriber| subscriber.call(event) }
430
+ end
431
+
432
+ # Returns the base timing for shift and rotate operations.
433
+ #
434
+ # @param mode [Symbol] the decoded addressing mode
435
+ # @return [Integer] the base cycle count
436
+ def shift_cycles(mode)
437
+ {
438
+ accumulator: 2,
439
+ zero_page: 5,
440
+ zero_page_x: 6,
441
+ absolute: 6,
442
+ absolute_x: 7
443
+ }.fetch(mode)
444
+ end
445
+
446
+ # Returns the base timing for increment and decrement memory operations.
447
+ #
448
+ # @param mode [Symbol] the decoded addressing mode
449
+ # @return [Integer] the base cycle count
450
+ def inc_dec_cycles(mode)
451
+ {
452
+ zero_page: 5,
453
+ zero_page_x: 6,
454
+ absolute: 6,
455
+ absolute_x: 7
456
+ }.fetch(mode)
457
+ end
458
+
459
+ # Returns the base timing for store instructions.
460
+ #
461
+ # @param mnemonic [Symbol] the decoded mnemonic
462
+ # @param mode [Symbol] the decoded addressing mode
463
+ # @return [Integer] the base cycle count
464
+ def store_cycles(mnemonic, mode)
465
+ {
466
+ sta: {
467
+ zero_page: 3,
468
+ zero_page_x: 4,
469
+ absolute: 4,
470
+ absolute_x: 5,
471
+ absolute_y: 5,
472
+ indirect_x: 6,
473
+ indirect_y: 6
474
+ },
475
+ stx: {
476
+ zero_page: 3,
477
+ zero_page_y: 4,
478
+ absolute: 4
479
+ },
480
+ sty: {
481
+ zero_page: 3,
482
+ zero_page_x: 4,
483
+ absolute: 4
484
+ }
485
+ }.fetch(mnemonic).fetch(mode)
486
+ end
487
+
488
+ # Returns the base timing for `BIT`.
489
+ #
490
+ # @param mode [Symbol] the decoded addressing mode
491
+ # @return [Integer] the base cycle count
492
+ def bit_cycles(mode)
493
+ { zero_page: 3, absolute: 4 }.fetch(mode)
494
+ end
495
+
496
+ # Returns the base timing for `LDY`.
497
+ #
498
+ # @param mode [Symbol] the decoded addressing mode
499
+ # @return [Integer] the base cycle count
500
+ def ldy_cycles(mode)
501
+ {
502
+ immediate: 2,
503
+ zero_page: 3,
504
+ zero_page_x: 4,
505
+ absolute: 4,
506
+ absolute_x: 4
507
+ }.fetch(mode)
508
+ end
509
+
510
+ # Returns the base timing for `LDX`.
511
+ #
512
+ # @param mode [Symbol] the decoded addressing mode
513
+ # @return [Integer] the base cycle count
514
+ def ldx_cycles(mode)
515
+ {
516
+ immediate: 2,
517
+ zero_page: 3,
518
+ zero_page_y: 4,
519
+ absolute: 4,
520
+ absolute_y: 4
521
+ }.fetch(mode)
522
+ end
523
+
524
+ # Returns the base timing for `CPX` and `CPY`.
525
+ #
526
+ # @param mode [Symbol] the decoded addressing mode
527
+ # @return [Integer] the base cycle count
528
+ def compare_register_cycles(mode)
529
+ {
530
+ immediate: 2,
531
+ zero_page: 3,
532
+ absolute: 4
533
+ }.fetch(mode)
534
+ end
535
+
536
+ # Returns the default base timing for operand-reading instructions.
537
+ #
538
+ # @param mnemonic [Symbol] the decoded mnemonic
539
+ # @param mode [Symbol] the decoded addressing mode
540
+ # @return [Integer] the base cycle count
541
+ # @raise [ArgumentError] when no timing is defined for the mnemonic/mode pair
542
+ def default_mode_cycles(mnemonic, mode)
543
+ {
544
+ immediate: 2,
545
+ zero_page: 3,
546
+ zero_page_x: 4,
547
+ zero_page_y: 4,
548
+ absolute: 4,
549
+ absolute_x: 4,
550
+ absolute_y: 4,
551
+ indirect_x: 6,
552
+ indirect_y: 5
553
+ }.fetch(mode)
554
+ rescue KeyError
555
+ raise ArgumentError, "No base cycle timing is defined for #{mnemonic} #{mode}"
556
+ end
557
+
558
+ # Adds the operand and carry flag into the accumulator.
559
+ #
560
+ # @param mode [Symbol] the decoded addressing mode
561
+ # @return [void]
562
+ def adc(mode)
563
+ add_with_carry(read_operand(mode))
564
+ end
565
+
566
+ # Performs a bitwise AND between the accumulator and the operand.
567
+ #
568
+ # @param mode [Symbol] the decoded addressing mode
569
+ # @return [void]
570
+ def and(mode)
571
+ self.accumulator = set_zero_and_negative(accumulator & read_operand(mode))
572
+ end
573
+
574
+ # Arithmetic shift left.
575
+ #
576
+ # The top bit moves into carry and the result is shifted left by one.
577
+ #
578
+ # @param mode [Symbol] the decoded addressing mode
579
+ # @return [void]
580
+ def asl(mode)
581
+ shift(mode) do |value|
582
+ self.carry = value.anybits?(0x80)
583
+ (value << 1) & 0xff
584
+ end
585
+ end
586
+
587
+ # Branches when the carry flag is clear.
588
+ #
589
+ # @param mode [Symbol] the decoded addressing mode
590
+ # @return [void]
591
+ def bcc(mode)
592
+ branch_if(mode) { !carry? }
593
+ end
594
+
595
+ # Branches when the carry flag is set.
596
+ #
597
+ # @param mode [Symbol] the decoded addressing mode
598
+ # @return [void]
599
+ def bcs(mode)
600
+ branch_if(mode) { carry? }
601
+ end
602
+
603
+ # Branches when the zero flag is set.
604
+ #
605
+ # @param mode [Symbol] the decoded addressing mode
606
+ # @return [void]
607
+ def beq(mode)
608
+ branch_if(mode) { zero? }
609
+ end
610
+
611
+ # Tests bits in memory against the accumulator.
612
+ #
613
+ # `BIT` does not store a result; it updates zero from `A & M`, and copies
614
+ # bits 7 and 6 of the operand into negative and overflow.
615
+ #
616
+ # @param mode [Symbol] the decoded addressing mode
617
+ # @return [void]
618
+ def bit(mode)
619
+ value = read_operand(mode)
620
+ self.zero = accumulator.nobits?(value)
621
+ self.overflow = value.anybits?(0x40)
622
+ self.negative = value.anybits?(0x80)
623
+ end
624
+
625
+ # Branches when the negative flag is set.
626
+ #
627
+ # @param mode [Symbol] the decoded addressing mode
628
+ # @return [void]
629
+ def bmi(mode)
630
+ branch_if(mode) { negative? }
631
+ end
632
+
633
+ # Branches when the zero flag is clear.
634
+ #
635
+ # @param mode [Symbol] the decoded addressing mode
636
+ # @return [void]
637
+ def bne(mode)
638
+ branch_if(mode) { !zero? }
639
+ end
640
+
641
+ # Branches when the negative flag is clear.
642
+ #
643
+ # @param mode [Symbol] the decoded addressing mode
644
+ # @return [void]
645
+ def bpl(mode)
646
+ branch_if(mode) { !negative? }
647
+ end
648
+
649
+ # Software interrupt.
650
+ #
651
+ # `BRK` pushes the return address and status register, sets interrupt disable,
652
+ # and loads the IRQ/BRK vector.
653
+ #
654
+ # @param _mode [Symbol] the decoded addressing mode
655
+ # @return [void]
656
+ def brk(_mode)
657
+ @program_counter = (@program_counter + 1) & 0xffff
658
+ interrupt_sequence(vector: IRQ_VECTOR, break_flag: true)
659
+ end
660
+
661
+ # Branches when the overflow flag is clear.
662
+ #
663
+ # @param mode [Symbol] the decoded addressing mode
664
+ # @return [void]
665
+ def bvc(mode)
666
+ branch_if(mode) { !overflow? }
667
+ end
668
+
669
+ # Branches when the overflow flag is set.
670
+ #
671
+ # @param mode [Symbol] the decoded addressing mode
672
+ # @return [void]
673
+ def bvs(mode)
674
+ branch_if(mode) { overflow? }
675
+ end
676
+
677
+ # Clears the carry flag.
678
+ #
679
+ # @param _mode [Symbol] the decoded addressing mode
680
+ # @return [void]
681
+ def clc(_mode)
682
+ self.carry = false
683
+ end
684
+
685
+ # Clears the decimal mode flag.
686
+ #
687
+ # @param _mode [Symbol] the decoded addressing mode
688
+ # @return [void]
689
+ def cld(_mode)
690
+ self.decimal = false
691
+ end
692
+
693
+ # Clears the interrupt-disable flag.
694
+ #
695
+ # @param _mode [Symbol] the decoded addressing mode
696
+ # @return [void]
697
+ def cli(_mode)
698
+ self.interrupt = false
699
+ end
700
+
701
+ # Clears the overflow flag.
702
+ #
703
+ # @param _mode [Symbol] the decoded addressing mode
704
+ # @return [void]
705
+ def clv(_mode)
706
+ self.overflow = false
707
+ end
708
+
709
+ # Compares the accumulator with the operand.
710
+ #
711
+ # @param mode [Symbol] the decoded addressing mode
712
+ # @return [void]
713
+ def cmp(mode)
714
+ compare(accumulator, read_operand(mode))
715
+ end
716
+
717
+ # Compares the X register with the operand.
718
+ #
719
+ # @param mode [Symbol] the decoded addressing mode
720
+ # @return [void]
721
+ def cpx(mode)
722
+ compare(register_x, read_operand(mode))
723
+ end
724
+
725
+ # Compares the Y register with the operand.
726
+ #
727
+ # @param mode [Symbol] the decoded addressing mode
728
+ # @return [void]
729
+ def cpy(mode)
730
+ compare(register_y, read_operand(mode))
731
+ end
732
+
733
+ # Decrements a memory location by one.
734
+ #
735
+ # @param mode [Symbol] the decoded addressing mode
736
+ # @return [void]
737
+ def dec(mode)
738
+ write_operand(mode) { |value| set_zero_and_negative((value - 1) & 0xff) }
739
+ end
740
+
741
+ # Decrements the X register by one.
742
+ #
743
+ # @param _mode [Symbol] the decoded addressing mode
744
+ # @return [void]
745
+ def dex(_mode)
746
+ self.register_x = set_zero_and_negative((register_x - 1) & 0xff)
747
+ end
748
+
749
+ # Decrements the Y register by one.
750
+ #
751
+ # @param _mode [Symbol] the decoded addressing mode
752
+ # @return [void]
753
+ def dey(_mode)
754
+ self.register_y = set_zero_and_negative((register_y - 1) & 0xff)
755
+ end
756
+
757
+ # Performs a bitwise exclusive OR with the accumulator.
758
+ #
759
+ # @param mode [Symbol] the decoded addressing mode
760
+ # @return [void]
761
+ def eor(mode)
762
+ self.accumulator = set_zero_and_negative(accumulator ^ read_operand(mode))
763
+ end
764
+
765
+ # Increments a memory location by one.
766
+ #
767
+ # @param mode [Symbol] the decoded addressing mode
768
+ # @return [void]
769
+ def inc(mode)
770
+ write_operand(mode) { |value| set_zero_and_negative((value + 1) & 0xff) }
771
+ end
772
+
773
+ # Increments the X register by one.
774
+ #
775
+ # @param _mode [Symbol] the decoded addressing mode
776
+ # @return [void]
777
+ def inx(_mode)
778
+ self.register_x = set_zero_and_negative((register_x + 1) & 0xff)
779
+ end
780
+
781
+ # Increments the Y register by one.
782
+ #
783
+ # @param _mode [Symbol] the decoded addressing mode
784
+ # @return [void]
785
+ def iny(_mode)
786
+ self.register_y = set_zero_and_negative((register_y + 1) & 0xff)
787
+ end
788
+
789
+ # Jumps to a new program counter.
790
+ #
791
+ # `JMP` supports both absolute and indirect addressing. The indirect form
792
+ # preserves the original 6502 page-wrap bug.
793
+ #
794
+ # @param mode [Symbol] the decoded addressing mode
795
+ # @return [void]
796
+ def jmp(mode)
797
+ @program_counter = if mode == :indirect
798
+ read_indirect_word(fetch_word)
799
+ else
800
+ fetch_word
801
+ end
802
+ end
803
+
804
+ # Jumps to a subroutine after pushing the return address.
805
+ #
806
+ # @param _mode [Symbol] the decoded addressing mode
807
+ # @return [void]
808
+ def jsr(_mode)
809
+ target = fetch_word
810
+ push_word((@program_counter - 1) & 0xffff)
811
+ @program_counter = target
812
+ end
813
+
814
+ # Loads the accumulator from memory or an immediate operand.
815
+ #
816
+ # @param mode [Symbol] the decoded addressing mode
817
+ # @return [void]
818
+ def lda(mode)
819
+ self.accumulator = set_zero_and_negative(read_operand(mode))
820
+ end
821
+
822
+ # Loads the X register from memory or an immediate operand.
823
+ #
824
+ # @param mode [Symbol] the decoded addressing mode
825
+ # @return [void]
826
+ def ldx(mode)
827
+ self.register_x = set_zero_and_negative(read_operand(mode))
828
+ end
829
+
830
+ # Loads the Y register from memory or an immediate operand.
831
+ #
832
+ # @param mode [Symbol] the decoded addressing mode
833
+ # @return [void]
834
+ def ldy(mode)
835
+ self.register_y = set_zero_and_negative(read_operand(mode))
836
+ end
837
+
838
+ # Logical shift right.
839
+ #
840
+ # The low bit moves into carry and zero is shifted into bit 7.
841
+ #
842
+ # @param mode [Symbol] the decoded addressing mode
843
+ # @return [void]
844
+ def lsr(mode)
845
+ shift(mode) do |value|
846
+ self.carry = value.anybits?(0x01)
847
+ (value >> 1) & 0xff
848
+ end
849
+ end
850
+
851
+ # No operation.
852
+ #
853
+ # @param _mode [Symbol] the decoded addressing mode
854
+ # @return [void]
855
+ def nop(_mode); end
856
+
857
+ # Performs a bitwise OR with the accumulator.
858
+ #
859
+ # @param mode [Symbol] the decoded addressing mode
860
+ # @return [void]
861
+ def ora(mode)
862
+ self.accumulator = set_zero_and_negative(accumulator | read_operand(mode))
863
+ end
864
+
865
+ # Pushes the accumulator onto the hardware stack.
866
+ #
867
+ # @param _mode [Symbol] the decoded addressing mode
868
+ # @return [void]
869
+ def pha(_mode)
870
+ push_byte(accumulator)
871
+ end
872
+
873
+ # Pushes the processor status register onto the hardware stack.
874
+ #
875
+ # `PHP` always pushes the break and high bits set, matching 6502 behaviour.
876
+ #
877
+ # @param _mode [Symbol] the decoded addressing mode
878
+ # @return [void]
879
+ def php(_mode)
880
+ push_byte((flags_encode & 0xef) | 0x30)
881
+ end
882
+
883
+ # Pulls the accumulator from the hardware stack.
884
+ #
885
+ # @param _mode [Symbol] the decoded addressing mode
886
+ # @return [void]
887
+ def pla(_mode)
888
+ self.accumulator = set_zero_and_negative(pull_byte)
889
+ end
890
+
891
+ # Pulls the processor status register from the hardware stack.
892
+ #
893
+ # @param _mode [Symbol] the decoded addressing mode
894
+ # @return [void]
895
+ def plp(_mode)
896
+ flags_decode(pull_byte | 0x20)
897
+ end
898
+
899
+ # Rotate left through the carry flag.
900
+ #
901
+ # @param mode [Symbol] the decoded addressing mode
902
+ # @return [void]
903
+ def rol(mode)
904
+ shift(mode) do |value|
905
+ new_value = ((value << 1) | bit_value(carry?)) & 0xff
906
+ self.carry = value.anybits?(0x80)
907
+ new_value
908
+ end
909
+ end
910
+
911
+ # Rotate right through the carry flag.
912
+ #
913
+ # @param mode [Symbol] the decoded addressing mode
914
+ # @return [void]
915
+ def ror(mode)
916
+ shift(mode) do |value|
917
+ new_value = ((bit_value(carry?) << 7) | (value >> 1)) & 0xff
918
+ self.carry = value.anybits?(0x01)
919
+ new_value
920
+ end
921
+ end
922
+
923
+ # Returns from an interrupt by restoring status and program counter.
924
+ #
925
+ # @param _mode [Symbol] the decoded addressing mode
926
+ # @return [void]
927
+ def rti(_mode)
928
+ flags_decode(pull_byte | 0x20)
929
+ @program_counter = pull_word
930
+ end
931
+
932
+ # Returns from a subroutine.
933
+ #
934
+ # @param _mode [Symbol] the decoded addressing mode
935
+ # @return [void]
936
+ def rts(_mode)
937
+ @program_counter = (pull_word + 1) & 0xffff
938
+ end
939
+
940
+ # Subtracts the operand and the inverted carry from the accumulator.
941
+ #
942
+ # On the 6502, `SBC` interprets the carry flag as "no borrow".
943
+ #
944
+ # @param mode [Symbol] the decoded addressing mode
945
+ # @return [void]
946
+ def sbc(mode)
947
+ subtract_with_carry(read_operand(mode))
948
+ end
949
+
950
+ # Sets the carry flag.
951
+ #
952
+ # @param _mode [Symbol] the decoded addressing mode
953
+ # @return [void]
954
+ def sec(_mode)
955
+ self.carry = true
956
+ end
957
+
958
+ # Sets the decimal mode flag.
959
+ #
960
+ # @param _mode [Symbol] the decoded addressing mode
961
+ # @return [void]
962
+ def sed(_mode)
963
+ self.decimal = true
964
+ end
965
+
966
+ # Sets the interrupt-disable flag.
967
+ #
968
+ # @param _mode [Symbol] the decoded addressing mode
969
+ # @return [void]
970
+ def sei(_mode)
971
+ self.interrupt = true
972
+ end
973
+
974
+ # Stores the accumulator into memory.
975
+ #
976
+ # @param mode [Symbol] the decoded addressing mode
977
+ # @return [void]
978
+ def sta(mode)
979
+ write_byte(resolve_address(mode), accumulator)
980
+ end
981
+
982
+ # Stores the X register into memory.
983
+ #
984
+ # @param mode [Symbol] the decoded addressing mode
985
+ # @return [void]
986
+ def stx(mode)
987
+ write_byte(resolve_address(mode), register_x)
988
+ end
989
+
990
+ # Stores the Y register into memory.
991
+ #
992
+ # @param mode [Symbol] the decoded addressing mode
993
+ # @return [void]
994
+ def sty(mode)
995
+ write_byte(resolve_address(mode), register_y)
996
+ end
997
+
998
+ # Transfers the accumulator into X.
999
+ #
1000
+ # @param _mode [Symbol] the decoded addressing mode
1001
+ # @return [void]
1002
+ def tax(_mode)
1003
+ self.register_x = set_zero_and_negative(accumulator)
1004
+ end
1005
+
1006
+ # Transfers the accumulator into Y.
1007
+ #
1008
+ # @param _mode [Symbol] the decoded addressing mode
1009
+ # @return [void]
1010
+ def tay(_mode)
1011
+ self.register_y = set_zero_and_negative(accumulator)
1012
+ end
1013
+
1014
+ # Transfers the stack pointer into X.
1015
+ #
1016
+ # @param _mode [Symbol] the decoded addressing mode
1017
+ # @return [void]
1018
+ def tsx(_mode)
1019
+ self.register_x = set_zero_and_negative(stack_pointer)
1020
+ end
1021
+
1022
+ # Transfers X into the accumulator.
1023
+ #
1024
+ # @param _mode [Symbol] the decoded addressing mode
1025
+ # @return [void]
1026
+ def txa(_mode)
1027
+ self.accumulator = set_zero_and_negative(register_x)
1028
+ end
1029
+
1030
+ # Transfers X into the stack pointer.
1031
+ #
1032
+ # @param _mode [Symbol] the decoded addressing mode
1033
+ # @return [void]
1034
+ def txs(_mode)
1035
+ @stack_pointer = register_x
1036
+ end
1037
+
1038
+ # Transfers Y into the accumulator.
1039
+ #
1040
+ # @param _mode [Symbol] the decoded addressing mode
1041
+ # @return [void]
1042
+ def tya(_mode)
1043
+ self.accumulator = set_zero_and_negative(register_y)
1044
+ end
1045
+
1046
+ # Core implementation for `ADC`.
1047
+ #
1048
+ # This supports both binary and decimal arithmetic, while computing carry,
1049
+ # overflow, zero, and negative according to 6502 conventions.
1050
+ #
1051
+ # @param value [Integer] the operand to add
1052
+ # @return [void]
1053
+ def add_with_carry(value)
1054
+ a = accumulator
1055
+ carry_in = bit_value(carry?)
1056
+ binary_sum = a + value + carry_in
1057
+ self.overflow = (~(a ^ value) & (a ^ binary_sum)).anybits?(0x80)
1058
+
1059
+ result = if decimal?
1060
+ decimal_sum = binary_sum
1061
+ decimal_sum += 0x06 if ((a & 0x0f) + (value & 0x0f) + carry_in) > 0x09
1062
+ decimal_sum += 0x60 if decimal_sum > 0x99
1063
+ self.carry = decimal_sum > 0x99
1064
+ decimal_sum & 0xff
1065
+ else
1066
+ self.carry = binary_sum > 0xff
1067
+ binary_sum & 0xff
1068
+ end
1069
+
1070
+ self.accumulator = set_zero_and_negative(result)
1071
+ end
1072
+
1073
+ # Core implementation for `SBC`.
1074
+ #
1075
+ # @param value [Integer] the operand to subtract
1076
+ # @return [void]
1077
+ def subtract_with_carry(value)
1078
+ a = accumulator
1079
+ borrow = carry? ? 0 : 1
1080
+ binary_difference = a - value - borrow
1081
+ self.overflow = ((a ^ binary_difference) & (a ^ value)).anybits?(0x80)
1082
+ self.carry = binary_difference >= 0
1083
+
1084
+ result = if decimal?
1085
+ low = (a & 0x0f) - (value & 0x0f) - borrow
1086
+ high = (a >> 4) - (value >> 4)
1087
+ if low.negative?
1088
+ low -= 0x06
1089
+ high -= 1
1090
+ end
1091
+ high -= 0x06 if high.negative?
1092
+ ((high << 4) | (low & 0x0f)) & 0xff
1093
+ else
1094
+ binary_difference & 0xff
1095
+ end
1096
+
1097
+ self.accumulator = set_zero_and_negative(result)
1098
+ end
1099
+
1100
+ # Applies a relative branch offset when the supplied condition is true.
1101
+ #
1102
+ # @param _mode [Symbol] the decoded addressing mode
1103
+ # @yieldreturn [Boolean] whether the branch should be taken
1104
+ # @return [void]
1105
+ def branch_if(_mode)
1106
+ offset = signed_byte(fetch_byte)
1107
+ @program_counter = (@program_counter + offset) & 0xffff if yield
1108
+ end
1109
+
1110
+ # Shared implementation for compare instructions.
1111
+ #
1112
+ # `CMP`, `CPX`, and `CPY` set carry on `register >= operand` and update
1113
+ # zero and negative from the subtraction result without storing it.
1114
+ #
1115
+ # @param register_value [Integer] the register to compare
1116
+ # @param operand [Integer] the operand to compare against
1117
+ # @return [Integer] the 8-bit subtraction result
1118
+ def compare(register_value, operand)
1119
+ result = (register_value - operand) & 0xff
1120
+ self.carry = register_value >= operand
1121
+ self.zero = result.zero?
1122
+ self.negative = result.anybits?(0x80)
1123
+ result
1124
+ end
1125
+
1126
+ # Reads the byte at the program counter and advances it.
1127
+ #
1128
+ # @return [Integer] the fetched byte
1129
+ def fetch_byte
1130
+ byte = read_byte(@program_counter)
1131
+ @program_counter = (@program_counter + 1) & 0xffff
1132
+ byte
1133
+ end
1134
+
1135
+ # Reads a little-endian word at the program counter and advances it.
1136
+ #
1137
+ # @return [Integer] the fetched 16-bit value
1138
+ def fetch_word
1139
+ low = fetch_byte
1140
+ high = fetch_byte
1141
+ low | (high << 8)
1142
+ end
1143
+
1144
+ # Pushes interrupt state and transfers control to the supplied vector.
1145
+ #
1146
+ # @param vector [Integer] the interrupt vector address
1147
+ # @param break_flag [Boolean] whether the pushed status should include the break bit
1148
+ # @return [void]
1149
+ def interrupt_sequence(vector:, break_flag:)
1150
+ push_word(@program_counter)
1151
+ status = flags_encode & 0xef
1152
+ status |= 0x10 if break_flag
1153
+ push_byte(status | 0x20)
1154
+ self.interrupt = true
1155
+ self.break = break_flag
1156
+ @program_counter = read_word(vector)
1157
+ end
1158
+
1159
+ # Pulls a byte from the hardware stack.
1160
+ #
1161
+ # @return [Integer] the byte restored from the stack
1162
+ def pull_byte
1163
+ @stack_pointer = (@stack_pointer + 1) & 0xff
1164
+ read_byte(STACK_BASE + @stack_pointer)
1165
+ end
1166
+
1167
+ # Pulls a 16-bit return address from the hardware stack.
1168
+ #
1169
+ # @return [Integer] the restored word value
1170
+ def pull_word
1171
+ low = pull_byte
1172
+ high = pull_byte
1173
+ low | (high << 8)
1174
+ end
1175
+
1176
+ # Pushes a byte onto the hardware stack.
1177
+ #
1178
+ # @param value [Integer] the value to push
1179
+ # @return [void]
1180
+ def push_byte(value)
1181
+ write_byte(STACK_BASE + @stack_pointer, value)
1182
+ @stack_pointer = (@stack_pointer - 1) & 0xff
1183
+ end
1184
+
1185
+ # Pushes a 16-bit value onto the hardware stack, high byte first.
1186
+ #
1187
+ # @param value [Integer] the word to push
1188
+ # @return [void]
1189
+ def push_word(value)
1190
+ push_byte(value >> 8)
1191
+ push_byte(value)
1192
+ end
1193
+
1194
+ # Reads a word through the indirect `JMP` page-wrap bug.
1195
+ #
1196
+ # On a real 6502, when the indirect pointer ends at `xxFF`, the high byte is
1197
+ # fetched from `xx00` rather than the next page.
1198
+ #
1199
+ # @param address [Integer] the indirect pointer location
1200
+ # @return [Integer] the resolved target address
1201
+ def read_indirect_word(address)
1202
+ low = read_byte(address)
1203
+ high_address = (address & 0xff00) | ((address + 1) & 0x00ff)
1204
+ high = read_byte(high_address)
1205
+ low | (high << 8)
1206
+ end
1207
+
1208
+ # Reads an operand value according to its addressing mode.
1209
+ #
1210
+ # @param mode [Symbol] the decoded addressing mode
1211
+ # @return [Integer] the fetched operand byte
1212
+ def read_operand(mode)
1213
+ return fetch_byte if mode == :immediate
1214
+
1215
+ read_byte(resolve_address(mode))
1216
+ end
1217
+
1218
+ # Resolves an addressing mode into a concrete memory address.
1219
+ #
1220
+ # @param mode [Symbol] the decoded addressing mode
1221
+ # @return [Integer] the resolved memory address
1222
+ # @raise [ArgumentError] if the mode does not point to memory
1223
+ def resolve_address(mode)
1224
+ case mode
1225
+ when :zero_page
1226
+ fetch_byte
1227
+ when :zero_page_x
1228
+ (fetch_byte + register_x) & 0xff
1229
+ when :zero_page_y
1230
+ (fetch_byte + register_y) & 0xff
1231
+ when :absolute
1232
+ fetch_word
1233
+ when :absolute_x
1234
+ (fetch_word + register_x) & 0xffff
1235
+ when :absolute_y
1236
+ (fetch_word + register_y) & 0xffff
1237
+ when :indirect_x
1238
+ pointer = (fetch_byte + register_x) & 0xff
1239
+ read_byte(pointer) | (read_byte((pointer + 1) & 0xff) << 8)
1240
+ when :indirect_y
1241
+ pointer = fetch_byte
1242
+ ((read_byte(pointer) | (read_byte((pointer + 1) & 0xff) << 8)) + register_y) & 0xffff
1243
+ else
1244
+ raise ArgumentError, "Address mode #{mode} does not resolve to a memory location"
1245
+ end
1246
+ end
1247
+
1248
+ # Applies zero and negative flag updates to an 8-bit value.
1249
+ #
1250
+ # @param value [Integer] the raw arithmetic or logic result
1251
+ # @return [Integer] the masked 8-bit result
1252
+ def set_zero_and_negative(value)
1253
+ result = value & 0xff
1254
+ self.zero = result.zero?
1255
+ self.negative = result.anybits?(0x80)
1256
+ result
1257
+ end
1258
+
1259
+ # Shared implementation for accumulator and memory shifts or rotates.
1260
+ #
1261
+ # @param mode [Symbol] the decoded addressing mode
1262
+ # @yieldparam value [Integer] the current value to transform
1263
+ # @yieldreturn [Integer] the transformed value
1264
+ # @return [void]
1265
+ def shift(mode)
1266
+ if mode == :accumulator
1267
+ self.accumulator = set_zero_and_negative(yield(accumulator))
1268
+ else
1269
+ write_operand(mode) { |value| set_zero_and_negative(yield(value)) }
1270
+ end
1271
+ end
1272
+
1273
+ # Interprets a byte as a signed 8-bit relative offset.
1274
+ #
1275
+ # @param value [Integer] the raw branch displacement byte
1276
+ # @return [Integer] the signed offset
1277
+ def signed_byte(value)
1278
+ value >= 0x80 ? value - 0x100 : value
1279
+ end
1280
+
1281
+ # Writes a transformed value back to the addressed memory operand.
1282
+ #
1283
+ # @param mode [Symbol] the decoded addressing mode
1284
+ # @yieldparam value [Integer] the current memory value
1285
+ # @yieldreturn [Integer] the new memory value
1286
+ # @return [void]
1287
+ def write_operand(mode)
1288
+ address = resolve_address(mode)
1289
+ write_byte(address, yield(read_byte(address)))
1290
+ end
1291
+ end
1292
+ end