JNEXT
Skip to content

Snapshots and span checks

Some checks compare the machine at two moments: a routine must return with the registers it was entered with, an interrupt handler must leave the MMU as it found it. A snapshot captures the machine at one event so a later event can compare against it. Snapshots are kept on named stacks, so a routine that is entered again before it returns (an interrupt during an interrupt, a recursive call) pairs each exit with its own entry.

Action or expression What
snap NAME push a snapshot onto stack NAME
unsnap NAME pop it
NAME.FIELD a field of the snapshot on top of the stack
changed(NAME, GROUP) 1 if the machine differs from the top snapshot in that group
depth(NAME) how many snapshots the stack holds (0 when empty)
dump_diff NAME log every field that differs from the top snapshot, one line each

What a snapshot holds

The registers AF BC DE HL IX IY SP PC AF2 BC2 DE2 HL2 I R (and their 8-bit halves A B C D E H L F), IFF1 IFF2 IM, STACK0 (the word at SP), the eight MMU slots MMU[0] to MMU[7] (as mmu[s] reads them), FRAME and CYCLE. Each is a field: isr.SP, isr.STACK0, isr.MMU[7], isr.CYCLE. Flags and HALTED are not fields.

The comparison groups

changed(NAME, …) True when
regs any of AF BC DE HL IX IY AF2 BC2 DE2 HL2 SP differs
mmu any of the eight slots differs
iff1 IFF1 differs
stack0 the word at the current SP differs from the one captured

dump_diff logs one line per differing field, NAME: FIELD old -> new, in the order AF BC DE HL IX IY AF2 BC2 DE2 HL2 SP IFF1 IFF2 IM STACK0 MMU0 … MMU7. PC, I, R, FRAME and CYCLE are never listed: between an entry and its exit they always differ, and would bury the line that matters.

on frame 5 do
    snap s
    log "A at the snap: ${s.A:x2}, MMU slot 2: ${s.MMU[2]:x2}, STACK0 ${s.STACK0:x4}, FRAME ${s.FRAME}"
end
on frame 6 do
    log "a frame later: changed(s, regs)=${changed(s, regs)} changed(s, mmu)=${changed(s, mmu)} changed(s, iff1)=${changed(s, iff1)} changed(s, stack0)=${changed(s, stack0)}"
    dump_diff s
    exit 0
end
[debugger] [info] [jds F:5 C:3354640] A at the snap: 3F, MMU slot 2: 0A, STACK0 F302, FRAME 5
[debugger] [info] [jds F:6 C:3913744] a frame later: changed(s, regs)=1 changed(s, mmu)=0 changed(s, iff1)=0 changed(s, stack0)=0
[debugger] [info] [jds F:6 C:3913744] dump_diff s: AF 3F0B -> 3F03
[debugger] [info] [jds F:6 C:3913744] dump_diff s: HL CCD2 -> C44A

A span check: the 48K's interrupt handler

The 48K ROM's IM 1 handler starts at 0x0038 and returns with the RET at 0x0052. Snap at the entry, compare at the exit:

# span.jds — check the 48K ROM's IM 1 handler (0x0038 .. its RET at 0x0052)
on execute 0x0038 when FRAME >= 100 and FRAME < 102 do
    snap im1
    log indent (depth(im1) * 2) "==> IM 1 handler, returns to ${stack[0]:x4}"
end

on execute 0x0052 when depth(im1) > 0 do
    log indent (depth(im1) * 2) "<== RET after ${(CYCLE - im1.CYCLE) / 8} T-states, IFF1 ${im1.IFF1} -> ${IFF1}"
    dump_diff im1
    if changed(im1, regs) or changed(im1, stack0) or changed(im1, mmu) then
        stop "the IM 1 handler did not restore the machine"
    end
    unsnap im1
end

on frame 102 do exit 0 end
$ jnext --headless --machine 48k --script span.jds
[debugger] [info] SCRIPT loaded span.jds: 3 rules
[debugger] [info] [jds F:100 C:55911080]   ==> IM 1 handler, returns to 10AC
[debugger] [info] [jds F:100 C:55918056]   <== RET after 872 T-states, IFF1 0 -> 1
[debugger] [info] [jds F:100 C:55918056] dump_diff im1: IFF1 0 -> 1
[debugger] [info] [jds F:100 C:55918056] dump_diff im1: IFF2 0 -> 1
[debugger] [info] [jds F:101 C:56470168]   ==> IM 1 handler, returns to 15E6
[debugger] [info] [jds F:101 C:56477144]   <== RET after 872 T-states, IFF1 0 -> 1
[debugger] [info] [jds F:101 C:56477144] dump_diff im1: IFF1 0 -> 1
[debugger] [info] [jds F:101 C:56477144] dump_diff im1: IFF2 0 -> 1
[debugger] [info] [jds F:102 C:57587776] SCRIPT EXIT 0
[platform] [info] script requested exit 0

The ROM's handler is clean: the only difference is the interrupt flip-flops, off on entry (accepting the interrupt cleared them) and on again at the RET (its EI). That is also why the check for "interrupts re-enabled" is if not IFF1 then stop … end at the exit, not changed(…, iff1): in a correct handler IFF1 does change. when depth(im1) > 0 keeps the exit rule from firing on a RET whose entry was not seen.

The interrupt-exit audit is the full form of this check, with a call-depth trace.

Errors

A snapshot stack holds up to 4096 entries. snap on a full stack, and unsnap, a field, changed() or dump_diff on an empty one, are run-time errors, and leave the stack as it was. depth() of an empty stack is 0, not an error — it is how a rule asks.

[debugger] [info] [jds F:5 C:3354640] depth 0
[debugger] [error] SCRIPT ERROR snaperr.jds:3:5: `unsnap`: snapshot stack `s` is empty - rule at 1:1 disabled