asmdiff 0.1.3__tar.gz → 0.1.4__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.4/FIELD-TEST-REPORT.md +123 -0
- {asmdiff-0.1.3 → asmdiff-0.1.4}/PKG-INFO +27 -2
- {asmdiff-0.1.3 → asmdiff-0.1.4}/README.md +26 -1
- {asmdiff-0.1.3 → asmdiff-0.1.4}/asmdiff.py +104 -18
- {asmdiff-0.1.3 → asmdiff-0.1.4}/pyproject.toml +1 -1
- {asmdiff-0.1.3 → asmdiff-0.1.4}/test_asmdiff.py +109 -0
- {asmdiff-0.1.3 → asmdiff-0.1.4}/.claude/settings.local.json +0 -0
- {asmdiff-0.1.3 → asmdiff-0.1.4}/.gitignore +0 -0
- {asmdiff-0.1.3 → asmdiff-0.1.4}/CLAUDE.md +0 -0
- {asmdiff-0.1.3 → asmdiff-0.1.4}/LICENSE +0 -0
- {asmdiff-0.1.3 → asmdiff-0.1.4}/asmdiff.example.toml +0 -0
- {asmdiff-0.1.3 → asmdiff-0.1.4}/asmdiff_example.c +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# asmdiff field-test report
|
|
2
|
+
|
|
3
|
+
**Version tested:** working tree at 0.1.3 (post response-file/object-label/walk-up/verbose fixes), run via `uvx --from <repo>` so the packaged entry point, not the checkout, is what executed.
|
|
4
|
+
**Environment:** S3-Amysynth - a real ESP32-S3 / ESP-IDF v6 firmware (FreeRTOS, TinyUSB, vendored AMY synth engine), `build/compile_commands.json` with 1024 entries, xtensa-esp-elf-gcc 15.2.0 (picolibc via specs in a `@response` file), plus host gcc/clang and g++ for contrast.
|
|
5
|
+
|
|
6
|
+
## Scenario matrix
|
|
7
|
+
|
|
8
|
+
| # | Scenario | Result |
|
|
9
|
+
|---|----------|--------|
|
|
10
|
+
| A1 | Small harness, `old_*`/`new_*` auto-pairing, default gcc+clang fallback | works - both pairs found, fold-vs-libcall visible immediately |
|
|
11
|
+
| A2 | Same harness through a config target: glob `cc`, `compile_commands = true`, harness not in db | works - glob resolution announced, soft-miss note printed, xtensa FPU asm side-by-side |
|
|
12
|
+
| A3 | Two-target matrix (`--target s3 --target s3-size`) | works - O2 vs Os counts differ as expected |
|
|
13
|
+
| B1 | Whole-file summary of a real DSP file (`oscillators.c`, 58 functions) via db flags | works - and immediately useful: softfloat `__muldf3`/`__extendsfdf2` calls stand out in oscillator init paths |
|
|
14
|
+
| B2 | `--pair render_lut:render_lut_fb` inside a real file | works |
|
|
15
|
+
| C1 | `--across render_lut` between two config profiles | works - zero-overhead loop spans (`.L*_LEND`) detected on both sides |
|
|
16
|
+
| C2 | `--across` real file vs modified copy outside the tree | fails cleanly: soft-miss note + trimmed "amy.h: No such file" error, `--verbose` hint |
|
|
17
|
+
| C2b | `--across` real file vs copy **next to** the original | **compiles but silently misleads** - see Trap 1 |
|
|
18
|
+
| C3 | Two real files (both in db), per-file summary blocks | works - each file gets its own recorded flags |
|
|
19
|
+
| E1-E3 | Error paths: bogus target, string `flags`, explicit db + absent source | all produce specific, actionable messages (bogus target lists available targets) |
|
|
20
|
+
| E4 | C++ file via `g++` | compiles, functions extracted - but see Trap 3 |
|
|
21
|
+
| F1 | Giant function (`play_delta`, ~1000 insns) across O2/Os | works; see note on span semantics |
|
|
22
|
+
| F2 | Bare `--` extra flags (README's own example) | works |
|
|
23
|
+
| F3 | Source with no functions | `(no functions found)`, no crash |
|
|
24
|
+
| F4 | `--pair` with two files | correct argparse guard pointing at `--across` |
|
|
25
|
+
|
|
26
|
+
## Traps and gaps (ranked)
|
|
27
|
+
|
|
28
|
+
### 1. Same-directory copy: identical source, different assembly, one stderr note
|
|
29
|
+
The natural before/after move - copy `oscillators.c` to `osc_tweak.c` in the same
|
|
30
|
+
directory and `--across` them - *succeeds*, because quote-includes resolve next to
|
|
31
|
+
the source. But the copy has no db entry, so it compiles without the recorded
|
|
32
|
+
`-D`/`-I` set: in this project that means no `ESP_PLATFORM`, a different
|
|
33
|
+
`AMY_SAMPLE_RATE`, the portable code paths. The two columns show different
|
|
34
|
+
register allocation and frame sizes **for byte-identical code**. The only
|
|
35
|
+
warning is the one-line soft-miss note, whose implication ("the flag sets now
|
|
36
|
+
differ") is easy to miss. A user would read real codegen differences into a
|
|
37
|
+
header-config artifact.
|
|
38
|
+
|
|
39
|
+
**Recommendation:** when the two sides of an `--across`/two-file run received
|
|
40
|
+
*different* borrowed-flag sets (one hit, one soft miss), say so loudly - e.g.
|
|
41
|
+
tag the column header `[no db entry]` and print a warning that the comparison
|
|
42
|
+
crosses header configurations.
|
|
43
|
+
|
|
44
|
+
### 2. Before/after on a project source has no supported path
|
|
45
|
+
The tool's pitch ("what does the compiler emit before and after my rewrite")
|
|
46
|
+
cannot currently be executed for a source that needs db flags, except by
|
|
47
|
+
editing in place and diffing two separate runs by eye, or by maintaining two
|
|
48
|
+
built checkouts with two databases. C2/C2b are both dead ends by design.
|
|
49
|
+
|
|
50
|
+
**Recommendation (feature):** an opt-in fallback such as "borrow flags from
|
|
51
|
+
the same-basename entry" (with a warning), or an explicit
|
|
52
|
+
`--compile-commands-entry PATH` to name which db entry a given source should
|
|
53
|
+
borrow from. Either would make the copy workflow first-class and would also
|
|
54
|
+
neutralize Trap 1.
|
|
55
|
+
|
|
56
|
+
### 3. C++ defeats auto-pairing silently
|
|
57
|
+
`_Z9old_scalef` does not start with `old_`, so a C++ harness falls back to
|
|
58
|
+
whole-file summary with no hint about why. Workable via `extern "C"` or
|
|
59
|
+
explicit `--pair` with mangled names, but nothing tells the user that.
|
|
60
|
+
Acceptable for a C-focused tool; a doc note (or a "mangled names detected,
|
|
61
|
+
auto-pairing skipped" hint) would close it.
|
|
62
|
+
|
|
63
|
+
### 4. Loop spans are coarse on large control flow
|
|
64
|
+
`play_delta` (1001 insns) reports spans like `.L513:676` - two-thirds of the
|
|
65
|
+
function - because any backward branch defines a span, and big switch/dispatch
|
|
66
|
+
functions branch backwards for control flow that is not a loop. The README's
|
|
67
|
+
"the listing and your source know, not the tool" caveat covers this honestly,
|
|
68
|
+
but on 1000-instruction functions the span column stops being informative.
|
|
69
|
+
|
|
70
|
+
### Minor observations
|
|
71
|
+
- The glob-resolution notice (`cc pattern matched 2 toolchains ...`) prints on
|
|
72
|
+
every run; correct behavior (never silent), slightly noisy in scripted use.
|
|
73
|
+
- Two-file summary blocks are rendered as independent tables, so their columns
|
|
74
|
+
do not align with each other - cosmetic.
|
|
75
|
+
- `-ffast-math` after `--` reached the compiler as documented (no behavior
|
|
76
|
+
change for the test case, which was already folded).
|
|
77
|
+
|
|
78
|
+
## What clearly works now (regression-verified this session)
|
|
79
|
+
- ESP-IDF v6 out of the box: `@response-file` expansion delivers
|
|
80
|
+
`-specs=picolibc.specs`; `main.c` and every AMY source compiled with only
|
|
81
|
+
`-O2 -mlongcalls` supplied by the user.
|
|
82
|
+
- Function extraction is clean: 72/72 real functions on `amy.c`, zero data
|
|
83
|
+
labels, counts match `.type @function` ground truth.
|
|
84
|
+
- Walk-up discovery: project root, `main/`, and `components/amy/src/` all find
|
|
85
|
+
`build/compile_commands.json`; the `.git` boundary prevents escaping the repo.
|
|
86
|
+
- Failure UX: 4-line trimmed error with a `--verbose` escape hatch instead of a
|
|
87
|
+
13 KB flag dump.
|
|
88
|
+
- Config system end-to-end: CWD discovery, `default`, multi-target matrices,
|
|
89
|
+
glob `cc`, env expansion, precise config error messages.
|
|
90
|
+
|
|
91
|
+
## Postscript: guards added after this report
|
|
92
|
+
|
|
93
|
+
Traps 1-3 were addressed in the working tree the same day, and re-verified
|
|
94
|
+
against the scenarios above:
|
|
95
|
+
|
|
96
|
+
- **Trap 1:** a two-file comparison with borrowed flags on only one side now
|
|
97
|
+
tags the column `[no db entry]` and warns that differences may be header
|
|
98
|
+
configuration, not codegen. The C2b rerun makes the case vividly: identical
|
|
99
|
+
source showed 51 vs 14 instructions (partial outlining under the portable
|
|
100
|
+
header config) — now labeled instead of silent.
|
|
101
|
+
- **Trap 2:** new `--flags-like PATH` lets a db-less source borrow a named
|
|
102
|
+
entry's flags. Rerunning C2 and C2b with it produced identical 51/51
|
|
103
|
+
columns for identical source, in-tree and out-of-tree. The absent-source
|
|
104
|
+
error and the soft-miss note both suggest the flag when a same-basename
|
|
105
|
+
entry exists.
|
|
106
|
+
- **Trap 3:** falling back to summary when only mangled `_Z*old_*` symbols
|
|
107
|
+
exist now prints a note naming `extern "C"` and `--pair` as the outs.
|
|
108
|
+
|
|
109
|
+
Trap 4 (span coarseness on giant control flow) is documented behavior and
|
|
110
|
+
was left as-is.
|
|
111
|
+
|
|
112
|
+
## Evaluation
|
|
113
|
+
For its stated job - harness-based codegen comparison and whole-file/function
|
|
114
|
+
inspection of real embedded sources - the tool now works credibly in a real
|
|
115
|
+
ESP-IDF v6 project, with fast (<2 s) turnarounds and output that surfaced a
|
|
116
|
+
genuine finding within minutes (software-double promotion in AMY filter/
|
|
117
|
+
oscillator setup paths via `__extendsfdf2`/`__muldf3` in the calls column).
|
|
118
|
+
Error paths are uniformly specific and short. The remaining weaknesses cluster
|
|
119
|
+
around one theme: comparing *modified copies* of db-backed sources, where the
|
|
120
|
+
current behavior ranges from a clean failure (C2) to a quietly misleading
|
|
121
|
+
success (C2b). That workflow is close enough to the tool's core promise that
|
|
122
|
+
Trap 1's warning and Trap 2's fallback are the highest-value next changes;
|
|
123
|
+
everything else found is polish.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Compare per-function assembly between paired C implementations
|
|
5
5
|
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
6
|
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
@@ -94,7 +94,8 @@ $ asmdiff.py asmdiff_example.c
|
|
|
94
94
|
```
|
|
95
95
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
96
96
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
97
|
-
[--compile-commands [PATH]] [-
|
|
97
|
+
[--compile-commands [PATH]] [--flags-like PATH] [-v]
|
|
98
|
+
[-- EXTRA_FLAGS...]
|
|
98
99
|
```
|
|
99
100
|
|
|
100
101
|
| Option | Meaning |
|
|
@@ -106,6 +107,7 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
106
107
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
107
108
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
108
109
|
| `--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). |
|
|
110
|
+
| `--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. |
|
|
109
111
|
| `-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. |
|
|
110
112
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
111
113
|
|
|
@@ -275,6 +277,29 @@ instead of being an error. That keeps standalone harness files working
|
|
|
275
277
|
when a `build/` directory happens to sit nearby; an explicitly named
|
|
276
278
|
database still treats an absent source as the error it is.
|
|
277
279
|
|
|
280
|
+
If a two-file comparison ends up with borrowed flags on only *one* side,
|
|
281
|
+
the mismatched column is tagged `[no db entry]` and a warning is printed:
|
|
282
|
+
the two sides then differ in header configuration — defines, include
|
|
283
|
+
paths — not just source, and byte-identical code can compile to visibly
|
|
284
|
+
different assembly. Don't read codegen meaning into such a diff.
|
|
285
|
+
|
|
286
|
+
### Comparing a modified copy of a project source
|
|
287
|
+
|
|
288
|
+
The before/after workflow — copy `oscillators.c` to `osc_tweak.c`, change
|
|
289
|
+
one thing, `--across` them — is exactly the situation above: the copy has
|
|
290
|
+
no database entry. `--flags-like` names the entry the copy should borrow:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
asmdiff.py src/oscillators.c src/osc_tweak.c --across render_lut \
|
|
294
|
+
--target s3 --flags-like src/oscillators.c
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Both sides now compile under the same recorded header environment, so the
|
|
298
|
+
diff is your edit and nothing else. This also covers a git-worktree
|
|
299
|
+
baseline (`../baseline/src/oscillators.c`), which is the same file under a
|
|
300
|
+
path the database has never heard of. The absent-source error and the
|
|
301
|
+
soft-miss note both point at this flag when a same-name entry exists.
|
|
302
|
+
|
|
278
303
|
## Whole-file summary
|
|
279
304
|
|
|
280
305
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -76,7 +76,8 @@ $ asmdiff.py asmdiff_example.c
|
|
|
76
76
|
```
|
|
77
77
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
78
78
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
79
|
-
[--compile-commands [PATH]] [-
|
|
79
|
+
[--compile-commands [PATH]] [--flags-like PATH] [-v]
|
|
80
|
+
[-- EXTRA_FLAGS...]
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
| Option | Meaning |
|
|
@@ -88,6 +89,7 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
88
89
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
89
90
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
90
91
|
| `--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). |
|
|
92
|
+
| `--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. |
|
|
91
93
|
| `-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. |
|
|
92
94
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
93
95
|
|
|
@@ -257,6 +259,29 @@ instead of being an error. That keeps standalone harness files working
|
|
|
257
259
|
when a `build/` directory happens to sit nearby; an explicitly named
|
|
258
260
|
database still treats an absent source as the error it is.
|
|
259
261
|
|
|
262
|
+
If a two-file comparison ends up with borrowed flags on only *one* side,
|
|
263
|
+
the mismatched column is tagged `[no db entry]` and a warning is printed:
|
|
264
|
+
the two sides then differ in header configuration — defines, include
|
|
265
|
+
paths — not just source, and byte-identical code can compile to visibly
|
|
266
|
+
different assembly. Don't read codegen meaning into such a diff.
|
|
267
|
+
|
|
268
|
+
### Comparing a modified copy of a project source
|
|
269
|
+
|
|
270
|
+
The before/after workflow — copy `oscillators.c` to `osc_tweak.c`, change
|
|
271
|
+
one thing, `--across` them — is exactly the situation above: the copy has
|
|
272
|
+
no database entry. `--flags-like` names the entry the copy should borrow:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
asmdiff.py src/oscillators.c src/osc_tweak.c --across render_lut \
|
|
276
|
+
--target s3 --flags-like src/oscillators.c
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Both sides now compile under the same recorded header environment, so the
|
|
280
|
+
diff is your edit and nothing else. This also covers a git-worktree
|
|
281
|
+
baseline (`../baseline/src/oscillators.c`), which is the same file under a
|
|
282
|
+
path the database has never heard of. The absent-source error and the
|
|
283
|
+
soft-miss note both point at this flag when a same-name entry exists.
|
|
284
|
+
|
|
260
285
|
## Whole-file summary
|
|
261
286
|
|
|
262
287
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -12,7 +12,8 @@ Usage:
|
|
|
12
12
|
tools/asmdiff/asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]...
|
|
13
13
|
[--across FUNC]... [--cc 'CC FLAGS']...
|
|
14
14
|
[--target NAME]... [--config PATH]
|
|
15
|
-
[--compile-commands [PATH]]
|
|
15
|
+
[--compile-commands [PATH]]
|
|
16
|
+
[--flags-like PATH] [-v]
|
|
16
17
|
[-- EXTRA_FLAGS...]
|
|
17
18
|
|
|
18
19
|
Three modes:
|
|
@@ -38,7 +39,9 @@ compile_commands = true in a target — or a bare --compile-commands on the
|
|
|
38
39
|
command line — instead finds the database by checking each directory from
|
|
39
40
|
the CWD up to the repository root, and its build/ subdirectory; a source
|
|
40
41
|
absent from a database found that way is compiled without borrowed flags
|
|
41
|
-
(with a note) rather than being an error.
|
|
42
|
+
(with a note) rather than being an error. --flags-like PATH lets such a
|
|
43
|
+
source borrow the flags recorded for PATH, so a modified copy compares
|
|
44
|
+
against its original under one header environment.
|
|
42
45
|
"""
|
|
43
46
|
import argparse
|
|
44
47
|
import glob
|
|
@@ -81,6 +84,12 @@ _CC_DB_PLAIN_FLAGS = ("-D", "-U")
|
|
|
81
84
|
_CC_DB_EQ_FLAGS = ("-specs", "--specs", "--sysroot")
|
|
82
85
|
_DB_CACHE = {}
|
|
83
86
|
_MISS_NOTED = set()
|
|
87
|
+
_BORROW_NOTED = set()
|
|
88
|
+
# --flags-like PATH: a source with no database entry borrows the flags
|
|
89
|
+
# recorded for PATH. This is how a modified copy of a project source gets
|
|
90
|
+
# the same header environment as its original, so an --across between them
|
|
91
|
+
# compares codegen instead of header configurations.
|
|
92
|
+
FLAGS_LIKE = None
|
|
84
93
|
|
|
85
94
|
|
|
86
95
|
class Target(str):
|
|
@@ -257,9 +266,47 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
257
266
|
and the message flags a same-name entry recorded under a different path.
|
|
258
267
|
With ``missing_ok`` (auto-discovered databases) an absent source instead
|
|
259
268
|
compiles without borrowed flags, after a one-time note per (db, source).
|
|
269
|
+
In either mode, --flags-like PATH satisfies an absent source with the
|
|
270
|
+
flags recorded for PATH — the modified-copy workflow.
|
|
260
271
|
"""
|
|
261
272
|
entries = load_compile_commands(db_path)
|
|
262
273
|
want = Path(source).resolve()
|
|
274
|
+
flags, name_seen = _lookup_entry(entries, want)
|
|
275
|
+
if flags is not None:
|
|
276
|
+
return flags
|
|
277
|
+
if FLAGS_LIKE:
|
|
278
|
+
like = Path(FLAGS_LIKE).resolve()
|
|
279
|
+
like_flags, _ = _lookup_entry(entries, like)
|
|
280
|
+
if like_flags is None:
|
|
281
|
+
sys.exit(f"error: --flags-like {FLAGS_LIKE} "
|
|
282
|
+
f"not found in {db_path}")
|
|
283
|
+
key = (db_path, str(want))
|
|
284
|
+
if key not in _BORROW_NOTED:
|
|
285
|
+
_BORROW_NOTED.add(key)
|
|
286
|
+
print(f"note: {source} not in {db_path}; borrowing the flags "
|
|
287
|
+
f"recorded for {FLAGS_LIKE}", file=sys.stderr)
|
|
288
|
+
return like_flags
|
|
289
|
+
suggest = ("; --flags-like PATH can borrow another entry's flags"
|
|
290
|
+
if name_seen else "")
|
|
291
|
+
if missing_ok:
|
|
292
|
+
key = (db_path, str(want))
|
|
293
|
+
if key not in _MISS_NOTED:
|
|
294
|
+
_MISS_NOTED.add(key)
|
|
295
|
+
print(f"note: {source} not in {db_path}; compiling without "
|
|
296
|
+
f"its include/define flags{suggest}", file=sys.stderr)
|
|
297
|
+
return []
|
|
298
|
+
hint = (f"; an entry named {want.name} exists under a different path — "
|
|
299
|
+
"pass the source as it appears in the database, or borrow its "
|
|
300
|
+
"flags with --flags-like") if name_seen else ""
|
|
301
|
+
sys.exit(f"error: {source} not found in {db_path}{hint}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _lookup_entry(entries, want):
|
|
305
|
+
"""Flags for the entry whose file resolves to ``want``, or None.
|
|
306
|
+
|
|
307
|
+
The second result reports whether some entry shares ``want``'s
|
|
308
|
+
basename — the raw material for the miss hints above.
|
|
309
|
+
"""
|
|
263
310
|
name_seen = False
|
|
264
311
|
for entry in entries:
|
|
265
312
|
f = entry.get("file")
|
|
@@ -278,20 +325,10 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
278
325
|
tokens = (list(args) if isinstance(args, list)
|
|
279
326
|
else shlex.split(entry.get("command", "")))
|
|
280
327
|
tokens = _expand_response_files(tokens, directory)
|
|
281
|
-
return include_flags(tokens, directory)
|
|
328
|
+
return include_flags(tokens, directory), name_seen
|
|
282
329
|
if Path(f).name == want.name:
|
|
283
330
|
name_seen = True
|
|
284
|
-
|
|
285
|
-
key = (db_path, str(want))
|
|
286
|
-
if key not in _MISS_NOTED:
|
|
287
|
-
_MISS_NOTED.add(key)
|
|
288
|
-
print(f"note: {source} not in {db_path}; "
|
|
289
|
-
"compiling without its include/define flags",
|
|
290
|
-
file=sys.stderr)
|
|
291
|
-
return []
|
|
292
|
-
hint = (f"; an entry named {want.name} exists under a different path — "
|
|
293
|
-
"pass the source as it appears in the database") if name_seen else ""
|
|
294
|
-
sys.exit(f"error: {source} not found in {db_path}{hint}")
|
|
331
|
+
return None, name_seen
|
|
295
332
|
|
|
296
333
|
# A label at column 0 that is not a local (.L*) label starts a function.
|
|
297
334
|
FUNC_LABEL = re.compile(r"^([A-Za-z_][\w$.]*):")
|
|
@@ -640,6 +677,41 @@ def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
|
640
677
|
return out_s.read_text()
|
|
641
678
|
|
|
642
679
|
|
|
680
|
+
def mark_db_misses(cc_cmd, sources, tags):
|
|
681
|
+
"""Column tags for a two-file comparison, marking a side that compiled
|
|
682
|
+
without borrowed flags while the other side had them.
|
|
683
|
+
|
|
684
|
+
That situation — one source in the database, its copy not — means the
|
|
685
|
+
columns differ in header configuration (defines, include paths), not
|
|
686
|
+
just in source, so byte-identical code can show different assembly.
|
|
687
|
+
The marked tag and a warning keep that from reading as a codegen
|
|
688
|
+
finding. Both sides missing is a consistent environment: no warning.
|
|
689
|
+
"""
|
|
690
|
+
db = getattr(cc_cmd, "compile_commands", None)
|
|
691
|
+
if not db:
|
|
692
|
+
return list(tags)
|
|
693
|
+
missed = [(db, str(Path(s).resolve())) in _MISS_NOTED for s in sources]
|
|
694
|
+
if not any(missed) or all(missed):
|
|
695
|
+
return list(tags)
|
|
696
|
+
print("warning: only one side got flags from the database — assembly "
|
|
697
|
+
"differences may reflect header configuration, not source "
|
|
698
|
+
"changes; --flags-like PATH gives the copy its original's flags",
|
|
699
|
+
file=sys.stderr)
|
|
700
|
+
return [t + " [no db entry]" if m else t for t, m in zip(tags, missed)]
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# C++ mangles old_scale(float) to _Z9old_scalef: the old_/new_ prefixes
|
|
704
|
+
# survive inside the mangled name but no longer lead it, so auto-pairing
|
|
705
|
+
# finds nothing and would fall back to the summary without explanation.
|
|
706
|
+
MANGLED_PAIR = re.compile(r"_Z\d+(?:old|new)_")
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def mangled_pair_hint(names):
|
|
710
|
+
"""True when the extracted names look like C++-mangled old_*/new_*
|
|
711
|
+
functions — the harness probably just needs extern \"C\"."""
|
|
712
|
+
return any(MANGLED_PAIR.match(n) for n in names)
|
|
713
|
+
|
|
714
|
+
|
|
643
715
|
def side_by_side(left, right, ltitle, rtitle, width=44):
|
|
644
716
|
"""Two-column view of a pair's asm lines."""
|
|
645
717
|
rows = [f"{ltitle:<{width}} | {rtitle}",
|
|
@@ -762,9 +834,10 @@ def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
|
762
834
|
if len(sides) < 2:
|
|
763
835
|
continue
|
|
764
836
|
ran_any = True
|
|
837
|
+
tags = mark_db_misses(cc_cmd, sources,
|
|
838
|
+
file_tags(sources[0], sources[1]))
|
|
765
839
|
print(f"\n== {cc_cmd} ==\n")
|
|
766
|
-
report_across(fn_names, sides[0], sides[1],
|
|
767
|
-
*file_tags(sources[0], sources[1]))
|
|
840
|
+
report_across(fn_names, sides[0], sides[1], *tags)
|
|
768
841
|
if not ran_any:
|
|
769
842
|
sys.exit("error: no usable compiler in the matrix")
|
|
770
843
|
return 0
|
|
@@ -802,8 +875,10 @@ def run_summary(sources, matrix, extra_flags, tmp):
|
|
|
802
875
|
if len(sections) < len(sources):
|
|
803
876
|
continue
|
|
804
877
|
ran_any = True
|
|
878
|
+
shown = (mark_db_misses(cc_cmd, sources, tags)
|
|
879
|
+
if len(sources) == 2 else tags)
|
|
805
880
|
print(f"\n== {cc_cmd} ==")
|
|
806
|
-
for tag, funcs in zip(
|
|
881
|
+
for tag, funcs in zip(shown, sections):
|
|
807
882
|
if len(sections) > 1:
|
|
808
883
|
print(f"\n-- {tag} --")
|
|
809
884
|
print()
|
|
@@ -830,6 +905,10 @@ def run_pairs(source, matrix, pair_specs, extra_flags, tmp):
|
|
|
830
905
|
pairs = ([tuple(p.split(":", 1)) for p in pair_specs]
|
|
831
906
|
or auto_pairs(funcs))
|
|
832
907
|
if not pairs:
|
|
908
|
+
if mangled_pair_hint(funcs):
|
|
909
|
+
print('note: C++ mangling defeats old_*/new_* auto-pairing; '
|
|
910
|
+
'declare the pairs extern "C" or use --pair with the '
|
|
911
|
+
'mangled names', file=sys.stderr)
|
|
833
912
|
print(f"\n== {cc_cmd} ==\n")
|
|
834
913
|
print(file_summary_table(funcs) if funcs
|
|
835
914
|
else "(no functions found)")
|
|
@@ -893,13 +972,20 @@ def main(argv=None):
|
|
|
893
972
|
help=f"config file; default search: {CONFIG_NAME} "
|
|
894
973
|
"next to SOURCE.c, in the current directory, "
|
|
895
974
|
"then in ~/.config/")
|
|
975
|
+
parser.add_argument("--flags-like", metavar="PATH",
|
|
976
|
+
help="when a source has no compile_commands entry, "
|
|
977
|
+
"borrow the include/define flags recorded for "
|
|
978
|
+
"PATH — lets a modified copy of a project "
|
|
979
|
+
"source compile (and compare) under its "
|
|
980
|
+
"original's header environment")
|
|
896
981
|
parser.add_argument("-v", "--verbose", action="store_true",
|
|
897
982
|
help="on compile failure, print the full compiler "
|
|
898
983
|
"command and complete error output instead of "
|
|
899
984
|
"the first lines")
|
|
900
985
|
args = parser.parse_args(argv)
|
|
901
|
-
global VERBOSE
|
|
986
|
+
global VERBOSE, FLAGS_LIKE
|
|
902
987
|
VERBOSE = args.verbose
|
|
988
|
+
FLAGS_LIKE = args.flags_like
|
|
903
989
|
|
|
904
990
|
if len(args.sources) > 2:
|
|
905
991
|
parser.error("at most two source files may be given")
|
|
@@ -647,6 +647,115 @@ __func__$1:
|
|
|
647
647
|
self.assertEqual(insns, 2)
|
|
648
648
|
|
|
649
649
|
|
|
650
|
+
class TestFlagsLike(unittest.TestCase):
|
|
651
|
+
"""--flags-like: a source absent from the db borrows a named entry."""
|
|
652
|
+
|
|
653
|
+
def _db(self, tmp):
|
|
654
|
+
path = Path(tmp) / "compile_commands.json"
|
|
655
|
+
path.write_text(json.dumps([{
|
|
656
|
+
"directory": tmp, "file": f"{tmp}/real.c",
|
|
657
|
+
"command": "cc -Iinc -DESP_PLATFORM -c real.c"}]))
|
|
658
|
+
asmdiff._DB_CACHE.clear()
|
|
659
|
+
asmdiff._MISS_NOTED.clear()
|
|
660
|
+
return str(path)
|
|
661
|
+
|
|
662
|
+
def test_missing_source_borrows_named_entry(self):
|
|
663
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
664
|
+
db = self._db(tmp)
|
|
665
|
+
asmdiff.FLAGS_LIKE = f"{tmp}/real.c"
|
|
666
|
+
try:
|
|
667
|
+
err = io.StringIO()
|
|
668
|
+
with contextlib.redirect_stderr(err):
|
|
669
|
+
flags = asmdiff.compile_commands_flags(
|
|
670
|
+
db, f"{tmp}/copy.c", missing_ok=True)
|
|
671
|
+
finally:
|
|
672
|
+
asmdiff.FLAGS_LIKE = None
|
|
673
|
+
self.assertEqual(flags, ["-I", f"{tmp}/inc", "-DESP_PLATFORM"])
|
|
674
|
+
self.assertIn("real.c", err.getvalue()) # borrow is announced
|
|
675
|
+
# A satisfied borrow is not a miss: no apples-to-oranges state.
|
|
676
|
+
self.assertEqual(asmdiff._MISS_NOTED, set())
|
|
677
|
+
|
|
678
|
+
def test_applies_to_explicit_db_too(self):
|
|
679
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
680
|
+
db = self._db(tmp)
|
|
681
|
+
asmdiff.FLAGS_LIKE = f"{tmp}/real.c"
|
|
682
|
+
try:
|
|
683
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
684
|
+
flags = asmdiff.compile_commands_flags(
|
|
685
|
+
db, f"{tmp}/copy.c", missing_ok=False)
|
|
686
|
+
finally:
|
|
687
|
+
asmdiff.FLAGS_LIKE = None
|
|
688
|
+
self.assertEqual(flags[-1], "-DESP_PLATFORM")
|
|
689
|
+
|
|
690
|
+
def test_bad_flags_like_target_is_an_error(self):
|
|
691
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
692
|
+
db = self._db(tmp)
|
|
693
|
+
asmdiff.FLAGS_LIKE = f"{tmp}/nowhere.c"
|
|
694
|
+
try:
|
|
695
|
+
with self.assertRaises(SystemExit):
|
|
696
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/copy.c",
|
|
697
|
+
missing_ok=True)
|
|
698
|
+
finally:
|
|
699
|
+
asmdiff.FLAGS_LIKE = None
|
|
700
|
+
|
|
701
|
+
def test_miss_messages_suggest_flags_like(self):
|
|
702
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
703
|
+
db = self._db(tmp)
|
|
704
|
+
with self.assertRaises(SystemExit) as ctx: # hard error path
|
|
705
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/other/real.c")
|
|
706
|
+
self.assertIn("--flags-like", str(ctx.exception))
|
|
707
|
+
err = io.StringIO() # soft-miss path
|
|
708
|
+
with contextlib.redirect_stderr(err):
|
|
709
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/sub/real.c",
|
|
710
|
+
missing_ok=True)
|
|
711
|
+
self.assertIn("--flags-like", err.getvalue())
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class TestMarkDbMisses(unittest.TestCase):
|
|
715
|
+
"""Two-file comparisons where only one side got borrowed flags."""
|
|
716
|
+
|
|
717
|
+
def _entry(self, db):
|
|
718
|
+
return asmdiff.Target("gcc -O2", db, db_discovered=True)
|
|
719
|
+
|
|
720
|
+
def test_one_sided_miss_tags_and_warns(self):
|
|
721
|
+
asmdiff._MISS_NOTED.clear()
|
|
722
|
+
asmdiff._MISS_NOTED.add(("/db.json", str(Path("b.c").resolve())))
|
|
723
|
+
err = io.StringIO()
|
|
724
|
+
with contextlib.redirect_stderr(err):
|
|
725
|
+
tags = asmdiff.mark_db_misses(self._entry("/db.json"),
|
|
726
|
+
["a.c", "b.c"], ("a.c", "b.c"))
|
|
727
|
+
self.assertEqual(tags, ["a.c", "b.c [no db entry]"])
|
|
728
|
+
self.assertIn("header", err.getvalue())
|
|
729
|
+
|
|
730
|
+
def test_both_missed_means_same_env_no_warning(self):
|
|
731
|
+
asmdiff._MISS_NOTED.clear()
|
|
732
|
+
for s in ("a.c", "b.c"):
|
|
733
|
+
asmdiff._MISS_NOTED.add(("/db.json", str(Path(s).resolve())))
|
|
734
|
+
err = io.StringIO()
|
|
735
|
+
with contextlib.redirect_stderr(err):
|
|
736
|
+
tags = asmdiff.mark_db_misses(self._entry("/db.json"),
|
|
737
|
+
["a.c", "b.c"], ("a.c", "b.c"))
|
|
738
|
+
self.assertEqual(tags, ["a.c", "b.c"])
|
|
739
|
+
self.assertEqual(err.getvalue(), "")
|
|
740
|
+
|
|
741
|
+
def test_no_db_entry_untouched(self):
|
|
742
|
+
tags = asmdiff.mark_db_misses("gcc -O2", ["a.c", "b.c"],
|
|
743
|
+
("a.c", "b.c"))
|
|
744
|
+
self.assertEqual(tags, ["a.c", "b.c"])
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class TestMangledPairHint(unittest.TestCase):
|
|
748
|
+
def test_mangled_old_new_detected(self):
|
|
749
|
+
self.assertTrue(asmdiff.mangled_pair_hint(
|
|
750
|
+
["_Z9old_scalef", "_Z9new_scalef"]))
|
|
751
|
+
|
|
752
|
+
def test_plain_c_names_no_hint(self):
|
|
753
|
+
self.assertFalse(asmdiff.mangled_pair_hint(["scale", "helper"]))
|
|
754
|
+
|
|
755
|
+
def test_mangled_but_not_old_new_no_hint(self):
|
|
756
|
+
self.assertFalse(asmdiff.mangled_pair_hint(["_Z6renderv"]))
|
|
757
|
+
|
|
758
|
+
|
|
650
759
|
class TestCompileFailureOutput(unittest.TestCase):
|
|
651
760
|
CMD = ["xtensa-gcc", "-O2"] + [f"-I/inc{i}" for i in range(50)] + ["a.c"]
|
|
652
761
|
STDERR = "\n".join(f"err line {i}" for i in range(60))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|