@@ -45,19 +45,25 @@ Every stepping decision is derived from four per-record facts:
4545 the initial loop entry plus one per re-test of the ` while ` condition.
4646
4747** Stop points.** At instruction granularity the stop points are the visible
48- records. At statement granularity the stop points are the run starts. The
49- cursor must only ever come to rest on a stop point of the active granularity —
50- never on an invisible record, and (at statement granularity) never on an
51- unmapped one, except when the trace contains no stop point at all.
48+ records. At statement granularity the stop points are the run starts that
49+ survive * declaration/brace filtering* (S17, S18): a debugger user expects to
50+ rest on statements and expressions, not on ` fn ` /` impl ` headers, the
51+ ` #[contractimpl] ` export shim, or block-closing braces. The cursor must only
52+ ever come to rest on a stop point of the active granularity — never on an
53+ invisible record, and (at statement granularity) never on an unmapped or
54+ filtered-out one, except when filtering would leave the trace (or a function
55+ frame) with no stop point at all.
5256
5357## Rules
5458
5559### Session start and run boundaries
5660
5761- ** S1** (entry stop): the initial stop lands on the first stop point of the
58- trace — the first run start when line info exists, else the first visible
59- record, else index 0. The user's first view is never a sourceless,
60- addressless frame.
62+ trace — the first statement stop (the first declaration/brace-filtered run
63+ start, S17/S18) when line info exists, else the first visible record, else
64+ index 0. The user's first view is a real statement inside the invoked
65+ function, never the ` #[contractimpl] ` export shim or a bare signature line,
66+ and never a sourceless, addressless frame.
6167- ** S2** (forward exhaustion): a forward step (any kind, any granularity) with
6268 no further stop point ahead moves to the ** last** stop point of the trace
6369 (staying put when already there) and still reports a stop. It never
@@ -125,6 +131,43 @@ unmapped one, except when the trace contains no stop point at all.
125131 sourceless only when the cursor legitimately rests on an unmapped stop point
126132 (instruction granularity, or no line info at all).
127133
134+ ### Statement stops: declarations and braces
135+
136+ Optimization-level 0 preserves a line-table row for lines a user does not think
137+ of as steppable — the ` fn ` /` impl ` header a function is entered "at", the
138+ ` #[contractimpl] ` macro shim that wraps every exported entry point, and the
139+ closing brace the compiler attributes a function's return/epilogue to. Left
140+ unfiltered these produce stops on declarations and lone braces, which read as
141+ noise. Statement-granularity stop points are therefore the run starts minus the
142+ following (instruction granularity is unaffected — every visible record remains
143+ an instruction stop):
144+
145+ - ** S17** (declaration suppression): a run start whose source line is a
146+ * declaration* — an attribute line (trimmed text begins ` #[ ` or ` #![ ` ,
147+ including the ` #[contractimpl] ` export shim) or an item header (` fn ` , with any
148+ ` pub ` /` const ` /` async ` /` unsafe ` /` extern ` qualifiers; ` impl ` ; ` trait ` ; ` mod ` ) —
149+ is not a statement stop. Step-in therefore lands on a function's first real
150+ body statement, and the session's entry stop (S1) never rests on the export
151+ shim or a signature line. ** Exception:** when a declaration line is the * only*
152+ run start of its function-body frame — a fully collapsed one-line function
153+ whose body shares the signature's line (common above opt-0) — it is kept, so
154+ step-in still has a target and S4/S16 hold. Attribute lines are never kept by
155+ this exception (the export shim is always glue); only ` fn ` /` impl ` headers are.
156+ - ** S18** (brace suppression): a run start whose source line is a bare closing
157+ brace (trimmed text is ` } ` , ` }; ` , or ` }, ` ) is not a statement stop ** unless**
158+ it is the function's * final* brace — the epilogue the return is attributed to,
159+ recognized as a brace run start immediately followed (in run order) by a
160+ shallower-depth run start, or by nothing. That single epilogue brace is kept
161+ as the one place to inspect the return value before the frame unwinds; every
162+ inner block-closing brace is suppressed. When a frame's epilogue is instead
163+ attributed to a declaration line (macro-generated export wrappers map their
164+ return to ` #[contractimpl] ` ), S17 governs and no epilogue stop is kept for it.
165+
166+ Filtering never empties the trace: if it would remove every stop, the unfiltered
167+ run starts stand (preserving S1/S2/S3). Breakpoints and instruction-granularity
168+ stepping are unchanged — S17/S18 shape only where statement stepping comes to
169+ rest.
170+
128171## Build prerequisite: optimization level
129172
130173Statement stepping is only as good as the DWARF line table the build emits, and
@@ -157,40 +200,46 @@ crate's production `opt-level` for the debug build only — the crate's
157200- ` test/fixtures/stepper-debug.{wasm,trace.jsonl} ` — ` sum_triples(3) ` on
158201 ` examples/stepper ` : a real ` call ` to an ` #[inline(never)] ` helper ** with
159202 implicit return** (no ` return ` record — S5, S7, S8), and a 3-iteration
160- ` while ` loop (S6, S9, S12, S13, S15). Ground truth (record ↦ line):
161- entry shim = lib.rs:20, loop condition = lib.rs:25 (records 21–26, 39–43,
162- 56–60, 73–77), call line = lib.rs:26 (27–28, 44–45, 61–62), callee body =
163- lib.rs:15 (29–31, 46–48, 63–65), epilogue ` return ` = lib.rs:20 (84).
164- Correct depths: callee records 29–31/46–48/63–65 are one frame deeper than
165- the surrounding loop records; the trace both enters and leaves ` triple `
166- three times.
203+ ` while ` loop (S6, S9, S12, S13, S15). Built above opt-0, so ` triple ` 's body
204+ collapses onto its signature line. Statement stops (trace index ↦ line):
205+ 21 (:25 ` while ` ), 27 (:26 call line), ** 29 (:15 ` triple ` )** , 39/44 (:25/:26),
206+ 46 (:15), 56/61 (:25/:26), 63 (:15), 73 (:25). The ` #[contractimpl] ` shim
207+ (record 5, :20) and the epilogue that maps back to it (record 84, :20) are
208+ dropped by S17; ` triple ` 's only line is its ` fn ` signature, kept by the S17
209+ sole-frame-stop exception so step-in still lands (records 29/46/63 are one
210+ frame deeper — the trace enters and leaves ` triple ` three times).
167211
168212- ` test/fixtures/control-debug.wasm ` + ` control-{seq,branch,count,while_call,choose}.trace.jsonl `
169213 — one opt-0 wasm (` examples/control ` ) shared by five traces, each isolating a
170214 Rust construct so the suite pins how source stepping crosses it. All five
171- functions live in ` impl Control ` (the ` #[contractimpl] ` export shim maps to
172- ` lib.rs:20 ` , entered at depths 0 then 1; the function body runs at depth 2, a
173- called ` bump ` at depth 3). Statement-stop ground truth (trace index ↦ line):
174- - ** seq(7)** — pure sequence: 6/21 (:20 shim), 219 (:23 sig), 236 (:24 ` let a ` ),
175- 249 (:25 ` let b ` ), 262 (:26 ` let c ` ), 265 (:28 return). Lines strictly
176- increase; one stop each (S4/S5 basics, no re-execution).
177- - ** branch(3)** — if/else, ` 3 <= 10 ` so the ** else** arm runs: 219 (:31 sig),
178- 226 (:33 ` if ` ), 244 (:36 else arm), 245 (:33 merge), 246 (:38 ` r ` ), 248
179- (:39). The ** then** arm (` :34 ` ) never appears — stepping enters only the
180- taken arm.
181- - ** count(3)** — ` for i in 0..3 ` : 219 (:42 sig), 228 (:43 ` let acc ` ), then the
182- ` for ` header (:44) and the body ` acc += i ` (:45) alternate per iteration (:44
183- at 233/414/561/708 — three iterations plus the terminating ` next() -> None `
184- check; :45 at 400/547/694 — the body), 805 (:47 ` acc ` ), 808 (:48). The body
185- line stops exactly once per iteration (S6/S12) — the defining loop behavior.
186- - ** while_call(3)** — ` while ` + a real ` bump ` call: 52/53/54 setup, then per
187- iteration 55 (` while ` ), 56 (` acc += bump(i) ` ), ** 15/16/18 = ` bump ` body at
188- depth 3** , 57 (` i += 1 ` ); three times; then 55 (final test), 59/60. ` stepIn `
189- at :56 descends to :15; ` next ` at :56 steps over to :57 (S4 vs S5); the
215+ functions live in ` impl Control ` ; the ` #[contractimpl] ` export shim (:20, at
216+ depths 0 then 1) and each ` pub fn ` signature are dropped by S17, so the body
217+ (depth 2, a called ` bump ` at depth 3) is where stepping rests. Statement-stop
218+ ground truth (trace index ↦ line, post-S17/S18):
219+ - ** seq(7)** — pure sequence: 236 (:24 ` let a ` ), 249 (:25 ` let b ` ), 262 (:26
220+ ` let c ` ), 265 (:28 ` } ` epilogue kept by S18). Body lines strictly increase;
221+ one stop each (S4/S5 basics, no re-execution). Dropped: 6/21 (:20 shim),
222+ 219 (:23 sig).
223+ - ** branch(3)** — if/else, ` 3 <= 10 ` so the ** else** arm runs: 226 (:33 ` if ` ),
224+ 244 (:36 else arm), 245 (:33 merge), 246 (:38 ` r ` ), 248 (:39 ` } ` ). The
225+ ** then** arm (` :34 ` ) never appears — stepping enters only the taken arm.
226+ - ** count(3)** — ` for i in 0..3 ` : 228 (:43 ` let acc ` ), then the ` for ` header
227+ (:44) and the body ` acc += i ` (:45) alternate per iteration (:44 at
228+ 233/414/561/708 — three iterations plus the terminating ` next() -> None `
229+ check; :45 at 400/547/694 — the body), 805 (:47 ` acc ` ), 808 (:48 ` } ` ). The
230+ body line stops exactly once per iteration (S6/S12) — the defining loop
231+ behavior.
232+ - ** while_call(3)** — ` while ` + a real ` bump ` call: 228/231 (:53/:54 setup),
233+ then per iteration 234 (:55 ` while ` ), 243 (:56 ` acc += bump(i) ` ), ** 266
234+ (:16 ` bump ` body) / 278 (:18 ` bump ` ` } ` epilogue) at depth 3** , 291 (:57
235+ ` i += 1 ` ); three times (266/278, 337/349, 408/420); then 447 (:55 final
236+ test), 456/459 (:59 ` acc ` / :60 ` } ` ). ` stepIn ` at :56 descends to the first
237+ ` bump ` ** statement** :16 — the ` fn bump ` signature (records 249/320/391,
238+ :15) is dropped by S17; ` next ` at :56 steps over to :57 (S4 vs S5); the
190239 callee returns implicitly (no ` return ` record — S5/S7/S8).
191- - ** choose(7)** — ` match x % 3 ` , ` 7 % 3 == 1 ` so arm ** ` 1 => 200 ` ** runs: 219
192- (:63 sig), 228 (: 64 ` match ` ), 240 (:66 arm), 243 (:69 ` r ` ), 245 (:70). Arms
193- ` 0 => ` (:65) and ` _ => ` (:67) never appear.
240+ - ** choose(7)** — ` match x % 3 ` , ` 7 % 3 == 1 ` so arm ** ` 1 => 200 ` ** runs: 228
241+ (:64 ` match ` ), 240 (:66 arm), 243 (:69 ` r ` ), 245 (:70 ` } ` ). Arms ` 0 => `
242+ (:65) and ` _ => ` (:67) never appear.
194243
195244Regenerate all pairs together with ` scripts/make-fixtures.sh ` .
196245
0 commit comments