asmdiff 0.1.7__tar.gz → 0.3.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 (44) hide show
  1. asmdiff-0.3.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
  2. asmdiff-0.3.0/2026-07-07-table-layout-handoff.md +90 -0
  3. asmdiff-0.3.0/2026-07-10-reverb-session-feedback-handoff.md +102 -0
  4. asmdiff-0.1.7/README.md → asmdiff-0.3.0/PKG-INFO +162 -10
  5. asmdiff-0.1.7/PKG-INFO → asmdiff-0.3.0/README.md +144 -28
  6. asmdiff-0.3.0/amy-hotspot-analysis.md +214 -0
  7. {asmdiff-0.1.7 → asmdiff-0.3.0}/asmdiff.example.toml +6 -0
  8. {asmdiff-0.1.7 → asmdiff-0.3.0}/asmdiff.py +431 -56
  9. asmdiff-0.3.0/demo/delay_macro.c +414 -0
  10. asmdiff-0.3.0/demo/delay_orig.c +355 -0
  11. asmdiff-0.3.0/demo/delay_restrict.c +361 -0
  12. asmdiff-0.3.0/demo/delay_struct.c +415 -0
  13. asmdiff-0.3.0/demo/esp32-matrix.gif +0 -0
  14. asmdiff-0.3.0/demo/esp32-matrix.png +0 -0
  15. asmdiff-0.3.0/demo/quickstart.gif +0 -0
  16. asmdiff-0.3.0/demo/quickstart.png +0 -0
  17. asmdiff-0.3.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +277 -0
  18. asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +147 -0
  19. asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +16 -0
  20. asmdiff-0.3.0/gif-plan.md +248 -0
  21. asmdiff-0.3.0/hackaday-tip-draft.md +101 -0
  22. asmdiff-0.3.0/hn-show-draft.md +57 -0
  23. asmdiff-0.3.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +3 -0
  24. {asmdiff-0.1.7 → asmdiff-0.3.0}/pyproject.toml +5 -2
  25. {asmdiff-0.1.7 → asmdiff-0.3.0}/test_asmdiff.py +530 -0
  26. {asmdiff-0.1.7 → asmdiff-0.3.0}/.claude/settings.local.json +0 -0
  27. {asmdiff-0.1.7 → asmdiff-0.3.0}/.gitignore +0 -0
  28. {asmdiff-0.1.7 → asmdiff-0.3.0}/2026-07-06-inspect-mode.md +0 -0
  29. {asmdiff-0.1.7 → asmdiff-0.3.0}/2026-07-07-elf-mode-handoff.md +0 -0
  30. {asmdiff-0.1.7 → asmdiff-0.3.0}/CLAUDE.md +0 -0
  31. {asmdiff-0.1.7 → asmdiff-0.3.0}/FIELD-TEST-REPORT.md +0 -0
  32. {asmdiff-0.1.7 → asmdiff-0.3.0}/LICENSE +0 -0
  33. {asmdiff-0.1.7 → asmdiff-0.3.0}/Screenshot 2026-07-07 225145.jpg +0 -0
  34. {asmdiff-0.1.7 → asmdiff-0.3.0}/asmdiff_example.c +0 -0
  35. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/.gitignore +0 -0
  36. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
  37. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.1.tar.gz +0 -0
  38. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
  39. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.2.tar.gz +0 -0
  40. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
  41. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.3.tar.gz +0 -0
  42. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
  43. {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.4.tar.gz +0 -0
  44. {asmdiff-0.1.7 → asmdiff-0.3.0}/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -0
@@ -0,0 +1,90 @@
1
+ # asmdiff summary-table legibility - checkpoint 2026-07-07
2
+
3
+ Handoff for a fresh session in THIS repo (`~/esp-idf/tools/asmdiff`).
4
+ Feature is COMPLETE and verified, NOTHING COMMITTED. Origin: ELF-mode
5
+ output (`asmdiff build/S3-Amysynth.elf --filter 'core_'`) was unreadable -
6
+ every row hard-wrapped in the terminal (screenshot in repo root,
7
+ `Screenshot 2026-07-07 225145.jpg`).
8
+
9
+ ## Root cause
10
+
11
+ `render_table` pads every column to its widest cell. The `calls` column
12
+ sat in the middle; with real firmware call lists (~150 chars at the
13
+ 8-callee cap) plus 50-char LTO symbol names, `loop spans` started past
14
+ column ~210 and every row wrapped: dangling `-` lines, split headers,
15
+ call lists colliding with the next function name.
16
+
17
+ ## What changed (asmdiff.py)
18
+
19
+ 1. **Column order**: all three builders now emit
20
+ `function / [role] / insns / loop spans / calls` - calls (the one
21
+ unbounded column) moved last, ragged right. Builders:
22
+ `summary_table`, `file_summary_table` (TOTAL row spans cell stays
23
+ `-`), `inspect_table`. Each gained a `max_width=None` passthrough
24
+ param.
25
+ 2. **Width-aware trim**: `render_table(rows, max_width=None)` - when
26
+ set, fixed columns keep full width, the last column's over-budget
27
+ cells are trimmed by `_fit_calls`: whole callees dropped from the
28
+ end, closed with `Total Calls:N` (N = full callee count, preserved
29
+ from an existing summary). The FIRST callee is never dropped - a
30
+ single huge symbol overflows/wraps instead of vanishing
31
+ (`sequencer_core_set_melodic_envelope2` is the real-world case).
32
+ Cells that fit, `-`, and headers are untouched. `TOTAL_CALLS_RE`
33
+ guards summary parsing.
34
+ 3. **`format_calls`** cap suffix changed from `+N more` to
35
+ `Total Calls:N` (user-requested format; one consistent suffix for
36
+ both the 8-callee cap and the width trim).
37
+ 4. **`table_width()`**: `shutil.get_terminal_size().columns` when
38
+ `sys.stdout.isatty()`, else `None` - piped/redirected output skips
39
+ the trim so it stays deterministic and greppable. Passed at all six
40
+ print sites (both summary_table sites, both file_summary_table
41
+ sites, both inspect_table sites).
42
+
43
+ All new code sits together around `render_table` (~line 1053 region).
44
+
45
+ ## Tests (test_asmdiff.py)
46
+
47
+ - New `TestTableMaxWidth` (9 tests): trim-to-fit, fitting cell left
48
+ alone, merges existing `Total Calls:N` (does not recount), single
49
+ callee never dropped, dash untouched, `max_width=None` unchanged,
50
+ builder passthrough, `table_width()` tty/piped (via `mock.patch` on
51
+ `asmdiff.sys.stdout.isatty` / `asmdiff.shutil.get_terminal_size`).
52
+ - Updated for new column order / suffix: `TestFormatCalls`
53
+ `test_long_list_capped`, `test_summary_table`,
54
+ `test_summary_table_loop_spans_column`,
55
+ `test_file_summary_totals_and_call_union`, both
56
+ `TestInspectRendering` inspect_table tests.
57
+ - Suite: 191 tests, sole failure the KNOWN env-dependent
58
+ `TestShortAliases.test_p_is_pair` (global ~/.config/asmdiff.toml with
59
+ target esp32s3 short-circuits it; fails at HEAD too).
60
+
61
+ ## Docs
62
+
63
+ README: all 8 example tables reordered; the truncation paragraph (ELF
64
+ section, "Only labels the assembler types...") now documents
65
+ `Total Calls:N`, the tty-only width trim, and the piped-output
66
+ exemption; intro prose order updated. Module docstring summary line
67
+ reordered to match. No flags or config keys changed, so
68
+ `asmdiff.example.toml` / `EXAMPLE_CONFIG` are untouched (the
69
+ byte-match unit test still applies to any future config edit).
70
+
71
+ ## Verification done
72
+
73
+ - Full suite as above.
74
+ - Real ELF (`~/esp-idf/S3-Amysynth/build/S3-Amysynth.elf`, config-derived
75
+ xtensa objdump), `--filter 'core_'`:
76
+ - piped: full capped lists, new column order, aligned.
77
+ - fake tty via `script -qec "stty cols 190 ..."`: all rows one line
78
+ except the designed first-callee-floor overflow (4 chars over).
79
+ - 110-col tty: fixed columns alone can exceed width when a loopy
80
+ function widens `loop spans`; calls floor at one callee - accepted
81
+ graceful degradation, only `calls` ever trims (scope decision).
82
+
83
+ ## Next steps
84
+
85
+ 1. Commit (user has not asked yet; suggested scope: asmdiff.py,
86
+ test_asmdiff.py, README.md in one commit - pure output-format
87
+ change, no behavior change to analysis).
88
+ 2. Optional follow-up the user explicitly deferred: making the
89
+ `loop spans` cells themselves friendlier (e.g. "2 ZOL + 3 br"
90
+ summary) - scoped OUT of this change ("layout only").
@@ -0,0 +1,102 @@
1
+ # Field feedback: S3-Amysynth reverb codegen session — handoff 2026-07-10
2
+
3
+ Feedback from a real investigation, not a scripted test matrix. Workload:
4
+ `stereo_reverb` in AMY's `delay.c` (ESP32-S3 project at `~/esp-idf/S3-Amysynth`,
5
+ xtensa-esp-elf-gcc 15.2.0, ship `-O2` flags via `-db`), comparing four source
6
+ variants: pre-optimization original (extracted from git history), the shipped
7
+ token-paste locals-cache, a struct/`static inline` respelling, and a
8
+ `restrict`-annotated original. Modes exercised: two-file `-a FUNC` compare,
9
+ `--target` matrix, `-db` + `--flags-like`, host-compiler contrast runs.
10
+
11
+ ## State
12
+
13
+ The tool answered every question the session asked, correctly. All findings
14
+ below are UX/ergonomics, not correctness: in each case asmdiff computed the
15
+ right thing and then made the caller dig for it. Ranked by time actually lost.
16
+
17
+ ## Findings
18
+
19
+ ### 1. Summary tables print last; no summaries-only mode
20
+ Two-file compare with the default target set produced a 102 KB dump (three full
21
+ side-by-side listings, esp32/s2/s3) with the `function/role/insns/loop
22
+ spans/calls` table at the *end* of each target section. Every subsequent
23
+ invocation was pre-piped through `grep -A4 "role.*insns"` to skip the listings.
24
+
25
+ Repro: `asmdiff delay_macro.c delay_struct.c -a stereo_reverb -db --flags-like
26
+ components/amy/src/delay.c` (no `--target`).
27
+
28
+ Proposal: `--summary-only` (or print summaries before listings). For
29
+ agent/scripted callers the table is the decision input; the listing is only
30
+ pulled when a delta needs explaining. A `--json` summary emitter would serve
31
+ the same callers.
32
+
33
+ ### 2. Doc drift: `host` target promised, absent from shipped config
34
+ The skill/README text says ESP profiles "+ host" are pre-configured;
35
+ `~/.config/asmdiff.toml` actually ships `gcc` and `clang`. `--target host`
36
+ errors out. The error's target list is what rescued the session — good — but
37
+ the drift costs one failed call per session that trusts the docs.
38
+
39
+ Fix: add a `[host]` alias target to the example/default toml, or correct the
40
+ README and the `~/.claude/skills/asmdiff` skill text (both say "host").
41
+
42
+ ### 3. `-db` on foreign-arch targets drops project include paths with the flags
43
+ For host targets, `-db` correctly refuses to borrow xtensa flags ("compiling
44
+ without its include/define flags") — but that also drops `-I`, so the compile
45
+ dies on `fatal error: amy.h: No such file or directory`. Workaround was manual:
46
+ `-- -I~/esp-idf/S3-Amysynth/components/amy/src`.
47
+
48
+ Proposal: the project's own `-I` paths are target-portable even when its `-D`s
49
+ are not. For foreign-arch targets, borrow `-I` only (skip `-D`), or offer
50
+ `--db-includes`. Keep the current loud note either way — the refusal itself is
51
+ right (see FIELD-TEST-REPORT Trap 1 for why silently borrowing would be worse).
52
+
53
+ ### 4. No per-span instruction mix (`--span-stats`)
54
+ The decisive number of the session — loop body 96 loads / 31 stores vs 85 / 26,
55
+ multiplies identical at 14 — required two rounds of hand-written awk over the
56
+ listing, and the first attempt silently counted past the loop end into the
57
+ epilogue. The tool's own doctrine is "weigh the span, not the function"; it
58
+ should be able to weigh it. The 2026-07-07 ELF round hit the same wall (ad-hoc
59
+ awk over objdump for loop tabulation), so this is a recurring gap.
60
+
61
+ Proposal: `--span-stats` — per-span opcode histogram (loads / stores / mul /
62
+ branch / other), per function, alongside the existing `span:count` column.
63
+
64
+ ### 5. Side-by-side pair listings don't collapse identical runs
65
+ Two ~500-insn functions differing by ~5 instructions print ~1000 listing lines
66
+ of which ~990 are identical pairs. Context-diff-style collapsing (keep N lines
67
+ around changed hunks, elide the rest with a count) would turn the 102 KB dump
68
+ from finding 1 into a few readable hunks. Interacts with finding 1: summary
69
+ first, collapsed hunks second, full listing on request.
70
+
71
+ ### 6. Minor: missing-toolchain error could name the compiler
72
+ `--target rp2350` with no ARM toolchain installed → "error: no usable compiler
73
+ in the matrix". Accurate, but naming the miss ("arm-none-eabi-gcc: not found
74
+ on PATH") makes the fix obvious.
75
+
76
+ ## Verified working this session (regression notes)
77
+
78
+ - `--flags-like <db-file>` on out-of-tree copies: exact borrowed-flag parity
79
+ both sides — this closes FIELD-TEST-REPORT Trap 2 ("before/after on a project
80
+ source has no supported path") and its Trap 1 hazard for the copy workflow.
81
+ - Two-file `-a FUNC` compare against a `git show <rev>:src/file.c` extraction:
82
+ frictionless; this is the natural "what did that upstream commit buy"
83
+ workflow.
84
+ - Default target resolution + multi-target matrix from config: zero toolchain
85
+ path digging; glob `cc` patterns resolved and announced.
86
+ - `span:count` + `calls` columns answered ZOL and libcall questions at a
87
+ glance.
88
+
89
+ Session results usable as a future test corpus (esp32s3, ship -O2, AMY at
90
+ v1.2.31): `stereo_reverb` original 437 insns / span 327; +`restrict` best
91
+ effort 473 / 306; shipped locals-cache 509 / 290; struct respelling 504 / 289.
92
+ Host: gcc -O3 467 = 467, clang -O3 443 = 443 (macro vs struct byte-count
93
+ identical). Variant sources preserved in the session job dir
94
+ (`~/.claude/jobs/b3de0cac/tmp/delay_{orig,restrict,macro,struct}.c`) — copy
95
+ them into `demo/` or the test suite if wanted before that dir is cleaned.
96
+
97
+ ## Next steps
98
+
99
+ 1. Finding 1 (`--summary-only`) — smallest change, largest per-call saving.
100
+ 2. Finding 4 (`--span-stats`) — recurring need across two sessions.
101
+ 3. Finding 5 (collapse identical hunks) — pairs with 1.
102
+ 4. Findings 2, 3, 6 — config/doc/message fixes, each ~minutes.
@@ -1,7 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: asmdiff
3
+ Version: 0.3.0
4
+ Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
5
+ Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
6
+ Project-URL: Repository, https://github.com/rt-rtos/asmdiff
7
+ Author: Rasmus Tikkanen
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,objdump,riscv,static-analysis,xtensa
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Compilers
15
+ Classifier: Topic :: Software Development :: Debuggers
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+
1
19
  # [asmdiff](https://pypi.org/project/asmdiff/)
2
- ## per-function assembly comparison for paired C implementations
20
+ ## per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
21
+
22
+ > asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for
23
+ investigating compiler code generation rather than benchmarking runtime performance.
24
+
25
+ <details>
26
+
27
+ <summary>Quickstart Example</summary>
28
+
29
+ [![asmdiff comparing gcc and clang assembly for paired C functions, with a fold-vs-libcall summary](demo/quickstart.gif)](demo/quickstart.gif)
30
+
31
+ *Each function appears twice - `old_*` and `new_*`, the same routine
32
+ before and after a rewrite - compiled across a gcc/clang matrix and diffed
33
+ side by side. The summary's `insns` and `calls` columns are the payoff:
34
+ `old_rt`'s 9-instruction `exp2f` call collapses to `new_rt`'s 2-instruction
35
+ `ldexpf`. (Click to enlarge.)*
36
+
37
+ ---
3
38
 
4
- > asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for investigating compiler code generation rather than benchmarking runtime performance.
39
+ </details>
5
40
 
6
41
  ### Try it yourself:
7
42
 
@@ -255,9 +290,10 @@ everywhere else; two things are ELF-specific:
255
290
  evidence is missing or the register is overwritten in between -
256
291
  a genuine function-pointer dispatch still reads as indirect.
257
292
 
258
- What this looks like on real firmware: the same binary's sequencer
259
- core (project code, not a synth library) surfaces three findings in
260
- one row scan.
293
+ What this looks like on real firmware: this binary's sequencer path -
294
+ here in the vendored AMY synth engine (`components/amy/src/sequencer.c`,
295
+ with the divide sourced from `amy_sysclock()` in `api.c`) - surfaces
296
+ three findings in one row scan.
261
297
 
262
298
  ```
263
299
  $ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback)'
@@ -271,7 +307,12 @@ sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaph
271
307
  A timer callback pays a software float divide *and* a 64-bit
272
308
  `__udivdi3` inside its 41-instruction loop - the
273
309
  [silent software divide](#example-catching-a-silent-software-divide)
274
- pattern, caught in shipped firmware instead of a harness.
310
+ pattern, caught in shipped firmware instead of a harness. asmdiff was
311
+ built to hunt exactly this in AMY, and this callback was the payoff: the
312
+ pairing was confirmed as both a codegen cost and an upstream correctness
313
+ bug - `amy_sysclock()`'s `uint32_t` sample count wraps far sooner than
314
+ its comment claims and loses precision on long uptimes - and is fixed
315
+ locally with a PR pending to shorepine/amy.
275
316
  `sequencer_recompute`'s `__extendsfdf2 -> __divdf3 -> __muldf3` chain
276
317
  is the double-promotion smell (typically an unsuffixed `60.0`-style
277
318
  literal) on a chip whose FPU is single-precision only. And the
@@ -317,9 +358,11 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
317
358
  ```
318
359
  asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
319
360
  [--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
320
- [--compile-commands [PATH]] [--flags-like PATH]
361
+ [--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
362
+ [--summary-only] [--collapse] [--span-stats]
321
363
  [--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
322
364
  asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
365
+ [--summary-only] [--span-stats]
323
366
  ```
324
367
 
325
368
  | Option | Meaning |
@@ -335,6 +378,12 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
335
378
  | `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
336
379
  | `-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). |
337
380
  | `--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
+ | `--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
+ | `-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. |
386
+ | `--version` | Print the version and exit. |
338
387
  | `-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. |
339
388
  | `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
340
389
 
@@ -358,9 +407,66 @@ asmdiff h.c -- -fno-math-errno
358
407
  ```
359
408
 
360
409
  Compilers missing from `PATH` are skipped with a warning; the run fails only
361
- if none are usable. Exit status is non-zero only for operational failures
362
- (compile error — the compiler's stderr is shown — unknown `--pair` name, no
363
- usable compiler). Differing assembly is the expected result, never an error.
410
+ if none are usable, and that error names each missed binary
411
+ (`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.
416
+
417
+ ## Shaping the output for reading vs deciding
418
+
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:
424
+
425
+ - **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
426
+ the scripted/agent view: the table is the decision input, and a listing
427
+ is pulled with a second, narrower run only when a delta needs
428
+ explaining.
429
+ - **`--collapse`** elides runs of identical line pairs in every
430
+ side-by-side listing, keeping 3 lines of context around each difference
431
+ plus a `... N identical lines ...` marker. Two ~500-instruction
432
+ 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
435
+ instruction mix, one row per span:
436
+
437
+ ```
438
+ function span insns load store mul branch other
439
+ stereo_reverb .L108 327 96 31 14 12 174
440
+ ```
441
+
442
+ This weighs the span rather than the whole function — the number that
443
+ actually decides a hot-loop comparison (is the rewrite trading loads
444
+ for stores? did the multiplies move?) without hand-counting a listing
445
+ and accidentally tallying past the loop end into the epilogue.
446
+ 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.
453
+
454
+ For scripted callers, `--json` replaces the tables entirely with one
455
+ 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:
460
+
461
+ ```bash
462
+ asmdiff old.c new.c -a stereo_reverb --json --span-stats \
463
+ | jq '.results[] | {role, insns, spans: .loop_spans}'
464
+ ```
465
+
466
+ The top level carries `asmdiff` (version) and `mode`
467
+ (`pairs`/`across`/`inspect`/`summary`/`elf`; ELF runs add the binary's
468
+ path as `elf`). Warnings and errors stay plain text on stderr, so a
469
+ failed run never emits half a document.
364
470
 
365
471
  ## Config file: named targets
366
472
 
@@ -532,9 +638,49 @@ Details that make it robust:
532
638
  - **`compile_commands` expands `~` and `$VARS`.** The `==` header always
533
639
  prints the resolved compiler command; run with `-- -v` if you want to see
534
640
  every include path the compiler actually received.
641
+ - **`--db-includes` restricts the borrow to header-search paths**,
642
+ dropping `-D`/`-U`, forced includes, and `-specs`/`--sysroot`. Use it
643
+ when the *target* doesn't match the arch the database was built for —
644
+ host gcc given an ESP-IDF database would otherwise inherit
645
+ `-specs=picolibc.specs` and cross-only defines it cannot compile
646
+ with, yet still needs the project's include paths to find its
647
+ headers. The kept paths are re-emitted as `-idirafter`, searched
648
+ *after* the compiler's own system directories: ESP-IDF's include set
649
+ contains a libc-overlay directory (`esp_libc/platform_include`) whose
650
+ `stdio.h` would shadow the host's under plain `-I`; demoted, the
651
+ project's own headers still resolve and the host's libc wins.
535
652
 
536
653
  ### Auto-discovering the database
537
654
 
655
+ <details>
656
+
657
+ <summary>Real Project Example</summary>
658
+
659
+ #### All Esp32 Xtensa toolchains in one call from the same compile_commands
660
+
661
+ Below, `-db` (short for `--compile-commands`) does this on a real ESP-IDF
662
+ synth firmware: one `synth_core` effects source, compiled across the
663
+ esp32 / esp32s2 / esp32s3 cross-toolchains in a single run, with each
664
+ chip's include/define flags auto-borrowed from `build/compile_commands.json`
665
+ so the component actually compiles. No `--target` and no path are given -
666
+ the config's `default` names the three chips and `-db` walks up to find the
667
+ database. Reading it left to right: the three `==` banners are the three
668
+ toolchains; each table is that chip's per-function `insns` / loop spans /
669
+ `calls`. The tell is in the `calls` column - the esp32-S2 (the one variant
670
+ with no hardware FPU) picks up softfloat conversion libcalls
671
+ `__floatsisf`, `__floatunsisf`, `__ltsf2` that its FPU-equipped siblings
672
+ never emit, so the same source is measurably heavier there:
673
+
674
+ [![asmdiff running a three-chip xtensa matrix (esp32/s2/s3) on one firmware source via -db](demo/esp32-matrix.gif)](demo/esp32-matrix.gif)
675
+
676
+ *One source, three real chip toolchains, build flags borrowed
677
+ automatically - the kind of comparison that otherwise needs three separate
678
+ build trees. (Click to enlarge.)*
679
+
680
+ ---
681
+
682
+ </details>
683
+
538
684
  When you run asmdiff from inside the project anyway, the path is
539
685
  redundant. Two opt-ins skip it:
540
686
 
@@ -550,6 +696,7 @@ asmdiff biquad.c --compile-commands # same, for any matrix
550
696
  asmdiff biquad.c --compile-commands path/to/compile_commands.json
551
697
  ```
552
698
 
699
+
553
700
  Both walk up from the current directory, checking each level for
554
701
  `compile_commands.json` and then `build/compile_commands.json` (where
555
702
  CMake and idf.py leave it) — first hit wins. The walk stops at the
@@ -594,6 +741,11 @@ baseline (`../baseline/src/oscillators.c`), which is the same file under a
594
741
  path the database has never heard of. The absent-source error and the
595
742
  soft-miss note both point at this flag when a same-name entry exists.
596
743
 
744
+ To contrast the same pair on a host compiler (does gcc-on-x86 make the
745
+ same choice?), add `--db-includes`: the borrow then carries only the
746
+ project's include paths, which are target-portable, and none of the
747
+ cross-only defines or `-specs` a host gcc would choke on.
748
+
597
749
  ## Whole-file summary
598
750
 
599
751
  With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to