asmdiff 0.1.5__tar.gz → 0.1.7__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.1.5 → asmdiff-0.1.7}/.claude/settings.local.json +3 -1
- asmdiff-0.1.7/2026-07-07-elf-mode-handoff.md +156 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/CLAUDE.md +7 -2
- {asmdiff-0.1.5 → asmdiff-0.1.7}/PKG-INFO +439 -128
- {asmdiff-0.1.5 → asmdiff-0.1.7}/README.md +437 -126
- asmdiff-0.1.7/Screenshot 2026-07-07 225145.jpg +0 -0
- asmdiff-0.1.7/asmdiff.example.toml +100 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/asmdiff.py +489 -36
- asmdiff-0.1.7/docs/superpowers/specs/2026-07-07-edit-config-design.md +97 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/pyproject.toml +2 -2
- {asmdiff-0.1.5 → asmdiff-0.1.7}/test_asmdiff.py +525 -8
- asmdiff-0.1.5/asmdiff.example.toml +0 -51
- {asmdiff-0.1.5 → asmdiff-0.1.7}/.gitignore +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/2026-07-06-inspect-mode.md +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/FIELD-TEST-REPORT.md +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/LICENSE +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/asmdiff_example.c +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/.gitignore +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.1.tar.gz +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.2.tar.gz +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.3.tar.gz +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
- {asmdiff-0.1.5 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.4.tar.gz +0 -0
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
"Bash(python3 test_asmdiff.py)",
|
|
5
5
|
"Bash(git -C /home/fatta/esp-idf/tools/asmdiff log --oneline -5)",
|
|
6
6
|
"Bash(git -C /home/fatta/esp-idf/tools/asmdiff diff main...HEAD --stat)",
|
|
7
|
-
"Bash(npx --no-install mmdc --version)"
|
|
7
|
+
"Bash(npx --no-install mmdc --version)",
|
|
8
|
+
"Bash(git -C /home/fatta/esp-idf/tools/asmdiff diff --stat)",
|
|
9
|
+
"Bash(git -C /home/fatta/esp-idf/tools/asmdiff diff)"
|
|
8
10
|
]
|
|
9
11
|
}
|
|
10
12
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# asmdiff ELF input mode — checkpoint 2026-07-07
|
|
2
|
+
|
|
3
|
+
Handoff for a fresh session in THIS repo (`~/esp-idf/tools/asmdiff`). Feature is
|
|
4
|
+
scoped and estimated but zero code is written. Origin: a zero-overhead-loop (ZOL)
|
|
5
|
+
audit in `~/esp-idf/S3-Amysynth` (report: `S3-Amysynth/docs/zol-audit-2026-07-07.md`)
|
|
6
|
+
where single-TU `-S` output could not show post-LTO reality — the audit had to
|
|
7
|
+
hand-roll awk over `objdump -d`. This mode makes that reusable.
|
|
8
|
+
|
|
9
|
+
## State
|
|
10
|
+
|
|
11
|
+
Design agreed with the user, sized at ~250-350 lines in `asmdiff.py` plus
|
|
12
|
+
~200-300 lines of tests (~20% repo growth), judged in line with the tool's
|
|
13
|
+
direction ("what did the compiler do" gains "what actually shipped after LTO").
|
|
14
|
+
No branch created. The awk prototypes lived in an ephemeral
|
|
15
|
+
session scratchpad — everything needed from them is restated below.
|
|
16
|
+
|
|
17
|
+
**2026-07-07 later session: item 1 (objdump parser / ZOL identifier) DONE.**
|
|
18
|
+
`extract_functions_objdump()` + `_relabel_objdump()` in asmdiff.py (after
|
|
19
|
+
`extract_functions`), TDD'd via `TestObjdumpExtract` (12 tests, fixture
|
|
20
|
+
`OBJDUMP_ASM`) in test_asmdiff.py; full suite green except the pre-existing
|
|
21
|
+
env-dependent `TestShortAliases.test_p_is_pair` failure (a global
|
|
22
|
+
~/.config/asmdiff.toml with target esp32s3 short-circuits it — fails at HEAD
|
|
23
|
+
too). Validated against the real S3-Amysynth ELF (1682 functions parsed):
|
|
24
|
+
ZOL counts match the fixtures exactly (render_lut 1, filter_process 6,
|
|
25
|
+
render_lut_cub 0, amy_fill_buffer 3; LTO-inlined trio absent). Branch-span
|
|
26
|
+
counts read HIGHER than the audit's (filter_process 4 vs 3,
|
|
27
|
+
amy_fill_buffer 11 vs 5) — explained, not a bug: the audit awk counted only
|
|
28
|
+
conditional backedges, while `loop_spans()` counts any backward label
|
|
29
|
+
reference incl. unconditional `j` latches (top-tested loops), same as it
|
|
30
|
+
does on -S output. Spans containing a conditional backedge match the audit
|
|
31
|
+
exactly (3 and 5).
|
|
32
|
+
|
|
33
|
+
**2026-07-07 third session: items 2-4 DONE — feature complete.**
|
|
34
|
+
- Longcall resolver (`_resolve_longcalls`, `L32R_VALUE`, `XTENSA_STORES`,
|
|
35
|
+
`TestLongcallResolver` 9 tests): nearest write to the call register in an
|
|
36
|
+
8-insn window decides; annotated l32r resolves, unannotated l32r or a
|
|
37
|
+
non-store overwrite (e.g. l32i fetching a function pointer from a
|
|
38
|
+
just-loaded struct — a REAL false positive caught on the amysynth ELF,
|
|
39
|
+
where `amy_fill_buffer` briefly reported data symbol `amy_global` as a
|
|
40
|
+
callee) leaves it indirect. `render_partial` now reports `__divsf3` etc.
|
|
41
|
+
- CLI (`is_elf` magic-bytes, `derive_objdump` gcc→objdump swap,
|
|
42
|
+
`run_objdump`, `run_elf`, `--filter REGEX`, `--objdump PATH`,
|
|
43
|
+
`TestElfMode` 14 tests): named fns get listing + table, --filter adds
|
|
44
|
+
table-only rows, bare ELF errors with a hint, unknown names get difflib
|
|
45
|
+
close-match suggestions, --pair/--across/--layout rejected. Deriving
|
|
46
|
+
objdump resolves config targets with `want_db=False` (new param on
|
|
47
|
+
build_matrix/target_command) so a target's `compile_commands = true`
|
|
48
|
+
no longer triggers db discovery when nothing is compiled — found by
|
|
49
|
+
running against the real config.
|
|
50
|
+
- Docs: module docstring (five modes + ELF usage line), README section
|
|
51
|
+
"ELF input: what actually shipped after LTO" (incl. the ZOL kill rules),
|
|
52
|
+
command-reference rows, a pointer from "Why not objdump by hand", and a
|
|
53
|
+
narrowed indirect-call limitation. Example toml untouched (no config
|
|
54
|
+
keys added).
|
|
55
|
+
- Suite: 182 tests, sole failure the pre-existing env-dependent
|
|
56
|
+
`test_p_is_pair`. End-to-end validated on the real ELF via config-derived
|
|
57
|
+
objdump (glob → esp-15.2.0 toolchain). Nothing committed.
|
|
58
|
+
|
|
59
|
+
## What the mode does
|
|
60
|
+
|
|
61
|
+
`asmdiff build/firmware.elf render_lut render_lut_cub` (or `--filter 'render_'`)
|
|
62
|
+
disassembles the linked ELF via the toolchain's objdump and feeds each function
|
|
63
|
+
through the EXISTING analyzers and renderers — same summary table
|
|
64
|
+
(`function / insns / calls / loop spans`, ZOL spans marked by `_LEND`-style
|
|
65
|
+
labels), no compile matrix.
|
|
66
|
+
|
|
67
|
+
Why it must exist: LTO fully inlines functions ("symbol from plugin" in the
|
|
68
|
+
linker map) — in the motivating audit, `stereo_reverb` / `parametric_eq_process` /
|
|
69
|
+
`delay_line_in_out` had no symbols at all; their loops lived inside
|
|
70
|
+
`amy_fill_buffer`. Only the ELF can answer "did this loop ship as ZOL".
|
|
71
|
+
|
|
72
|
+
## Reuse map (verified against asmdiff.py @ 1298 lines)
|
|
73
|
+
|
|
74
|
+
Unchanged, reused verbatim:
|
|
75
|
+
- `analyze()` (line ~530) — insns + calls from "cleaned asm lines" (mnemonic +
|
|
76
|
+
operands, labels end with `:`). Format-agnostic.
|
|
77
|
+
- `loop_spans()` (~557) — backward-branch spans + Xtensa `loop*`-to-end-label
|
|
78
|
+
spans. Works on the same cleaned-lines shape IF branch targets are labels.
|
|
79
|
+
- `CALL_RE` (~524), `inspect_table`, `file_summary_table`, `format_spans`,
|
|
80
|
+
`format_calls`, `render_table`.
|
|
81
|
+
|
|
82
|
+
New code:
|
|
83
|
+
1. **objdump parser** (~70 lines), sibling of `extract_functions()` (~477):
|
|
84
|
+
- Function header: `^([0-9a-f]+) <([^>]+)>:$`.
|
|
85
|
+
- Insn line: tab-separated `addr:\thexbytes\tmnemonic\toperands` (hex bytes
|
|
86
|
+
are ONE token, e.g. `004136` — do not confuse with the mnemonic).
|
|
87
|
+
- Convert to cleaned lines: rewrite branch-target addresses
|
|
88
|
+
(`beqz a7, 4037d176 <fn+0x82>`) into synthetic local labels (e.g. `.L82`)
|
|
89
|
+
and insert the label line at the target instruction, so `loop_spans()`
|
|
90
|
+
works unchanged. Xtensa `loop aN, <end-addr>` gets the same treatment;
|
|
91
|
+
name the synthetic end labels `.L<off>_LEND` so the existing ZOL-vs-branch
|
|
92
|
+
visual convention holds.
|
|
93
|
+
- Skip data regions: headers with `+`/`-` offset forms
|
|
94
|
+
(`<render_lpf_lut$constprop$0-0x1130>:` = literal pools / symbol-less
|
|
95
|
+
gaps that disassemble as garbage) and bare `...` filler lines.
|
|
96
|
+
2. **Longcall resolver** (~25 lines, Xtensa): `-mlongcalls` turns direct calls
|
|
97
|
+
into `l32r aX, <lit> (VALUE <sym>)` … `callx8 aX`. Resolve by scanning back
|
|
98
|
+
≤8 insns for the `l32r` into the same register and take the `<sym>` from its
|
|
99
|
+
parenthesized annotation; rewrite the line so `CALL_RE` reports the real
|
|
100
|
+
symbol instead of an indirect. Degrade gracefully to "indirect" when the
|
|
101
|
+
annotation is absent (binutils format drift) — do not promise universality.
|
|
102
|
+
3. **objdump invocation + CLI** (~80 lines): detect an `.elf` positional (or
|
|
103
|
+
ELF magic), derive objdump from the resolved cc path (swap trailing `gcc` →
|
|
104
|
+
`objdump`; the resolve_cc glob machinery ~708 already finds the toolchain),
|
|
105
|
+
optional `--objdump PATH` override. A whole ELF has ~1700 functions, so
|
|
106
|
+
bare-ELF invocation needs `--filter REGEX` (function names remain the
|
|
107
|
+
positional form). No compile, no matrix, no `-db`.
|
|
108
|
+
4. **Tests + docs**: golden objdump snippets, one class per feature area
|
|
109
|
+
(repo convention, `python3 test_asmdiff.py`, currently 147 tests pass).
|
|
110
|
+
README + module docstring + (if flags/config change) `asmdiff.example.toml`
|
|
111
|
+
AND its byte-identical `EXAMPLE_CONFIG` constant — a unit test enforces the
|
|
112
|
+
match. Stdlib-only, no third-party deps (hard repo rule).
|
|
113
|
+
|
|
114
|
+
## Ground-truth fixtures (from the motivating audit; ELF at
|
|
115
|
+
`S3-Amysynth/build/S3-Amysynth.elf`, toolchain esp-15.2.0 xtensa-esp32s3-elf)
|
|
116
|
+
|
|
117
|
+
Use as acceptance checks (re-verify against the current build — it may have
|
|
118
|
+
been rebuilt since):
|
|
119
|
+
- `render_lut` 1 ZOL loop, call-free; `filter_process` 6 ZOL + 3 branch.
|
|
120
|
+
- `render_lut_cub` 0 ZOL, 1 backward-branch loop, call-free (register-pressure
|
|
121
|
+
loss — the interesting true-negative).
|
|
122
|
+
- `amy_fill_buffer` 3 ZOL + 5 branch loops, all call-free (holds the inlined
|
|
123
|
+
FX pipeline).
|
|
124
|
+
- `render_partial` loop contains `__divsf3` longcalls that resolve via the
|
|
125
|
+
l32r pattern — the longcall-resolver test case.
|
|
126
|
+
|
|
127
|
+
## ZOL domain facts (empirically verified on esp-15.2.0, -O2)
|
|
128
|
+
|
|
129
|
+
One call surviving to codegen anywhere in the loop body kills ZOL even behind a
|
|
130
|
+
never-taken runtime condition; early `break` kills it; spilled loop counter
|
|
131
|
+
(register pressure) kills it; nested loops: inner only. These belong in the
|
|
132
|
+
README section explaining what the mode is for.
|
|
133
|
+
|
|
134
|
+
## Hypotheses
|
|
135
|
+
|
|
136
|
+
N/A (feature work, not debugging). Open design choices, pick during TDD:
|
|
137
|
+
- OPEN: `.elf` detection — extension vs magic bytes (magic is safer, 4 bytes).
|
|
138
|
+
- OPEN: whether ELF mode with no names and no `--filter` prints the full
|
|
139
|
+
~1700-row table or errors asking for a filter (lean: error with hint).
|
|
140
|
+
|
|
141
|
+
## Next steps
|
|
142
|
+
|
|
143
|
+
1. Read this repo's `CLAUDE.md`, `README.md` (inspect-mode section), and skim
|
|
144
|
+
`2026-07-06-inspect-mode.md` for how the last mode was integrated + doc'd.
|
|
145
|
+
2. TDD: write the objdump-parser tests first (golden snippet with a ZOL loop,
|
|
146
|
+
a backward branch, a data region to skip, a longcall) — then implement the
|
|
147
|
+
parser to green.
|
|
148
|
+
3. Longcall resolver + tests.
|
|
149
|
+
4. CLI routing + objdump-path derivation + tests.
|
|
150
|
+
5. README/docstring updates; full suite `python3 test_asmdiff.py`.
|
|
151
|
+
6. Validate against the real S3-Amysynth ELF using the fixtures above.
|
|
152
|
+
|
|
153
|
+
## Hardware-verify pending
|
|
154
|
+
|
|
155
|
+
None — this tool never touches hardware. Real-ELF validation (step 6) is the
|
|
156
|
+
equivalent gate.
|
|
@@ -22,7 +22,12 @@ PyPI as `asmdiff` (hatchling build, see pyproject.toml).
|
|
|
22
22
|
|
|
23
23
|
- `asmdiff.py` — the entire tool.
|
|
24
24
|
- `test_asmdiff.py` — unit tests, one class per feature area.
|
|
25
|
-
- `asmdiff.example.toml` — documented starter config; keep it generic
|
|
26
|
-
|
|
25
|
+
- `asmdiff.example.toml` — documented starter config; keep it generic:
|
|
26
|
+
portable gcc/clang defaults plus active embedded target profiles
|
|
27
|
+
grouped by toolchain family (riscv32-esp, xtensa-esp, and basic
|
|
28
|
+
stm32/rp2350 entries), default install paths or bare PATH names and
|
|
29
|
+
minimal arch flags only, nothing machine-specific. Mirrored
|
|
30
|
+
byte-for-byte in asmdiff.py's EXAMPLE_CONFIG (a unit test enforces
|
|
31
|
+
the match): any edit to the toml must update the constant too.
|
|
27
32
|
- README.md documents every user-visible behavior; changes to flags or
|
|
28
33
|
config keys update README, example toml, and the module docstring.
|