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.
- asmdiff-0.3.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
- asmdiff-0.3.0/2026-07-07-table-layout-handoff.md +90 -0
- asmdiff-0.3.0/2026-07-10-reverb-session-feedback-handoff.md +102 -0
- asmdiff-0.1.7/README.md → asmdiff-0.3.0/PKG-INFO +162 -10
- asmdiff-0.1.7/PKG-INFO → asmdiff-0.3.0/README.md +144 -28
- asmdiff-0.3.0/amy-hotspot-analysis.md +214 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/asmdiff.example.toml +6 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/asmdiff.py +431 -56
- asmdiff-0.3.0/demo/delay_macro.c +414 -0
- asmdiff-0.3.0/demo/delay_orig.c +355 -0
- asmdiff-0.3.0/demo/delay_restrict.c +361 -0
- asmdiff-0.3.0/demo/delay_struct.c +415 -0
- asmdiff-0.3.0/demo/esp32-matrix.gif +0 -0
- asmdiff-0.3.0/demo/esp32-matrix.png +0 -0
- asmdiff-0.3.0/demo/quickstart.gif +0 -0
- asmdiff-0.3.0/demo/quickstart.png +0 -0
- asmdiff-0.3.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +277 -0
- asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +147 -0
- asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +16 -0
- asmdiff-0.3.0/gif-plan.md +248 -0
- asmdiff-0.3.0/hackaday-tip-draft.md +101 -0
- asmdiff-0.3.0/hn-show-draft.md +57 -0
- asmdiff-0.3.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +3 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/pyproject.toml +5 -2
- {asmdiff-0.1.7 → asmdiff-0.3.0}/test_asmdiff.py +530 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/.claude/settings.local.json +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/.gitignore +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/2026-07-06-inspect-mode.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/2026-07-07-elf-mode-handoff.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/CLAUDE.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/FIELD-TEST-REPORT.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/LICENSE +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/Screenshot 2026-07-07 225145.jpg +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/asmdiff_example.c +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/.gitignore +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.1.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.2.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.3.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/dist_1/asmdiff-0.1.4.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.3.0}/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -0
|
File without changes
|
|
@@ -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
|
|
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
|
+
[](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
|
-
>
|
|
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:
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
362
|
-
(
|
|
363
|
-
|
|
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
|
+
[](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
|