c1_sampler2.py launches CL.Exe CREATE_SUSPENDED so the main thread handle is
owned outright - no Toolhelp, no OpenThread, which is what defeated the first
version - then tight-loops Suspend/Wow64GetThreadContext/Resume and
accumulates over N compiles. It works: 333,991 C1 samples for m6only.c vs
163,140 for the control over 6 runs each, showing C1 does ~2x the work when
the inline expression is present.
Two traps caught by verification, not assumed:
- int3-padding "function starts" are labels, not entry points; five
top-ranked ones took zero breakpoint hits. Raw sampled EIPs are the valid
targets.
- the top differential EIPs are hot shared parser code: breakpointing the
five highest-excess raw EIPs fires in BOTH compiles at the 250-hit cap, so
the "excess" is largely volume, not expression specificity.
dbg32 now prints hit counts with 8 hex digits instead of 1 (250 printed as
"a" before), so differential counting is possible at all.
Requirements for a next attempt recorded: count-only mode with a raised cap,
a work-matched control (same expression, non-commutative operators), and
call-target attribution instead of int3 boundaries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c1_sampler.py profiles m6only.c against m6none.c (identical headers and
locals, cross product removed) so header parsing cancels and expression
building stands out. It collects zero samples today, diagnosed rather than
assumed: OpenThread fails for all four threads, and the loop gets a single
pass before the sub-second compile exits, so Toolhelp-based sampling from
64-bit Python is far too coarse either way.
Fix for both recorded: launch CL suspended via CreateProcess to hold the
thread handle directly (no Toolhelp, no OpenThread), then tight-loop and
accumulate across many compiles - the approach the older acc_sampler used.
Cheaper alternative also recorded: a differential BREAKPOINT profile with
dbg32, which already fires on C1, trading many runs for a proven harness.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Planting was hard-coded to C2.Dll at 0x10700000. It now matches the top 12
address bits of whichever module just loaded, so C1.Dll (0x10600000) and
C2.Dll breakpoints can be mixed in one dbg_bp.txt.
Measured on probes/m6only.c: C1.Dll loads before C2, four C1 breakpoints
plant, and two of them fire alongside a C2 control. The C front end runs
in-process and is debuggable with the same instrument - which is where the
frame walk pointed, having shown no C2 frame chooses between commutative
operands.
Scope recorded honestly: reaching C1 is not mapping it. C1 has none of the
anchors this file documents for C2, so locating the canonicaliser there is a
from-scratch effort over a 400KB .text, best started from a sampler profile
rather than guessed addresses.
Also records a discarded hypothesis: node[+0x34] is NOT a frame displacement
(two values matched member displacements by coincidence; across all 46
chain-consumer hits it decodes as pointers and unrelated constants). The
node-to-instruction correlation remains unsolved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chain-carrying node is opcode 0x275; of 73 static creation sites exactly
one executes (0x10760f80), reached past two operand tests. The branch there
leads to an alternate path (0x10760e84) that DOES order two operands
explicitly - and it takes zero hits in this compile, measured against a
30-hit control on the same run. All 30 constructions go through the same
node maker 0x10763ac3 with the order already fixed by the caller.
So across encoder, fld-node creation, chain-head consumer and arithmetic
build, no frame applies a rule choosing between commutative operands; each
propagates an order established upstream. That is a measured explanation for
the ~65 inert source shapes, including every operand text order.
Explicitly NOT claimed as below-source-visibility: the same compiler
produced January's opposite order, so something upstream differs - in
expression-tree shape or in the context that drives canonicalisation, above
everything mapped here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At 0x10736518-0x1073653d, one frame back from fld-node creation: the code
reads the operand chain from [node+0x28], calls the accessor on the first
element and keeps it, calls the same accessor on the linked second element
and DISCARDS the result, then hands the first to the node builder. Confirmed
live at 0x1073652c over 46 hits - edi/ebx is the chain head, itself a 0x24e
node with a symbol pointer at +0x18 and the link at +0.
So this frame applies no rule: the head of the chain becomes the load. The
commutative operand role is decided by the chain ORDER, fixed upstream when
the arithmetic node's operands are linked.
That explains the source-side result: ~65 shapes inert, including every
operand text order, because commutative operands are canonicalised into the
chain before this point. Only a change to the chain order can move it.
Open: which chain element is which source operand for the tied instruction
(needs the per-hit correlation). Next target: the code that builds
[node+0x28].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Arming dbg_armreg at node CREATION (not at the encoder, which is too late by
construction) catches the write. Route: FP opcode constants are immediates,
so creation sites are findable statically; of 186 candidates for 0x24e
exactly one fires for probes/m6only.c - 0x10707995, called from 0x10736542
in the per-node codegen region.
The operand pointer is written by two sites sharing one idiom: operand
producer 0x10708c34 -> node constructor 0x107019f4 -> opcode into [node+4],
class/reg word into [node+0xa] copied from another node, then the operand
pointer into BOTH [node+0x18] and [node+0x1c] (def/use pair). Stores at
0x1070db1d and 0x10710332. 0x107019f4 independently confirms the node
constructor address recorded in the earlier POGO work.
Caveat recorded in full: the watched slot was recycled by the arena, so the
captured stores build a fresh 0x24d node reusing that memory rather than
provably the tied instruction's node. This maps the creation path and the
write, not the specific execution; correlating a site execution to the tied
fld needs a per-hit sweep or a value filter in dbg_armreg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Against probes/m6only.c: encoder entry 0x107455e6 reads the node opcode from
[ebp+4] and range-checks the 0x249-0x25a band, establishing ebp as the IR
node (breakpoint 0x107455fc, after ebp is loaded). Byte dispatch 0x107455ad
via the table at 0x107455c0, store at 0x107455da, then emit_bytes 0x10750023
-> 0x10781bbc -> inlined copy 0x10781c18, with the code-buffer descriptor in
ebx (+0x0c length, verified 0x18 as the tied fld was appended).
Corrects the standing note on [0x1088b788]: it is the per-instruction
STAGING cursor, not the code buffer, which is why it looked wrong for
integer code.
Node view: hits repeat with period 18; exactly three nodes per pass carry an
operand pointer at +0x18, and those point at per-variable symbol nodes one
0x60 stride apart. Field differential between the a- and b-referencing nodes
recorded, reproduced across two independent captures. Which field encodes
the operand role is NOT decoded - +0x34 is explicitly falsified as a pure
displacement by the result node.
Settled: the encoder only renders node fields, so arming a watchpoint at
encode time can never catch the operand choice being made. It is fixed
upstream in lowering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DR0..DR3 are programmed when C2 loads, so dbg_wp.txt can only watch an
address known up front. Compiler-internal addresses are not: the arena base
shifts ~1MB per run, so IR nodes and the code buffer move (offsets stable,
base moves). Two new modes resolve the address while the compile runs:
dbg_armwp.txt "<hexpattern> @N +<hexoff> <len>" - at the Nth hit of bp0,
scan the debuggee for the pattern and watch match+off.
dbg_armreg.txt "<reg> <hexoff> <hit> <len>" - at that hit, take the
register from the trapping context and watch reg+off.
Used against probes/m6only.c this pins the write of the tied instruction's
displacement byte and the whole emission chain: encoder store 0x107455da
(mov [edx],al) advancing the emit cursor [0x1088b788]; its owner 0x107455e6
reads the node opcode from [ebp+4] and range-checks edx-0x249<=0x11, which
confirms the FP opcode band AND that ebp is the IR node there; then
emit_bytes 0x10750023 -> 0x10781bbc -> inlined byte copy 0x10781c18.
Settles the question the mapping was for: the encoder only renders node
fields, so the operand role is already fixed in the node graph before it
runs and the decision is upstream in lowering. dbg_armreg on the node's
operand pointer is the way in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dbg_run1.txt and dbg_scan.txt are per-run output, not source; the README
records the measurements taken from them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single-function target m6only.c isolates the cross-product operand tie.
Measured: the FP path runs through byte emitter 0x107455e6 (called from
0x10744452) with edi holding a live IR node; opcode at node[+4] (0x24e),
operand pointer at node[+0x18], member leaves one 0x60 stride apart. The
assembled function is findable in the arena by pattern scan - prologue at
buffer+0 and the tied fld at buffer+0x18 - so the deciding bytes are
addressable at run time. Node/buffer offsets are deterministic; only the
arena base shifts per run.
Negatives recorded: all previously-mapped FP decision sites (0x10745628,
0x10744304, 0x10735135) take zero hits here - they were mapped from an
ai_debug compile and are path-specific. And the emitter's hits are NOT
1:1 with FP instructions (4 of 12 displacement positions match), so an
earlier node differential keyed on hit order was invalid and is discarded.
Next instrument: runtime-resolved watchpoints in dbg32, watching the
displacement byte at buffer+0x1a to get the operand encoder.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
micro.c::micro_m6 is a 96-byte reproduction of the cross-product operand
tie; probe_cross.c reproduces the full caller context. probelab.py compiles
either with the unit's real flags and disassembles one function, so the
next session can iterate in seconds instead of minutes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Carry forward three Units reconstruction advances, complete hs_library_external at 36/36 target-owned functions and 3824/3824 padded bytes, and admit it as Matching.
Also harden parked-function validation for assembly transcriptions and enable MSVC header dependency tracking. Full 568-object builds and all 205 tooling tests pass; the parked manifest has zero stale or invalid entries.