Assertions
An assertion is a claim written in your source and checked as the program runs. FantASM records it in the SLD, and Bizmuth stops where it fails.
ld hl,4
call copy_row
!assert hl==4,"HL survived the copy"With the SLD loaded, a false claim stops the machine:
assert failed at main.asm:88 — HL survived the copy (hl==4)FantASM takes each annotation with or without its !: assert and !assert are the same directive, as are debug and !debug, and watch and !watch. The SLD always records the ! spelling.
Image cost
An annotation adds no bytes and no cycles to the assembled image. It is recorded beside the program, not in it, so there is no debug build to turn off and nothing to strip before release.
Without the SLD loaded, no annotation acts. The SLD is what carries them.
Claims
A claim is anything a condition can say about registers, flags, memory, Next registers, ports and the names in your own program; the language is on Breakpoints and Conditions:
assert b==0
assert mem[player_x:2] < 320,"player is on screen"
assert nr[$56]==20,"the right bank is paged in"The claim ends at the first comma; the quoted message after it is not part of the condition. The message appears in the report and nowhere else. An assertion with no message reports its claim.
Assertion stops
An assertion stops where it was written, in the bank it was written in. It is a breakpoint carrying the opposite of your claim, so it fires when the claim is false and only then. A claim that holds does not stop the machine.
The failure is reported in the log and in the debugger window, and a connected debugger sees it too. DeZog shows the claim and its message as the reason for the stop. An ADP client receives reason=assert, with at= naming the line the assertion was written on, not the line the program counter is on. The message is also shown on the breakpoint’s entry in bp list.
⚠ An assertion in a loop stops on the first failing iteration, every time. For a later failure, tighten the claim, or let a script decide, as below.
Script control
A script’s on_breakpoint runs on an assertion stop as on any other, and annotation() says which kind of stop it was: an empty map for an ordinary breakpoint.
fn on_breakpoint(id, pc) {
let a = annotation();
if !a.is_empty() {
log(a.site + " claimed " + a.claim + ": " + a.message);
}
"continue"
}Returning "continue" resumes, so a handler can log every failure and stop only at the one it is looking for.
Headless runs
Annotations act the same in a headless capture (--screenshot), except that a failed claim is reported and the run continues, since nobody is there to resume it. One run then reports every failed claim.
At the end of the run, a report counts the claims armed, the claims execution reached, and the claims that failed:
assert failed at main.asm:88 — HL survived the copy (hl==4)
12 claim(s) armed, 9 checked, 1 failedThe summary is logged as a warning when a claim failed or was never reached, and as information otherwise. A run that reached none of its claims adds a second warning:
no claim was reached: the run did not execute the code they were written inA run with no claims armed prints no report.
A run that declares page windows, with map add or a script’s map_page, also gets a line per window (see Breakpoints and Conditions). It says how many of the annotations written in the window were reached, and names the page that ran their addresses where that was a different page:
map: 6000-7FFF page 1C: 1 of 3 annotation(s) reached; page 19 ran 3 of themThe exit status is unchanged, so an existing capture check keeps its meaning. Read the log, or use a script.
!watch
!watch arms a register watchpoint for the whole run. It is bound to no address: the line it is written on is a marker, and the watch covers every instruction from the moment the SLD loads.
!watch sp < $7F00The operand is a register, a comparison and a value, written as in a condition. The watch breaks on the instruction that makes the comparison true, and again each time it becomes true after having been false. wp list shows it as watch main.asm:12 (sp < $7F00), and wp del removes it.
A watch whose operand is not a register, a comparison and a value is refused when the SLD loads, naming its source line.
A watch is the one annotation that needs no memory management, so it also works on the classic Spectrums.
debug
debug makes no claim and never stops the machine.
debug "entered draw_row" ; logged, every time
debug draw_row_state ; a function in your script, calledQuoted text is logged as written. Anything else is the name of a function in the loaded script, called with the machine paused, so it can read registers and memory and log what it finds.
A debug acts every time its address is reached, so in a loop it logs every iteration.
Annotation rules
An annotation inside a macro is recorded once per expansion. Twenty uses of the macro are twenty places to stop, each naming the line the macro was invoked from.
Annotations on the same instruction share one stop, and all of them act. A debug written beside an assert still logs when the claim fails. FantASM records an annotation against the instruction that follows it, so two on consecutive lines land on the same address.
A second assert on one instruction is the exception. One stop cannot report two failed claims, so the second is refused when the SLD loads, naming both.
Every annotation is a breakpoint, and all of them appear in the breakpoint list. Bizmuth says how many it armed when the SLD loads:
sld: armed 14 annotation(s), 12 page-qualifiedAn annotation that does not parse is refused on its own, naming its source line, and the rest still arm. Loading an SLD also warns about a claim that could never be true, before the program runs.
⚠ Assertions and debug annotations are page-qualified, so they need the Next’s memory management and do not fire on a classic Spectrum. Loading an SLD carrying them on a 48K says so.
See also
- Breakpoints and Conditions: the condition language an assertion is written in.