asmdiff 0.3.0__tar.gz → 0.4.0__tar.gz

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.
Files changed (57) hide show
  1. {asmdiff-0.3.0 → asmdiff-0.4.0}/PKG-INFO +544 -123
  2. {asmdiff-0.3.0 → asmdiff-0.4.0}/README.md +542 -121
  3. {asmdiff-0.3.0 → asmdiff-0.4.0}/asmdiff.example.toml +45 -6
  4. asmdiff-0.4.0/asmdiff.py +3753 -0
  5. asmdiff-0.4.0/golden/across-two-files.txt +14 -0
  6. asmdiff-0.4.0/golden/elf-span-stats.txt +17 -0
  7. asmdiff-0.4.0/golden/elf.txt +14 -0
  8. asmdiff-0.4.0/golden/inspect-span-stats.txt +14 -0
  9. asmdiff-0.4.0/golden/inspect-two-cc.txt +15 -0
  10. asmdiff-0.4.0/golden/inspect.txt +11 -0
  11. asmdiff-0.4.0/golden/pairs-cost.txt +5 -0
  12. asmdiff-0.4.0/golden/pairs-s.txt +11 -0
  13. asmdiff-0.4.0/golden/pairs-two-cc.txt +25 -0
  14. asmdiff-0.4.0/golden/pairs.txt +10 -0
  15. asmdiff-0.4.0/golden/summary-two-files.txt +8 -0
  16. {asmdiff-0.3.0 → asmdiff-0.4.0}/pyproject.toml +19 -0
  17. asmdiff-0.4.0/test_asmdiff.py +3784 -0
  18. asmdiff-0.3.0/.claude/settings.local.json +0 -12
  19. asmdiff-0.3.0/2026-07-06-inspect-mode.md +0 -754
  20. asmdiff-0.3.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
  21. asmdiff-0.3.0/2026-07-07-elf-mode-handoff.md +0 -156
  22. asmdiff-0.3.0/2026-07-07-table-layout-handoff.md +0 -90
  23. asmdiff-0.3.0/2026-07-10-reverb-session-feedback-handoff.md +0 -102
  24. asmdiff-0.3.0/CLAUDE.md +0 -33
  25. asmdiff-0.3.0/FIELD-TEST-REPORT.md +0 -123
  26. asmdiff-0.3.0/Screenshot 2026-07-07 225145.jpg +0 -0
  27. asmdiff-0.3.0/amy-hotspot-analysis.md +0 -214
  28. asmdiff-0.3.0/asmdiff.py +0 -1963
  29. asmdiff-0.3.0/demo/delay_macro.c +0 -414
  30. asmdiff-0.3.0/demo/delay_orig.c +0 -355
  31. asmdiff-0.3.0/demo/delay_restrict.c +0 -361
  32. asmdiff-0.3.0/demo/delay_struct.c +0 -415
  33. asmdiff-0.3.0/demo/esp32-matrix.gif +0 -0
  34. asmdiff-0.3.0/demo/esp32-matrix.png +0 -0
  35. asmdiff-0.3.0/demo/quickstart.gif +0 -0
  36. asmdiff-0.3.0/demo/quickstart.png +0 -0
  37. asmdiff-0.3.0/dist_1/.gitignore +0 -1
  38. asmdiff-0.3.0/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
  39. asmdiff-0.3.0/dist_1/asmdiff-0.1.1.tar.gz +0 -0
  40. asmdiff-0.3.0/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
  41. asmdiff-0.3.0/dist_1/asmdiff-0.1.2.tar.gz +0 -0
  42. asmdiff-0.3.0/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
  43. asmdiff-0.3.0/dist_1/asmdiff-0.1.3.tar.gz +0 -0
  44. asmdiff-0.3.0/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
  45. asmdiff-0.3.0/dist_1/asmdiff-0.1.4.tar.gz +0 -0
  46. asmdiff-0.3.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +0 -277
  47. asmdiff-0.3.0/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -97
  48. asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +0 -147
  49. asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +0 -16
  50. asmdiff-0.3.0/gif-plan.md +0 -248
  51. asmdiff-0.3.0/hackaday-tip-draft.md +0 -101
  52. asmdiff-0.3.0/hn-show-draft.md +0 -57
  53. asmdiff-0.3.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +0 -3
  54. asmdiff-0.3.0/test_asmdiff.py +0 -2302
  55. {asmdiff-0.3.0 → asmdiff-0.4.0}/.gitignore +0 -0
  56. {asmdiff-0.3.0 → asmdiff-0.4.0}/LICENSE +0 -0
  57. {asmdiff-0.3.0 → asmdiff-0.4.0}/asmdiff_example.c +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: asmdiff
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
5
5
  Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
6
6
  Project-URL: Repository, https://github.com/rt-rtos/asmdiff
@@ -76,6 +76,39 @@ the tool is a single stdlib-only file: `python3 asmdiff.py HARNESS.c`.
76
76
  Requires Python >= 3.8; `asmdiff.toml` config files need >= 3.11
77
77
  (stdlib `tomllib`).
78
78
 
79
+ ### Shell completion
80
+
81
+ ```bash
82
+ asmdiff --install-completion # bash, zsh or fish, taken from $SHELL
83
+ ```
84
+
85
+ This writes a completion script into the directory your shell already
86
+ autoloads from - `bash-completion`'s `completions/`,
87
+ `~/.config/fish/completions/`, or `~/.zfunc/` - and never touches an rc
88
+ file. A file asmdiff did not write is named and left alone rather than
89
+ replaced. zsh reads `~/.zfunc` only once it is on `fpath`, so the command
90
+ prints the two lines to add to `.zshrc` yourself:
91
+
92
+ ```zsh
93
+ fpath=(~/.zfunc $fpath)
94
+ autoload -Uz compinit && compinit
95
+ ```
96
+
97
+ To keep everything in one rc file instead, `--completion SHELL` prints
98
+ the script to stdout:
99
+
100
+ ```bash
101
+ eval "$(asmdiff --completion bash)" # in .bashrc
102
+ ```
103
+
104
+ The flag list is read off the argument parser when the script is
105
+ generated, so a script regenerated after an upgrade cannot drift from the
106
+ tool. Completing `-t` offers the targets and groups from the config that
107
+ run would resolve - a `--config PATH` already on the line is honoured -
108
+ and a comma list is completed element by element. Each tab press starts
109
+ Python and parses the config: fine for a `pip`, `pipx` or `uv tool`
110
+ install, noticeably slow behind a cold `uvx` launcher.
111
+
79
112
  **Portability:** pure-stdlib Python with nothing intentionally
80
113
  platform-specific, developed and tested on Linux. On native Windows,
81
114
  `HOME` is usually unset, so replace `$HOME` with `$USERPROFILE` (or
@@ -100,10 +133,21 @@ Run:
100
133
  $ asmdiff myharness.c
101
134
  ```
102
135
 
103
- Output (gcc section shown, one per compiler):
136
+ Output:
104
137
 
105
138
  ```
106
- == gcc -O3 ... ==
139
+ cc#1: gcc -O3
140
+ cc#2: clang -O3
141
+
142
+ target function role insns loop spans calls
143
+ cc#1 old_scale baseline 3 - -
144
+ cc#1 new_scale candidate 3 - ldexpf
145
+ cc#1 delta 0 - +ldexpf
146
+ cc#2 old_scale baseline 2 - -
147
+ cc#2 new_scale candidate 2 - ldexpf
148
+ cc#2 delta 0 - +ldexpf
149
+
150
+ == cc#1 ==
107
151
 
108
152
  old_scale | new_scale
109
153
  ---------------------------------------------+---------------------------------------------
@@ -111,21 +155,36 @@ endbr64 | endbr64
111
155
  mulss .LC0(%rip), %xmm0 | movl $-5, %edi
112
156
  ret | jmp ldexpf@PLT
113
157
 
114
- function role insns loop spans calls
115
- old_scale baseline 3 - -
116
- new_scale candidate 3 - ldexpf
158
+ == cc#2 ==
159
+
160
+ old_scale | new_scale
161
+ ---------------------------------------------+---------------------------------------------
162
+ mulss .LCPI0_0(%rip), %xmm0 | movl $-5, %edi
163
+ retq | jmp ldexpf@PLT
117
164
  ```
118
165
 
166
+ Every run has the same three parts: a legend naming each row of the
167
+ compiler matrix, one table covering the whole matrix, then the listings
168
+ grouped under `== LABEL ==`. The label in the `target` column is the
169
+ config target a row came from; the two fallback compilers here are
170
+ unnamed, so they are numbered by their position in the matrix. A single
171
+ unnamed row (one `--cc`) is its own label, and then the legend and the
172
+ `target` column are both dropped - the blocks further down this README
173
+ show both shapes.
174
+
119
175
  Read the `calls` column first: `-` means the construct lowered to inline
120
- instructions; a symbol name means a libcall. The side-by-side asm above it is
121
- the evidence.
176
+ instructions; a symbol name means a libcall. The listings under the table
177
+ are the evidence.
122
178
 
123
179
  The `loop spans` column reports `label:N` for every local label that some
124
180
  instruction branches back to: N instructions lie between the label and the
125
181
  last backward branch targeting it. Whole-function `insns` charges loop-hoisting
126
182
  changes for their one-time setup/writeback code; the span count is the part
127
183
  that repeats. Which span is your hot loop — and how often it runs — the
128
- listing and your source know, not the tool.
184
+ listing and your source know, not the tool. A function with more than
185
+ six spans shows the first six and a `+N more` tail, since past that the
186
+ column stops being readable and the count is what is left to say;
187
+ `--json` carries every span.
129
188
 
130
189
  A worked example is included — `asmdiff_example.c` reproduces the
131
190
  exp2f/ldexpf analysis for both constant and runtime shift amounts:
@@ -159,7 +218,10 @@ uint32_t new_elapsed_ms(uint64_t now, uint64_t then) {
159
218
  ```
160
219
  $ asmdiff elapsed.c --cc 'xtensa-esp32s3-elf-gcc -O2 -mlongcalls'
161
220
 
162
- == xtensa-esp32s3-elf-gcc -O2 -mlongcalls ==
221
+ function role insns loop spans calls
222
+ old_elapsed_ms baseline 10 - __udivdi3
223
+ new_elapsed_ms candidate 6 - -
224
+ delta -4 - -__udivdi3
163
225
 
164
226
  old_elapsed_ms | new_elapsed_ms
165
227
  ---------------------------------------------+---------------------------------------------
@@ -173,12 +235,11 @@ sub a11, a3, a11 |
173
235
  call8 __udivdi3 |
174
236
  mov.n a2, a10 |
175
237
  retw.n |
176
-
177
- function role insns loop spans calls
178
- old_elapsed_ms baseline 10 - __udivdi3
179
- new_elapsed_ms candidate 6 - -
180
238
  ```
181
239
 
240
+ One `--cc` row is its own label, so this run prints neither a legend nor
241
+ a `target` column and the listing needs no `== LABEL ==` header.
242
+
182
243
  The candidate is six inline instructions ending in a multiply-high by
183
244
  the reciprocal constant in `.LC0`. The baseline hands the division to
184
245
  `__udivdi3` - a software divide loop whose cost the visible 10
@@ -198,11 +259,12 @@ the file - no harness, no pairing:
198
259
 
199
260
  $ asmdiff src/oscillators.c render_lut
200
261
 
201
- With one usable compiler you get the function's listing and a stats
202
- row; with exactly two (the default gcc + clang matrix) the listings
203
- appear side by side; with a bigger matrix each compiler gets its own
204
- block. `-l list` / `-l side-by-side` forces a presentation. Several
205
- function names can be given at once.
262
+ With one usable compiler you get a stats row and the function's
263
+ listing under it; with exactly two (the default gcc + clang matrix)
264
+ the two listings are set side by side; with a bigger matrix the table
265
+ covers the whole matrix and each row's listing follows under its own
266
+ `== LABEL ==`. `-l list` / `-l side-by-side` forces a presentation.
267
+ Several function names can be given at once.
206
268
 
207
269
  A bare name is inspected as a function; an argument that exists on
208
270
  disk is a second source file, and a path-looking argument that does
@@ -215,6 +277,9 @@ like this:
215
277
  ```
216
278
  $ asmdiff dsp_util.c apply_gain --cc 'xtensa-esp32s3-elf-gcc -O2 -mlongcalls'
217
279
 
280
+ function insns loop spans calls
281
+ apply_gain 13 .L3_LEND:4 -
282
+
218
283
  apply_gain:
219
284
  entry sp, 32
220
285
  wfr f1, a4
@@ -232,9 +297,6 @@ apply_gain:
232
297
  .L3_LEND:
233
298
  .L1:
234
299
  retw.n
235
-
236
- function insns loop spans calls
237
- apply_gain 13 .L3_LEND:4 -
238
300
  ```
239
301
 
240
302
  The span `.L3_LEND:4` is the four-instruction body of the Xtensa
@@ -283,12 +345,22 @@ everywhere else; two things are ELF-specific:
283
345
  body as a ZOL, `render_lut_cub` fell back to a 74-instruction
284
346
  branch loop.
285
347
  - **`-mlongcalls` calls are resolved.** An out-of-range call survives
286
- linking as `l32r a8, <lit>` + `callx8 a8`, which would otherwise
287
- report a call to "a8". When the disassembly annotates the literal
288
- with its value (`l32r a8, ... (40002274 <__divsf3>)`), the real
289
- callee is reported instead. Calls stay register-named when the
290
- evidence is missing or the register is overwritten in between -
291
- a genuine function-pointer dispatch still reads as indirect.
348
+ linking as `l32r a8, <lit>` + `callx8 a8`, which has no callee name
349
+ of its own. When the disassembly annotates the literal with its
350
+ value (`l32r a8, ... (40002274 <__divsf3>)`), the real callee is
351
+ reported instead. When the evidence is missing or the register is
352
+ overwritten in between, the listing keeps the raw `callx8 a8` and
353
+ the `calls` column reports `indirect(a8)` - a genuine
354
+ function-pointer dispatch still reads as indirect, without the
355
+ register name masquerading as a symbol.
356
+ - **Nearest-symbol noise is stripped.** objdump names every address
357
+ after the closest preceding symbol, so a literal pool word renders
358
+ as e.g. `l32r a8, <_stext+0x44> (<some_font_table+0x707ad9>)` - two
359
+ unrelated symbols decorating an address and a constant. Offset-form
360
+ annotations of foreign symbols become raw hex
361
+ (`l32r a8, 0x40370100 (0x3f8f5f80)`); bare-symbol annotations (a
362
+ real callee or object start) and offsets into the current function
363
+ itself are kept.
292
364
 
293
365
  What this looks like on real firmware: this binary's sequencer path -
294
366
  here in the vendored AMY synth engine (`components/amy/src/sequencer.c`,
@@ -301,7 +373,7 @@ $ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|time
301
373
  function insns loop spans calls
302
374
  sequencer_timer_callback$lto_priv$0 77 .L21:41 __divsf3, __udivdi3
303
375
  sequencer_recompute 46 - __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
304
- sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, a8
376
+ sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, indirect(a8)
305
377
  ```
306
378
 
307
379
  A timer callback pays a software float divide *and* a 64-bit
@@ -318,8 +390,8 @@ is the double-promotion smell (typically an unsuffixed `60.0`-style
318
390
  literal) on a chip whose FPU is single-precision only. And the
319
391
  `$lto_priv$0` suffixes show LTO renaming the survivors, which is why
320
392
  `--filter` matters: you can't name symbols you don't know exist. The
321
- lone `a8` is a genuine function-pointer dispatch, reported as the
322
- register rather than guessed at.
393
+ lone `indirect(a8)` is a genuine function-pointer dispatch, reported
394
+ as such rather than guessed at.
323
395
 
324
396
  Functions LTO inlined away don't table at all, and the error says so
325
397
  usefully:
@@ -357,12 +429,16 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
357
429
 
358
430
  ```
359
431
  asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
360
- [--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
432
+ [--target NAME]... [--cc 'CC FLAGS']... [--config PATH]
361
433
  [--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
362
- [--summary-only] [--collapse] [--span-stats]
434
+ [--filter REGEX] [--summary-only] [--collapse] [--span-stats]
435
+ [--cost] [--costs NAME] [--fail-on-growth] [--json] [--width N]
363
436
  [--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
364
437
  asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
365
- [--summary-only] [--span-stats]
438
+ [-l list] [--summary-only] [--span-stats] [--cost] [--costs NAME]
439
+ [--json] [--width N]
440
+ asmdiff --edit-config | --example-config | --list-targets | --version
441
+ asmdiff --completion bash|zsh|fish | --install-completion [SHELL]
366
442
  ```
367
443
 
368
444
  | Option | Meaning |
@@ -370,19 +446,26 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
370
446
  | `SOURCE.c` | C file to compile — a purpose-built harness or a real project source. A second file may be given: with `--across` to compare a function, without it for a whole-file A/B summary. Bare names after the file are functions to inspect (see [Quick inspect](#quick-inspect-one-function-no-comparison)). An ELF binary (any name — detected by magic bytes) switches to [ELF input](#elf-input-what-actually-shipped-after-lto) instead of compiling. |
371
447
  | `-p, --pair OLD:NEW` | Compare two *different* functions within one compilation. Repeatable. Default: every `old_X` is auto-paired with its `new_X`; with no pairs at all, the whole-file summary is printed instead. |
372
448
  | `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
373
- | `-l, --layout list\|side-by-side` | Force the inspect-mode presentation instead of the adaptive default (1 usable compiler lists, 2 go side by side, more list). Inspect mode only. |
374
- | `--filter REGEX` | ELF input only: also analyze every function whose name matches `REGEX` (`re.search`) — sweep a subsystem without naming each function. Matches appear in the stats table but are not listed in full. |
449
+ | `-l, --layout list\|side-by-side` | Force the inspect-mode presentation instead of the adaptive default (1 usable compiler lists, 2 go side by side, more list). With ELF input, `list` also prints `--filter` matches' listings. |
450
+ | `-f, --filter REGEX` | Also analyze every function whose name matches `REGEX` (`re.search`) — sweep a subsystem, or reach compiler-generated clones (`$constprop$0`, `.isra.0`), without naming each function. In compile modes matches are full peers of named functions (lenient when a match exists under only part of the matrix) and narrow the whole-file summary; in ELF mode matches appear in the stats table but are not listed in full — add `-l list` to print their listings too (a trailing note reminds you when listings were withheld). Not combinable with `--pair`/`--across`. |
375
451
  | `--objdump PATH` | Disassembler for ELF input. Default: derived from the first gcc in the matrix by swapping the trailing `gcc` for `objdump`, so `--target`/config globs locate it like they locate the compiler. |
376
452
  | `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
377
- | `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
453
+ | `-t, --target NAME` | A named target from the config file, resolved to a `--cc` entry. `NAME` may also be a `[groups]` entry, a comma-separated list, or a glob over target names (`-t 'esp32c*'`). Repeatable; appended to the matrix after `--cc` entries. See [Target groups](#target-groups). |
378
454
  | `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
455
+ | `--list-targets` | Print the resolved config's `default`, groups, cost profiles (name and `measured_on`), and targets (name and `cc`), then exit. No source file needed. |
379
456
  | `-db, --compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, walk up from the CWD checking each directory and its `build/` until the repository root. See [below](#borrowing-includes-from-compile_commandsjson). |
380
457
  | `--flags-like PATH` | A source with no `compile_commands` entry borrows the flags recorded for `PATH` — the way to compare a modified copy of a project source under its original's header environment. |
381
458
  | `--db-includes` | Borrow only the header-search paths from the database, dropping its defines, forced includes, and `-specs`/`--sysroot`; kept paths are re-emitted as `-idirafter` so they cannot shadow the host's own system headers. This is how a host target compiles a cross project's source. |
382
459
  | `-s`, `--summary-only` | Print only the summary/stats tables, suppressing every assembly listing (see [Shaping the output](#shaping-the-output-for-reading-vs-deciding)). |
383
- | `--json` | Emit the summary as JSON on stdout instead of tables — one record per function per compiler. Implies `--summary-only`; errors stay plain text on stderr. |
384
- | `--collapse` | In side-by-side listings, elide runs of identical line pairs, keeping 3 lines of context around each difference. |
385
- | `--span-stats` | Follow each stats table with a per-loop-span instruction mix: load/store/mul/branch/other counts per span. |
460
+ | `--json` | Emit the summary as JSON on stdout instead of tables — one record per function per compiler, each carrying the `target` label of the matrix row it came from. Implies `--summary-only`; errors stay plain text on stderr. |
461
+ | `-C, --collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
462
+ | `--width N` | Column budget for tables and side-by-side listings. Default: the terminal's width, else `$COLUMNS`, else 120. `0` is unlimited, leaving the callee column untrimmed for a script to grep; a negative `N` is a usage error. |
463
+ | `--span-stats` | Follow the stats table with a per-loop-span instruction mix: nesting depth and load/store/mul/div/branch/other counts per span. |
464
+ | `--cost` | Add a `cost` column to the stats tables: the instruction classes of each function and the tier of every call site (`softfp`, `softfp-div`, `int-div`, `libm`, `mem`, `call`), with the sites a loop span holds counted apart. Counts only; a score appears when a profile prices them. See [Cost column](#cost-column). |
465
+ | `--costs NAME` | Price the cost column with the config's `[costs.NAME]` profile on every matrix row, `--cc` rows and ELF input included; implies `--cost`. Overrides a `costs = "NAME"` the target names. See [Cost profiles](#cost-profiles). |
466
+ | `--fail-on-growth` | Exit 3, naming each offender on stderr, if any candidate has more instructions than its baseline; exit 0 otherwise. Needs paired functions (`--pair`, auto-paired `old_X`/`new_X`, or `--across`). |
467
+ | `--completion bash\|zsh\|fish` | Print a completion script for that shell on stdout and exit. Its flag list is read off the argument parser at generation time, so it cannot drift from the tool. See [Shell completion](#shell-completion). |
468
+ | `--install-completion [SHELL]` | Write that script to the shell's own user completion directory and exit; no rc file is touched. `SHELL` defaults to the basename of `$SHELL`. An existing file is replaced only if asmdiff wrote it. |
386
469
  | `--version` | Print the version and exit. |
387
470
  | `-v`, `--verbose` | On compile failure, print the full compiler command and complete error output. Default shows only the compiler, the source, and the first error lines. |
388
471
  | `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
@@ -409,58 +492,150 @@ asmdiff h.c -- -fno-math-errno
409
492
  Compilers missing from `PATH` are skipped with a warning; the run fails only
410
493
  if none are usable, and that error names each missed binary
411
494
  (`no-such-gcc: not found on PATH`) so the fix is in the message, not in a
412
- warning that scrolled away. Exit status is non-zero only for operational
413
- failures (compile error — the compiler's stderr is shown — unknown `--pair`
414
- name, no usable compiler). Differing assembly is the expected result, never
415
- an error.
495
+ warning that scrolled away. A row whose compile *fails* is dropped the same
496
+ way: the remaining rows are compiled, their table and listings print, and
497
+ the failing row's compiler output follows on stderr under its label
498
+ (`error: [esp32s3] xtensa-esp32s3-elf-gcc failed on biquad.c`) once the
499
+ run has printed everything it could. One broken target no longer costs
500
+ you the others; the run still exits 1.
501
+
502
+ | Status | Meaning |
503
+ |---|---|
504
+ | 0 | The run printed what was asked for. Differing assembly is the expected result, never an error. |
505
+ | 1 | The tool failed: a compile error, an unknown `--pair` name, a config that does not load, no usable compiler in the matrix. |
506
+ | 2 | argparse's usage error - an unknown flag, a missing value, a negative `--width`. |
507
+ | 3 | `--fail-on-growth` found a candidate with more instructions than its baseline. |
416
508
 
417
509
  ## Shaping the output for reading vs deciding
418
510
 
419
- A two-file compare over a three-target matrix prints three full
420
- side-by-side listings with the summary table at the *end* of each — the
421
- right shape for reading an unfamiliar delta, and roughly 100 KB of the
422
- wrong shape when the table is all the decision needs. Three flags cut the
423
- output to purpose:
511
+ Every run leads with the table: the legend, one summary table for the
512
+ whole matrix, the `--span-stats` and provenance lines that belong to it,
513
+ and then the listings, grouped per target under `== LABEL ==`. A
514
+ two-file compare over a three-target matrix still prints three full
515
+ side-by-side listings under that table, roughly 100 KB of them when the
516
+ table was all the decision needed. Four flags cut the output to purpose:
424
517
 
425
518
  - **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
426
519
  the scripted/agent view: the table is the decision input, and a listing
427
520
  is pulled with a second, narrower run only when a delta needs
428
- explaining.
429
- - **`--collapse`** elides runs of identical line pairs in every
521
+ explaining. What is left is the legend, the table, and its footer.
522
+ - **`--collapse`** (`-C`) elides runs of identical line pairs in every
430
523
  side-by-side listing, keeping 3 lines of context around each difference
431
524
  plus a `... N identical lines ...` marker. Two ~500-instruction
432
525
  functions differing by five instructions render as a few readable hunks
433
- instead of ~1000 lines.
434
- - **`--span-stats`** follows each stats table with a per-loop-span
526
+ instead of ~1000 lines. Two sides that are equal line for line collapse
527
+ to their header with `(identical)` on the left title and no body at
528
+ all.
529
+
530
+ What it cannot collapse is the case where the two sides differ on every
531
+ line without differing in substance: a changed register allocation, a
532
+ reordered but equivalent schedule, one extra spill slot shifting every
533
+ offset. There is no identical run to elide, so the listing prints in
534
+ full. The summary table and `--span-stats` are the reading unit there:
535
+ the instruction classes, the calls, and the per-span mix say what
536
+ moved, where a line-by-line read of two differently allocated bodies
537
+ does not.
538
+ - **`--span-stats`** follows the stats table with a per-loop-span
435
539
  instruction mix, one row per span:
436
540
 
437
541
  ```
438
- function span insns load store mul branch other
439
- stereo_reverb .L108 327 96 31 14 12 174
542
+ function span depth insns load store mul div branch other
543
+ stereo_reverb .L108 0 327 96 31 14 0 12 174
440
544
  ```
441
545
 
442
546
  This weighs the span rather than the whole function — the number that
443
547
  actually decides a hot-loop comparison (is the rewrite trading loads
444
548
  for stores? did the multiplies move?) without hand-counting a listing
445
549
  and accidentally tallying past the loop end into the epilogue.
550
+ *depth* is how many other spans of the same function contain this
551
+ one, so an inner loop is told from the loop around it without reading
552
+ the listing ([How a span is found](#how-a-span-is-found)).
446
553
  Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
447
- memory-operand heuristic for x86; *branch* includes calls (outbound
448
- calls are already itemised in the `calls` column), and anything the
449
- tables don't know lands in *other*.
450
-
451
- All three combine with every compile mode; `--summary-only` and
452
- `--span-stats` also apply to ELF input.
554
+ memory-operand heuristic for x86; *branch* means any control
555
+ transfer — conditional and unconditional branches, calls, and
556
+ returns (outbound calls are already itemised by name in the `calls`
557
+ column) — and anything the tables don't know lands in *other*.
558
+ *div* is a hardware divide or square root; a division done by a
559
+ libcall (`__divsf3`, `__udivdi3`) is a call, and the Xtensa FPU
560
+ divide sequence counts as the several instructions it is.
561
+
562
+ Reading *branch* inside a span: a software loop's own backedge is
563
+ one of them, because the span runs from the label to the last
564
+ backward branch inclusive. A Xtensa zero-overhead loop instead
565
+ encloses only the body between `loop` and its end label, so the loop
566
+ machinery contributes zero and any branch counted there is real
567
+ per-iteration control flow — early exits, per-sample `if`/`else`,
568
+ wrap-around checks. Track it across variants: branches replaced by
569
+ conditional moves (`movnez`, `csel`, `cmov` — counted under *other*)
570
+ show as *branch* falling while *insns* stays roughly flat, and a
571
+ call appearing inside a hot span is the ZOL killer the `calls`
572
+ column already flagged.
573
+ - **`--cost`** adds a `cost` column to the stats tables saying what
574
+ each function is made of: the same instruction classes over the
575
+ whole body, and the tier of every call site with the sites a loop
576
+ span holds counted apart. `--costs NAME` prices those counts with a
577
+ measured profile from the config. Both are described under [Cost
578
+ column](#cost-column) below.
579
+
580
+ All four combine with every compile mode; `--summary-only`,
581
+ `--span-stats`, `--cost` and `--costs` also apply to ELF input.
582
+
583
+ Tables and side-by-side listings take their width from the terminal,
584
+ falling back off one to `$COLUMNS` and then to 120 columns; `--width N`
585
+ sets the budget directly, and `--width 0` means unlimited, which is the
586
+ untrimmed callee column a script greps.
453
587
 
454
588
  For scripted callers, `--json` replaces the tables entirely with one
455
589
  JSON document on stdout: a flat `results` list holding one record per
456
- function per compiler — `cc`, `tag` (source label in two-file runs),
457
- `role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
458
- `calls`, and `span_stats` when `--span-stats` is given. Flat records
459
- keep it one `jq` expression away from any question the tables answer:
590
+ function per compiler — `target` (the matrix row's label, the same
591
+ string the table's `target` column prints), `cc`, `tag` (source label
592
+ in two-file runs), `role` (`baseline`/`candidate` in paired runs),
593
+ `insns`, `loop_spans` (each `{label, insns, depth}`), `calls`, `delta`
594
+ (on candidate records), `cost` when `--cost` or `--costs` is given, and
595
+ `span_stats` when `--span-stats` is given. An ELF run compiles nothing,
596
+ so its records have no matrix row to name and carry neither `target`
597
+ nor `cc`. Flat records keep it one `jq` expression away from any
598
+ question the tables answer.
599
+
600
+ The pairing questions the tool answers itself, because the pairing is
601
+ its own: every summary table closes a pair with a `delta` row (signed
602
+ instruction count, per-span `before -> after`, callees gained with `+`
603
+ and lost with `-`, and under `--cost` the classes and tiers that
604
+ moved), each candidate record carries the same as a `delta` object,
605
+ and `--fail-on-growth` makes it an exit status:
460
606
 
461
607
  ```bash
608
+ # Fail the job when a rewrite that was meant to shrink did not
609
+ asmdiff old.c new.c -a stereo_reverb -t esp32s3 --fail-on-growth -s
610
+ ```
611
+
612
+ Exit 3 and one `growth: FUNC +N insns (BASELINE -> CANDIDATE)` line per
613
+ offender on stderr if any candidate has more instructions than its
614
+ baseline, exit 0 otherwise; status 1 is left meaning the tool itself
615
+ failed and 2 is argparse's usage error, so a job can tell "grew" from
616
+ "did not compile" from "misused the flags".
617
+
618
+ Arbitrary selection stays jq's: the tool prints JSON on stdout, so a
619
+ plain `| jq` pipe *is* the integration, and jq does that filtering
620
+ better than a wrapper flag could. A few recipes:
621
+
622
+ ```bash
623
+ # Instruction count per function per side, as a table
624
+ asmdiff old.c new.c --json \
625
+ | jq -r '.results[] | "\(.tag)\t\(.function)\t\(.insns)"'
626
+
627
+ # Only the functions that emit libcalls (soft-float, __divdi3, memcpy…)
628
+ asmdiff old.c new.c --json \
629
+ | jq '.results[] | select(.calls | length > 0) | {function, tag, calls}'
630
+
631
+ # The full record for one hot function, spans and all
462
632
  asmdiff old.c new.c -a stereo_reverb --json --span-stats \
463
633
  | jq '.results[] | {role, insns, spans: .loop_spans}'
634
+
635
+ # Functions reaching a soft-float helper from inside a loop
636
+ asmdiff old.c new.c --json --cost \
637
+ | jq '.results[] | select(.cost.tiers_in_loop.softfp > 0)
638
+ | {function, tag, in_loop: .cost.tiers_in_loop}'
464
639
  ```
465
640
 
466
641
  The top level carries `asmdiff` (version) and `mode`
@@ -468,6 +643,99 @@ The top level carries `asmdiff` (version) and `mode`
468
643
  path as `elf`). Warnings and errors stay plain text on stderr, so a
469
644
  failed run never emits half a document.
470
645
 
646
+ ### Cost column
647
+
648
+ `--cost` adds a `cost` column before `calls`, saying what each function
649
+ is made of rather than how many lines long it is:
650
+
651
+ ```
652
+ $ asmdiff asmdiff_example.c --cost -s --cc 'gcc -O2'
653
+
654
+ function role insns loop spans cost calls
655
+ old_const baseline 3 - mul:1 | br:1 | oth:1 -
656
+ new_const candidate 3 - br:1 | oth:2 | libm:1 ldexpf
657
+ delta 0 - mul:-1 | oth:+1 | libm:+1 +ldexpf
658
+ old_rt baseline 9 - st:1 | mul:1 | br:2 | oth:5 | libm:1 exp2f
659
+ new_rt candidate 2 - br:1 | oth:1 | libm:1 ldexpf
660
+ delta -7 - st:-1 | mul:-1 | br:-1 | oth:-4 +ldexpf -exp2f
661
+ ```
662
+
663
+ Instructions fall into the six classes `--span-stats` uses, abbreviated
664
+ here (`ld`, `st`, `mul`, `div`, `br`, `oth`). Every call site is
665
+ additionally tiered by the callee's name:
666
+
667
+ | Tier | Callees |
668
+ |---|---|
669
+ | `softfp` | soft-float add, subtract, multiply, compare, convert: `__addsf3`, `__muldf3`, `__floatsidf`, `__aeabi_dmul`, `__aeabi_i2d` |
670
+ | `softfp-div` | soft-float divide: `__divsf3`, `__divdf3`, `__aeabi_fdiv`, `__aeabi_ddiv` |
671
+ | `int-div` | integer divide and modulo helpers: `__udivdi3`, `__moddi3`, `__aeabi_idivmod`, `__aeabi_uldivmod` |
672
+ | `libm` | the math families, float and double: `sqrtf`, `sin`, `exp2f`, `ldexpf`, `pow`, `fmod` |
673
+ | `mem` | `memcpy`, `memset`, `memmove`, `memcmp` |
674
+ | `call` | everything else, `indirect(<reg>)` included |
675
+
676
+ The libgcc and the ARM EABI spelling of one helper land in the same
677
+ tier, so a `__muldf3` target and a `__aeabi_dmul` target read alike. A
678
+ name no pattern knows lands in `call`, where the `calls` column already
679
+ spells it out.
680
+
681
+ Each tier is followed by how many of its call sites a loop span holds -
682
+ `softfp:3 (2 in loop)` - since a soft-float helper reached once per
683
+ iteration is a different finding from one on an error path. The counts
684
+ are per call site, not per distinct callee: two calls to `__muldf3`
685
+ count twice, where the `calls` column names it once.
686
+
687
+ The delta row's cost cell lists only what moved. A tier whose count
688
+ came through the rewrite unchanged is absent from it even when the
689
+ callee changed: `old_rt` and `new_rt` above each hold one `libm` call,
690
+ so the cell says nothing about `libm` while the `calls` column carries
691
+ the swap as `+ldexpf -exp2f`. The two cells are read together.
692
+
693
+ With a profile (`--costs NAME`, or a target's `costs = "NAME"`), the
694
+ cell opens with a score and the number of instructions and call sites
695
+ no weight covered, and the table is followed by the line saying where
696
+ the weights came from:
697
+
698
+ ```
699
+ score 399 (1 unweighted) | ld:40 | st:12 | mul:14 | ...
700
+ ...
701
+ costs: esp32s3-iram - ESP32-S3 rev 0.2, code in IRAM, esp-15.2.0 libgcc; esp_cpu_get_cycle_count harness, median of 1000 runs
702
+ ```
703
+
704
+ The weights behind that score are the commented template in
705
+ `asmdiff --example-config`, not a measurement; no measured profile
706
+ ships with the tool, and [Cost profiles](#cost-profiles) below is how
707
+ one is written. The delta row leads with the signed score. A cell is
708
+ capped at 56 characters and closes with `...` when the mix does not fit
709
+ (as it does here), so the table stays aligned; `--json` carries all of
710
+ it:
711
+
712
+ ```json
713
+ "cost": {
714
+ "classes": {"load": 40, "store": 12, "mul": 14, "div": 1,
715
+ "branch": 9, "other": 61},
716
+ "tiers": {"softfp": 3, "call": 1},
717
+ "tiers_in_loop": {"softfp": 2},
718
+ "score": 399,
719
+ "unweighted": 1,
720
+ "profile": {"name": "esp32s3-iram",
721
+ "measured_on": "ESP32-S3 rev 0.2, code in IRAM, ...",
722
+ "method": "esp_cpu_get_cycle_count harness, ..."}
723
+ }
724
+ ```
725
+
726
+ `classes` always carries all six keys. `tiers` and `tiers_in_loop` list
727
+ only what the function reaches, so `.cost.tiers.softfp` on a function
728
+ with no soft-float is `null` rather than `0` - `// 0` in the jq
729
+ expression if you want a number. Without a profile, `score` and
730
+ `profile` are `null` and `unweighted` is every instruction and call
731
+ site the function has. Candidate records also gain `delta.cost`
732
+ (`classes`, `tiers`, `score`), again signed and again only what moved.
733
+
734
+ ELF input takes `--cost` and `--costs` like a compile run does. A
735
+ target's `costs = "NAME"` is not consulted there: nothing is compiled,
736
+ so no matrix row stands behind the binary's instructions. Name the
737
+ profile with `--costs`.
738
+
471
739
  ## Config file: named targets
472
740
 
473
741
  Retyping a cross-compiler path and ten flags per run is the enemy of actually
@@ -495,8 +763,9 @@ flags = ["-O2", "-DMY_FEATURE", "-I$HOME/project/components/dsp/include"]
495
763
  survives toolchain upgrades (`esp-14` → `esp-15`) without editing. A
496
764
  pattern matching several installed toolchains resolves to the highest
497
765
  version-sorted one — numerically, so `esp-15` beats `esp-9` — and the
498
- choice is printed to stderr; the `==` header in the output always shows
499
- the fully resolved command that actually ran. No match is an error. Pin
766
+ choice is printed to stderr; the legend line above the table always
767
+ shows the fully resolved command that actually ran, so the short
768
+ `target` label never hides which binary it was. No match is an error. Pin
500
769
  the exact directory instead when reproducibility matters more than
501
770
  convenience. Flags expand `$VARS` only (no globbing).
502
771
 
@@ -505,16 +774,122 @@ shapes:
505
774
 
506
775
  ```bash
507
776
  asmdiff h.c # config default target(s)
508
- asmdiff h.c --target esp32s3 --target host # two-target matrix
509
- asmdiff h.c --across f --target esp32s3 --cc 'gcc -O2' # mix freely
777
+ asmdiff h.c -t esp32s3 -t host # two-target matrix
778
+ asmdiff h.c -t esp32s3,host # same, as a comma list
779
+ asmdiff h.c --across f -t esp32s3 --cc 'gcc -O2' # mix freely
780
+ asmdiff --list-targets # what does my config define?
510
781
  ```
511
782
 
512
783
  A config placed next to your harness files travels with them: any invocation
513
- naming a source in that directory finds it, from any CWD. `default` may be a
514
- single name or a list (a whole default matrix). The
515
- included `asmdiff.example.toml` is a starting point. If a flag or include
516
- path must vary per machine, that's what per-machine config files are for —
517
- nothing lives in the tool.
784
+ naming a source in that directory finds it, from any CWD.
785
+
786
+ `default` takes whatever `-t` takes: a target name, a `[groups]` name, a
787
+ comma list, a glob over target names, or an array mixing those
788
+ (`default = ["native", "esp32s3"]`). A name it cannot resolve is the
789
+ same error `-t` would give. One idiom worth knowing is the one-member
790
+ group: `[groups] ship = ["esp32s3"]` with `default = "ship"` gives the
791
+ standing matrix a name of its own, so widening the run later is editing
792
+ the group rather than every `default` and script that mentions the
793
+ target.
794
+
795
+ The included `asmdiff.example.toml` is a starting point. If a flag or
796
+ include path must vary per machine, that's what per-machine config files
797
+ are for — nothing lives in the tool.
798
+
799
+ ### Target groups
800
+
801
+ A `[groups]` table names matrices of targets so a whole family runs
802
+ from one `-t`, without editing `default`:
803
+
804
+ ```toml
805
+ [groups]
806
+ riscv32-esp = ["esp32c3", "esp32c6", "esp32h2", "esp32p4"]
807
+ native = ["gcc", "clang"]
808
+ ```
809
+
810
+ ```bash
811
+ asmdiff h.c -t riscv32-esp # four RISC-V targets
812
+ asmdiff h.c -t native -t esp32s3 # a group plus a single target
813
+ asmdiff h.c -t 'esp32c*' # glob over target names (quote it)
814
+ ```
815
+
816
+ A `-t` value is resolved as an exact target name first, then as a group
817
+ name, then as a glob (`*`, `?`, `[...]`) over target names in config
818
+ order. A group naming an undefined target, or an empty group, is an
819
+ error. `--list-targets` prints what the resolved config defines.
820
+
821
+ ### Cost profiles
822
+
823
+ A `[costs.NAME]` table holds measured weights and where they were
824
+ measured. `--cost` alone counts; with a profile in hand it also scores:
825
+
826
+ ```toml
827
+ [costs.esp32s3-iram]
828
+ measured_on = "ESP32-S3 rev 0.2, code in IRAM, esp-15.2.0 libgcc"
829
+ method = "esp_cpu_get_cycle_count harness, median of 1000 runs"
830
+ other = 1
831
+ load = 2
832
+ store = 1
833
+ mul = 2
834
+ div = 20
835
+ branch = 2
836
+ softfp = 60
837
+ softfp-div = 200
838
+ int-div = 40
839
+ "__muldf3" = 90
840
+
841
+ [esp32s3]
842
+ cc = "..."
843
+ flags = ["-O2", "-mlongcalls"]
844
+ costs = "esp32s3-iram"
845
+ ```
846
+
847
+ A key's value decides what it is. A number is a weight, keyed by an
848
+ instruction class (`load`, `store`, `mul`, `div`, `branch`, `other`),
849
+ a libcall tier (`softfp`, `softfp-div`, `int-div`, `libm`), or a call
850
+ symbol in quotes (`"__muldf3" = 90`). A string is provenance:
851
+ `measured_on` and `method` are required and the load fails naming the
852
+ one that is missing, and any further string key is kept and printed
853
+ after them. Anything else is an error naming the key. `mem` and `call`
854
+ are refused a weight, since a `memcpy`'s cost is its size argument and
855
+ an unknown callee's is its body, neither of which the assembly shows;
856
+ both stay counted and land in the unweighted total. `extends = "OTHER"`
857
+ copies another profile's weights before this table's own apply, one
858
+ level deep - a chain is an error.
859
+
860
+ Weights resolve in this order. An instruction costs its class weight,
861
+ or counts as unweighted. A call site costs its callee's own weight
862
+ where the profile names that symbol, else its tier's weight, else it
863
+ counts as unweighted. The score is therefore always printed with the
864
+ number of instructions and call sites it left out. It is an integer
865
+ while every weight is; one fractional weight anywhere rounds the total
866
+ to a decimal.
867
+
868
+ Which profile a row uses: `--costs NAME` sets it for every matrix row
869
+ and is the only route for a `--cc` row, which has no target to read one
870
+ from; otherwise each target brings the one its `costs = "NAME"` names,
871
+ so a two-target matrix scores each target under its own. A name that
872
+ does not resolve fails before the first compile. `--list-targets`
873
+ prints the profiles a config defines, with their `measured_on`.
874
+
875
+ What a score claims, and what it does not:
876
+
877
+ - **A weight is a static issue cost.** Dual issue, pipeline stalls,
878
+ cache and flash-fetch latency, and the operand-dependence of helpers
879
+ like `__divdf3` are not in it. Two scores that differ by a few
880
+ percent say nothing.
881
+ - **Scores compare within one target.** Cycle costs differ per core,
882
+ per memory placement, and per libgcc build, so a score under
883
+ `esp32s3-iram` and a score under some other profile are two different
884
+ units. That is why the provenance line prints under every scored
885
+ table.
886
+ - **Depth is reported, not multiplied.** A span at depth 2 scores like
887
+ one at depth 0; the trip count is not in the assembly. `--span-stats`
888
+ gives the depth and the per-span mix, and the weighing is yours.
889
+
890
+ No measured profile ships with the tool. `asmdiff --example-config`
891
+ carries the template above as comments, with the fields a reader needs
892
+ in order to judge how approximate the numbers are.
518
893
 
519
894
  ### Bundled ESP profiles
520
895
 
@@ -541,7 +916,10 @@ flags = ["-O2", "-march=rv32imac_zicsr_zifencei", "-mabi=ilp32"]
541
916
  A profile is nothing more than a curated group of targets — the `esp-*`
542
917
  glob finds the toolchains `idf_tools.py install` left in `~/.espressif`
543
918
  (the newest, by the version-sort rule above, when several are
544
- installed), and setting `default` to the group runs it as one matrix:
919
+ installed). The example config defines each profile as a `[groups]`
920
+ entry, so `-t riscv32-esp` or `-t xtensa-esp` runs it as one matrix and
921
+ `-t esp` runs every ESP chip; set `default` to a group's list to make it
922
+ the no-argument matrix:
545
923
 
546
924
  ```toml
547
925
  default = ["esp32c3", "esp32c6", "esp32h2", "esp32p4"]
@@ -635,7 +1013,7 @@ Details that make it robust:
635
1013
  - **Absent source is an error**, not a silent empty flag set — otherwise
636
1014
  you'd just hit the missing-header failure this feature exists to prevent.
637
1015
  The message flags a same-basename entry recorded under a different path.
638
- - **`compile_commands` expands `~` and `$VARS`.** The `==` header always
1016
+ - **`compile_commands` expands `~` and `$VARS`.** The legend line always
639
1017
  prints the resolved compiler command; run with `-- -v` if you want to see
640
1018
  every include path the compiler actually received.
641
1019
  - **`--db-includes` restricts the borrow to header-search paths**,
@@ -702,10 +1080,25 @@ Both walk up from the current directory, checking each level for
702
1080
  CMake and idf.py leave it) — first hit wins. The walk stops at the
703
1081
  repository root (the first directory with a `.git`), so running from any
704
1082
  depth of component directory finds the project database, but an unrelated
705
- one further up the filesystem is never picked up. Finding nothing is an
706
- error: discovery is opt-in, so if you asked for it, silence would be a
707
- lie. It is never on by default — a target without `compile_commands` and
708
- no `--compile-commands` flag borrows nothing.
1083
+ one further up the filesystem is never picked up. It is never on by
1084
+ default — a target without `compile_commands` and no
1085
+ `--compile-commands` flag borrows nothing.
1086
+
1087
+ The two opt-ins differ in what happens when the walk finds nothing:
1088
+
1089
+ - **A bare `--compile-commands` is an error.** You asked for a database
1090
+ in this run and there is none; compiling anyway would silently drop
1091
+ the flags you came for.
1092
+ - **`compile_commands = true` in a target is a note.** The config
1093
+ describes where that target is usually compiled, not what this run
1094
+ is. A harness file compiled from a directory with no `build/` under
1095
+ it gets `target [esp32s3]: no compile_commands.json found near the
1096
+ current directory; compiling without borrowed flags` on stderr and
1097
+ compiles with the target's own `cc`/`flags`, which is what a
1098
+ standalone harness wants.
1099
+
1100
+ `compile_commands = "PATH"` naming a database that is not there stays an
1101
+ error under both: a path that does not resolve is a typo, not a context.
709
1102
 
710
1103
  The precedence is what you'd hope: a target that names its own
711
1104
  `compile_commands` path always keeps it; `--compile-commands` (with or
@@ -750,25 +1143,23 @@ cross-only defines or `-specs` a host gcc would choke on.
750
1143
 
751
1144
  With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
752
1145
  auto-pair, the tool prints what it parsed instead of erroring: every
753
- function's counts plus a file total. With two files, one block per file:
1146
+ function's counts plus a file total. With two files, a leading `file`
1147
+ column tells the two apart inside the one table:
754
1148
 
755
1149
  ```
756
1150
  $ asmdiff old/delay.c new/delay.c
757
1151
 
758
- == xtensa-esp32s3-elf-gcc -O2 ... ==
759
-
760
- -- old/delay.c --
761
-
762
- function insns loop spans calls
763
- stereo_reverb 437 .L108:327 -
764
- ...
765
- TOTAL (13 functions) 956 - malloc_caps, free, ...
766
-
767
- -- new/delay.c --
768
- ...
769
- TOTAL (13 functions) 1028 - malloc_caps, free, ...
1152
+ file function insns loop spans calls
1153
+ old/delay.c stereo_reverb 437 .L108:327 -
1154
+ old/delay.c ...
1155
+ old/delay.c TOTAL (13 functions) 956 - malloc_caps, free, ...
1156
+ new/delay.c ...
1157
+ new/delay.c TOTAL (13 functions) 1028 - malloc_caps, free, ...
770
1158
  ```
771
1159
 
1160
+ Over a matrix of several targets a `target` column leads the `file` one,
1161
+ so the whole run is still one table.
1162
+
772
1163
  The TOTAL row is a coarse sanity check — did this refactor move the file's
773
1164
  weight, did a call appear that shouldn't have? It sums parsed function
774
1165
  bodies only (no literal pools, data, or alignment), so it is not a size
@@ -777,13 +1168,15 @@ measurement, and per-function rows are where the real information is.
777
1168
  Only labels the assembler types as functions are listed — global data
778
1169
  (string constants, state structs, lookup tables) gets column-0 labels too
779
1170
  but is not code. A `calls` list longer than 8 symbols is truncated to
780
- `..., Total Calls:N`; real firmware dispatch functions call dozens of
781
- distinct symbols and would otherwise make rows thousands of characters
782
- wide. When stdout is a terminal, rows are additionally trimmed to the
783
- terminal width: callees are dropped from the end of the `calls` column
784
- (never the first one) behind the same `Total Calls:N` summary, so every
785
- row stays on one line. Piped or redirected output skips the width trim
786
- and keeps the full capped list, so it stays stable and greppable.
1171
+ `..., ... (N total)` (the leading `...` marks the elision, so the
1172
+ marker never reads as one more callee); real firmware dispatch
1173
+ functions call dozens of distinct symbols and would otherwise make
1174
+ rows thousands of characters wide. Rows are additionally trimmed to the
1175
+ column budget - the terminal, else `$COLUMNS`, else 120: callees are
1176
+ dropped from the end of the `calls` column (never the first one) behind
1177
+ the same `... (N total)` marker, so every row stays on one line.
1178
+ `--width 0` turns the trim off and keeps the full capped list, which is
1179
+ the shape to pipe into a grep.
787
1180
 
788
1181
  ## Comparing the same function across two builds (`--across`)
789
1182
 
@@ -815,6 +1208,11 @@ asmdiff src/filters.c --across dsps_biquad_f32_ansi \
815
1208
  cc#1: gcc -Os
816
1209
  cc#2: gcc -O3
817
1210
 
1211
+ function role insns loop spans calls
1212
+ dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
1213
+ dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
1214
+ delta +30 32 -> 54 -SMULR6
1215
+
818
1216
  == cc#1 vs cc#2 ==
819
1217
 
820
1218
  dsps_biquad_f32_ansi [cc#1] | dsps_biquad_f32_ansi [cc#2]
@@ -843,10 +1241,6 @@ addl %eax, %edx | sarl $11, %r12d
843
1241
  movl %r14d, %r11d | sarl $11, %ebp
844
1242
  call SMULR6 | leal 1024(%rax), %edx
845
1243
  [... 60 rows omitted ...]
846
-
847
- function role insns loop spans calls
848
- dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
849
- dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
850
1244
  ```
851
1245
 
852
1246
  (The listing is abridged here; the tool prints all 92 rows. The columns
@@ -855,8 +1249,8 @@ the listing shows why — `SMULR6` inlined into the loop body, vector setup
855
1249
  around it. Whether that trade is good is your call.)
856
1250
 
857
1251
  The output prints a legend mapping `cc#N` tags to the full compiler
858
- invocations, then one section per baseline/candidate pairing. Runnable
859
- against the bundled example file:
1252
+ invocations, then the table, then one listing section per
1253
+ baseline/candidate pairing. Runnable against the bundled example file:
860
1254
 
861
1255
  ```
862
1256
  $ asmdiff asmdiff_example.c --across new_rt --cc 'gcc -O0' --cc 'gcc -O3'
@@ -864,6 +1258,11 @@ $ asmdiff asmdiff_example.c --across new_rt --cc 'gcc -O0' --cc 'gcc -O3'
864
1258
  cc#1: gcc -O0
865
1259
  cc#2: gcc -O3
866
1260
 
1261
+ function role insns loop spans calls
1262
+ new_rt [cc#1] baseline 13 - ldexpf
1263
+ new_rt [cc#2] candidate 2 - ldexpf
1264
+ delta -11 - -
1265
+
867
1266
  == cc#1 vs cc#2 ==
868
1267
 
869
1268
  new_rt [cc#1] | new_rt [cc#2]
@@ -881,15 +1280,12 @@ movd %eax, %xmm0 |
881
1280
  call ldexpf@PLT |
882
1281
  leave |
883
1282
  ret |
884
-
885
- function role insns loop spans calls
886
- new_rt [cc#1] baseline 13 - ldexpf
887
- new_rt [cc#2] candidate 2 - ldexpf
888
1283
  ```
889
1284
 
890
1285
  **Two files** — before/after versions of a source file (e.g. from a git
891
- worktree, a branch checkout, or a patched copy). Each compiler in the matrix
892
- gets its own section:
1286
+ worktree, a branch checkout, or a patched copy). Every compiler in the
1287
+ matrix lands in the one table under its own `target` label, and each
1288
+ gets its own listing section:
893
1289
 
894
1290
  ```bash
895
1291
  git worktree add ../baseline main
@@ -941,8 +1337,10 @@ The summary makes the trade-off immediate:
941
1337
  function role insns calls
942
1338
  exp2_lut [amy-baseline/log2_exp2.c] baseline 65 exp2f
943
1339
  exp2_lut [amy/log2_exp2.c] candidate 59 ldexpf
1340
+ delta -6 +ldexpf -exp2f
944
1341
  log2_lut [amy-baseline/log2_exp2.c] baseline 58 -
945
1342
  log2_lut [amy/log2_exp2.c] candidate 64 ldexpf
1343
+ delta +6 +ldexpf
946
1344
  ```
947
1345
 
948
1346
  The runtime site improves (a leaner libcall replaces `exp2f` + multiply),
@@ -963,6 +1361,8 @@ probed the same way.
963
1361
  x86 (`call`, `jmp` tail calls), ARM (`bl`, `blx`), RISC-V (`call`,
964
1362
  `tail`, `jal`), and Xtensa (`call0/4/8/12`, `callx*`, `j`). Local-label
965
1363
  branches and register-indirect x86 jumps are not counted as calls.
1364
+ Register-indirect call mnemonics (`callx8 a8`, single-operand `jalr`,
1365
+ `blx r3`) are reported as `indirect(<reg>)`.
966
1366
  4. Loop spans come from label references alone — no mnemonic tables, no
967
1367
  control-flow analysis. The next section walks through it.
968
1368
 
@@ -996,6 +1396,16 @@ into that label's single span. Nested labels report separately — the
996
1396
  outer span simply contains the inner one. Forward references (loop exits
997
1397
  like `jle .L24`) are ignored.
998
1398
 
1399
+ Containment is what the `depth` column of `--span-stats` reports: a
1400
+ span's depth is the number of other spans of the same function that
1401
+ start at or before it and end at or after it. An outermost loop is
1402
+ depth 0, a loop nested in it 1, and a span at depth 1 runs its body
1403
+ once per iteration of the span at depth 0. Two labels covering exactly
1404
+ the same lines (one loop body reached by two edges) contain each other
1405
+ under no reading, so both keep the depth of whatever encloses them.
1406
+ Depth is a nesting fact and nothing more - the trip count is not in the
1407
+ assembly, so nothing in the tool multiplies a count by it.
1408
+
999
1409
  The one arch-specific case is Xtensa zero-overhead loops, where the
1000
1410
  hardware — not a branch — repeats the body, and the `loop` instruction
1001
1411
  names its *end* label, forward:
@@ -1026,6 +1436,11 @@ misreadings to avoid are few and predictable:
1026
1436
  cost more than the rest of the function. Treat the count as a *size*
1027
1437
  and *structure* fact - a call appearing, a loop body growing, a
1028
1438
  softfloat sequence materializing - and measure time on the target.
1439
+ `--cost` splits the same body into instruction classes and libcall
1440
+ tiers, so that one `__udivdi3` line reads as an `int-div` call
1441
+ instead of as one more instruction; a `[costs.NAME]` profile prices
1442
+ those counts in measured cycles, which is still a static issue cost
1443
+ and still not a measurement of your program.
1029
1444
 
1030
1445
  - **The cost of a call is in the callee.** The listing shows only the
1031
1446
  call site. The `-O3` version of `new_rt` above is two instructions,
@@ -1034,7 +1449,9 @@ misreadings to avoid are few and predictable:
1034
1449
 
1035
1450
  - **Weigh the span, not the function.** One instruction added inside a
1036
1451
  loop that runs per sample outweighs twenty added to setup code. The
1037
- whole-function count charges both the same.
1452
+ whole-function count charges both the same; `--span-stats` gives the
1453
+ per-span mix and its nesting depth, and `--cost` counts the call
1454
+ sites a span holds apart from the rest.
1038
1455
 
1039
1456
  - **Bigger is often faster.** Unrolling and vectorization raise every
1040
1457
  count on purpose - the `-Os` vs `-O3` biquad above is bigger by every
@@ -1085,15 +1502,19 @@ standard library, and contains no project-specific constants. To port:
1085
1502
  supported — on a Mac, compare inside a Linux container or with a
1086
1503
  cross-toolchain.
1087
1504
  - Call detection is a mnemonic heuristic. Register-indirect calls through a
1088
- loaded address (other than x86 `jmp *reg`) can be reported as a call to
1089
- the register's name (e.g. Xtensa `callx8 a10`), which errs toward
1090
- visibility rather than silence. ELF input narrows this: `-mlongcalls`
1091
- sequences are resolved to their real callee when objdump's literal
1092
- annotation names one; genuine function-pointer dispatch still reads
1093
- as a register.
1094
- - Columns truncate long instruction lines to keep pairs aligned; when a
1095
- line matters, widen it via the `width` parameter of `side_by_side()` or
1096
- read the raw `-S` output by hand.
1505
+ loaded address (other than x86 `jmp *reg`) are reported as
1506
+ `indirect(<reg>)` (e.g. Xtensa `callx8 a10` -> `indirect(a10)`), which
1507
+ errs toward visibility rather than silence. ELF input narrows this:
1508
+ `-mlongcalls` sequences are resolved to their real callee when objdump's
1509
+ literal annotation names one; genuine function-pointer dispatch still
1510
+ reads as indirect.
1511
+ - Side-by-side columns truncate long instruction lines to keep pairs
1512
+ aligned, and drop a trailing assembler comment (clang's `# TAILCALL`,
1513
+ `# 8-byte Reload`) before truncating, so the operands survive at half
1514
+ a terminal's width. An ARM `#4` immediate has no space after the hash
1515
+ and is untouched. Each side gets half the column budget, so `--width`
1516
+ widens them; `-l list`, the single-column listings and `--json` keep
1517
+ the line as extracted, comment included.
1097
1518
  - Loop spans are layout facts, not loop analysis. Label numbers are
1098
1519
  compiler-assigned, so a baseline's `.L27` and a candidate's `.L26` may
1099
1520
  or may not be "the same" loop — match them through the listing, not by