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.
@@ -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
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]] [-v] [-- EXTRA_FLAGS...]
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]] [-v] [-- EXTRA_FLAGS...]
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]] [-v]
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
- if missing_ok:
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(tags, sections):
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")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "asmdiff"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Compare per-function assembly between paired C implementations"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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