11#!/usr/bin/env python3
2- """P-037 A18-0: the three-way decomposition of the eight B1 UNEXPLAINED rows.
2+ """P-037 A18: the three-way decomposition, A18-0 (the eight B1 UNEXPLAINED rows)
3+ and A18-1 (`--population`, `--selftest`: docs/notes/p037-a18-population-decomposition.md).
34
45Contract: docs/notes/p037-a18-legacy-decomposition.md (pre-registered in
56ccde27c). For exactly the eight rows committed in B1's evidence, from the
1516witness.
1617
1718Run: python scripts/p037_a18_decompose.py --out docs/evidence/p037-a18/eight-rows.json
19+ python scripts/p037_a18_decompose.py --selftest
20+ python scripts/p037_a18_decompose.py --population --out docs/evidence/p037-a18/population.json
1821"""
1922from __future__ import annotations
2023
2124import argparse
25+ import collections
26+ import contextlib
2227import copy
2328import json
2429import re
2530import subprocess
2631import sys
2732import tempfile
33+ from collections .abc import Iterator
2834from pathlib import Path
2935from typing import Any
3036
@@ -61,6 +67,11 @@ def names(op: Any, p: str) -> bool:
6167 return p in (op .get ("var" ), op .get ("src" ), op .get ("result" )) or p in (op .get ("args" ) or [])
6268
6369
70+ def ops_at (fn : Any , line : Any , p : str ) -> list [tuple [list [Any ], int ]]:
71+ return [(lst , i ) for lst , i in op_sites (fn ["body" ])
72+ if lst [i ].get ("line" ) == line and names (lst [i ], p )]
73+
74+
6475def locate (facts : Any , row : Any ) -> tuple [Any , Any , list [tuple [list [Any ], int ]]]:
6576 """The row's function, its one forwarding sidecar call, and the legacy ops on
6677 the parameter at that call's statement line."""
@@ -69,10 +80,7 @@ def locate(facts: Any, row: Any) -> tuple[Any, Any, list[tuple[list[Any], int]]]
6980 if any (a ["kind" ] == "param" and a .get ("source_param" ) == row ["ordinal" ]
7081 for a in c ["args" ])]
7182 call = calls [0 ] if len (calls ) == 1 else None
72- line = call ["statement_line" ] if call else None
73- ops = [(lst , i ) for lst , i in op_sites (fn ["body" ])
74- if lst [i ].get ("line" ) == line and names (lst [i ], row ["param" ])]
75- return fn , call , ops
83+ return fn , call , ops_at (fn , call ["statement_line" ] if call else None , row ["param" ])
7684
7785
7886def decompose (facts : Any , rep : Any , row : Any ) -> dict [str , Any ]:
@@ -115,7 +123,8 @@ def shape_probe(tree: Path, files: list[str], row: Any, line: int, work: Path) -
115123 facts , rep , _ = b1 .run_document (tree , files , work )
116124 finally :
117125 src .write_text (text )
118- ops = locate (facts , row )[2 ]
126+ fn = next (f for f in facts ["functions" ] if f ["name" ] == row ["method" ])
127+ ops = ops_at (fn , line , row ["param" ])
119128 return {"line_before" : old .strip (), "line_after" : lines [line - 1 ].strip (),
120129 "op_after" : ops [0 ][0 ][ops [0 ][1 ]]["op" ] if len (ops ) == 1 else None ,
121130 "actual_after" : coord (rep , row ["method" ], "index" , row ["index" ])["legacy" ]}
@@ -132,15 +141,221 @@ def witnessed(d: dict[str, Any], row: dict[str, Any]) -> bool:
132141 return False
133142
134143
144+ # ---------------------------------------------------------------------------
145+ # A18-1: the population gate
146+ # ---------------------------------------------------------------------------
147+
148+ LOCAL = {"release" : "must" , "use" : "no" } # a legacy op's own local value
149+
150+
151+ def reason_of (actual : str , canonical : str , mechs : set [str ], broken : bool ) -> str :
152+ """The normalization column: only the pre-registered reasons."""
153+ if broken :
154+ return "UNEXPLAINED"
155+ if canonical == actual :
156+ return "EQUAL"
157+ if len (mechs ) == 1 and mechs <= {"CONSUMES_PARAM_FOLD" , "ARGUMENT_SHAPE_LOSS" }:
158+ return next (iter (mechs ))
159+ return "UNEXPLAINED"
160+
161+
162+ def population (facts : Any , rep : Any , rows : list [dict [str , Any ]], probe : Any ,
163+ rewrite : bool = True ) -> list [dict [str , Any ]]:
164+ """One document: normalize every locally non-honest forwarding site of every
165+ comparable row, run the production MOS once, attribute through closures."""
166+ canon = copy .deepcopy (facts )
167+ fns = {f ["name" ]: f for f in canon ["functions" ]}
168+ info : dict [tuple [str , int ], dict [str , Any ]] = {}
169+ for r in rows :
170+ entry : dict [str , Any ] = {"broken" : False , "sites" : []}
171+ fn = fns [r ["method" ]]
172+ for c in (fn .get ("guarded_facts" ) or {}).get ("calls" , []):
173+ slots = [a ["param" ] for a in c ["args" ]
174+ if a ["kind" ] == "param" and a .get ("source_param" ) == r ["ordinal" ]]
175+ if not slots :
176+ continue
177+ ops , callee = ops_at (fn , c ["statement_line" ], r ["param" ]), coord (
178+ rep , c ["callee" ], "ordinal" , slots [0 ])
179+ if len (ops ) != 1 or callee is None :
180+ entry ["broken" ] = True
181+ continue
182+ lst , i = ops [0 ]
183+ kind , mech = lst [i ]["op" ], None
184+ if LOCAL .get (kind , callee ["legacy" ]) != callee ["legacy" ]:
185+ mech = ("CONSUMES_PARAM_FOLD" if kind == "release" else
186+ "ARGUMENT_SHAPE_LOSS" if probe (r , c ["statement_line" ]) else "UNKNOWN" )
187+ if rewrite : # the A18-0 honest positional forward
188+ lst [i ] = {"op" : "call" , "callee" : c ["callee" ], "sig" : c ["sig" ], "line" : c [
189+ "statement_line" ], "args" : ["_" ] * callee ["index" ] + [r ["param" ]]}
190+ entry ["sites" ].append ({"callee" : (c ["callee" ], callee ["index" ]), "op" : kind ,
191+ "line" : c ["statement_line" ], "mechanism" : mech })
192+ info [(r ["method" ], r ["index" ])] = entry
193+ after_rep = report (canon )
194+ out = []
195+ for r in rows :
196+ k = (r ["method" ], r ["index" ])
197+ seen , stack , mechs = {k }, [k ], set ()
198+ while stack :
199+ for site in info .get (stack .pop (), {"sites" : []})["sites" ]:
200+ mechs |= {site ["mechanism" ]} - {None }
201+ if site ["callee" ] not in seen :
202+ seen .add (site ["callee" ])
203+ stack .append (site ["callee" ])
204+ after = coord (after_rep , r ["method" ], "index" , r ["index" ])
205+ reason = reason_of (r ["actual" ], after ["legacy" ], mechs , info [k ]["broken" ])
206+ out .append ({** r , "canonical" : after ["legacy" ], "guarded_after" : after ["guarded" ],
207+ "semantic_class" : after ["class" ], "normalization_reason" : reason ,
208+ "closure_mechanisms" : sorted (mechs ), "sites" : info [k ]["sites" ],
209+ "equal_kind" : (("identity" if not mechs else "after_rewrite" )
210+ if reason == "EQUAL" else None )})
211+ return out
212+
213+
214+ def coverage (rows : list [dict [str , Any ]], expected : dict [str , dict [str , int ]],
215+ committed : list [dict [str , Any ]]) -> list [tuple [str , str ]]:
216+ """N1 (missing/duplicate) and N2 (drift) against B1's committed evidence."""
217+ bad = []
218+ keys = [(r ["doc" ], r ["method" ], r ["index" ]) for r in rows ]
219+ if len (set (keys )) != len (keys ):
220+ bad .append (("duplicate" , "a comparable row occurs twice" ))
221+ got : dict [str , collections .Counter [str ]] = collections .defaultdict (collections .Counter )
222+ for r in rows :
223+ got [r ["pop" ]][r ["b1_class" ]] += 1
224+ for pop in set (expected ) | set (got ):
225+ want = {c : n for c , n in expected .get (pop , {}).items () if c != "NO_GUARDED_EVIDENCE" }
226+ if dict (got [pop ]) != want :
227+ kind = "drift" if sum (got [pop ].values ()) == sum (want .values ()) else "missing"
228+ bad .append ((kind , f"{ pop } : { dict (got [pop ])} != B1 { want } " ))
229+ index = {(r ["doc" ], r ["method" ], r ["index" ]): r for r in rows }
230+ for u in committed :
231+ hit = index .get ((u ["doc" ], u ["method" ], u ["index" ]))
232+ if hit is None :
233+ bad .append (("missing" , f"committed row absent: { u ['doc' ]} { u ['method' ]} " ))
234+ elif (hit ["actual" ], hit ["guarded" ]) != (u ["legacy" ], u ["guarded" ]):
235+ bad .append (("drift" , f"committed row drifted: { u ['doc' ]} { u ['method' ]} " ))
236+ return bad
237+
238+
239+ @contextlib .contextmanager
240+ def frozen_tree () -> Iterator [tuple [Path , Path , dict [str , tuple [str , list [str ]]]]]:
241+ commit = b1 .frozen (b1 .POPULATION_COMMIT )
242+ with tempfile .TemporaryDirectory (prefix = "p037-a18-" ) as td :
243+ tree = Path (td ) / "tree"
244+ tree .mkdir ()
245+ archive = subprocess .run (["git" , "archive" , commit ], cwd = ROOT , capture_output = True ,
246+ check = True ).stdout
247+ subprocess .run (["tar" , "-x" , "-C" , str (tree )], input = archive , check = True )
248+ yield tree , Path (td ), {n : (pop , f ) for pop , n , f in b1 .documents (tree , commit )}
249+
250+
251+ def comparable (tree : Path , work : Path , name : str , pop : str , files : list [str ]
252+ ) -> tuple [Any , Any , list [dict [str , Any ]], Any ]:
253+ facts , rep , _ = b1 .run_document (tree , files , work )
254+ rows = [{"doc" : name , "pop" : pop , "method" : s ["method" ], "index" : s ["index" ],
255+ "ordinal" : s ["ordinal" ], "param" : s ["param" ], "actual" : s ["legacy" ],
256+ "guarded" : s ["guarded" ], "b1_class" : s ["class" ]}
257+ for s in rep ["summary" ] if s ["class" ] != "NO_GUARDED_EVIDENCE" ]
258+
259+ def probe (r : Any , line : int ) -> bool :
260+ p = shape_probe (tree , files , r , line , work )
261+ return bool (p ["op_after" ] == "release" and p ["line_after" ] != p ["line_before" ])
262+ return facts , rep , rows , probe
263+
264+
265+ def selftest () -> int :
266+ fails = 0
267+
268+ def check (label : str , ok : bool ) -> None :
269+ nonlocal fails
270+ print (f"{ 'ok ' if ok else 'MISS' } { label } " )
271+ fails += not ok
272+ with frozen_tree () as (tree , work , docs ):
273+ facts , rep , rows , probe = comparable (tree , work , "guard-forward-bare" ,
274+ * docs ["guard-forward-bare" ])
275+ outer = [r for r in rows if r ["method" ] == "ShapeForwardBare.Outer" ]
276+ on = population (facts , rep , outer , probe )[0 ]["normalization_reason" ]
277+ off = population (facts , rep , outer , probe , rewrite = False )[0 ]["normalization_reason" ]
278+ check (f"F1 a fold row without its rewrite stops being a fold ({ on } -> { off } )" ,
279+ on == "CONSUMES_PARAM_FOLD" and off != on )
280+ facts , rep , rows , probe = comparable (tree , work , "arg-cast-and-bang" ,
281+ * docs ["arg-cast-and-bang" ])
282+ cast = [r for r in rows if r ["method" ] == "ShapeCastBang.Cast" ]
283+ broken = copy .deepcopy (facts )
284+ fn = next (f for f in broken ["functions" ] if f ["name" ] == "ShapeCastBang.Cast" )
285+ for c in fn ["guarded_facts" ]["calls" ]:
286+ c ["statement_line" ] += 100
287+ got = population (broken , rep , cast , probe )[0 ]["normalization_reason" ]
288+ check (f"F2 a broken sidecar-to-forward link is UNEXPLAINED, not a fold ({ got } )" ,
289+ got == "UNEXPLAINED" )
290+ check ("F3 actual=unknown, canonical=must, no mechanism is UNEXPLAINED" ,
291+ reason_of ("unknown" , "must" , set (), False ) == "UNEXPLAINED" )
292+ sem = population (facts , rep , cast , probe , rewrite = False )[0 ]["semantic_class" ]
293+ check (f"F4 G=may vs L_canonical=no is semantic UNEXPLAINED ({ sem } )" ,
294+ sem == "UNEXPLAINED" )
295+ base = population (facts , rep , rows , probe )
296+ want = {"p037-shapes" : dict (collections .Counter (r ["b1_class" ] for r in base ))}
297+ check ("F5' the coverage check passes on a complete set" , not coverage (base , want , []))
298+ check ("F5 a deleted comparable row fails coverage" , bool (coverage (base [1 :], want , [])))
299+ check ("F6 a duplicated comparable row fails coverage" ,
300+ bool (coverage ([* base , base [0 ]], want , [])))
301+ print (f"RESULT: { 'all falsifiers fire' if not fails else f'{ fails } falsifier(s) missed' } " )
302+ return 1 if fails else 0
303+
304+
305+ def population_run (out_path : str ) -> int :
306+ ev = json .loads (EVIDENCE .read_text ())
307+ expected = {k .split ("/" , 1 )[1 ]: v for k , v in ev ["counts" ].items ()
308+ if k .startswith ("summary/" )}
309+ committed = [u for u in ev ["unexplained" ] if u ["level" ] == "summary" ]
310+ rows : list [dict [str , Any ]] = []
311+ with frozen_tree () as (tree , work , docs ):
312+ for name , (pop , files ) in docs .items ():
313+ facts , rep , comp , probe = comparable (tree , work , name , pop , files )
314+ if comp :
315+ rows += population (facts , rep , comp , probe )
316+ sem = collections .Counter (r ["semantic_class" ] for r in rows )
317+ norm = collections .Counter (r ["normalization_reason" ] for r in rows )
318+ cross = collections .Counter (f"{ r ['semantic_class' ]} x { r ['normalization_reason' ]} "
319+ for r in rows )
320+ cov = coverage (rows , expected , committed )
321+ drift = [msg for kind , msg in cov if kind == "drift" ]
322+ moved = [r for r in rows if r ["guarded_after" ] != r ["guarded" ]]
323+ verdict = ("VOID — B1 instrument drift" if drift else
324+ "FAIL — A18-1 COVERAGE BROKEN" if cov or moved else
325+ "FAIL — A18-1 NORMALIZATION MODEL INCOMPLETE" if norm ["UNEXPLAINED" ] else
326+ "FAIL — A18-1 SEMANTIC UNEXPLAINED" if sem ["UNEXPLAINED" ] else
327+ "PASS — A18 POPULATION DECOMPOSITION HOLDS" )
328+ out = {"schema" : "p037-a18-population/1" , "population_commit" : b1 .frozen (b1 .POPULATION_COMMIT ),
329+ "instrument_commit" : b1 .git ("rev-parse" , "HEAD" ).strip (),
330+ "comparable_expected" : sum (n for v in expected .values () for c , n in v .items ()
331+ if c != "NO_GUARDED_EVIDENCE" ),
332+ "covered" : len (rows ), "coverage_problems" : cov , "guarded_moved" : len (moved ),
333+ "semantic" : dict (sem ), "normalization" : dict (norm ),
334+ "equal_kind" : dict (collections .Counter (r ["equal_kind" ] for r in rows
335+ if r ["equal_kind" ])),
336+ "cross_tab" : dict (cross ), "rows" : rows , "result" : verdict }
337+ Path (out_path ).parent .mkdir (parents = True , exist_ok = True )
338+ Path (out_path ).write_text (json .dumps (out , indent = 1 ) + "\n " , encoding = "utf-8" )
339+ print (json .dumps ({k : v for k , v in out .items () if k != "rows" }, indent = 1 ))
340+ print (f"RESULT: { verdict } " )
341+ return 0 if verdict .startswith ("PASS" ) else 1
342+
343+
135344def main (argv : list [str ]) -> int :
136345 ap = argparse .ArgumentParser (description = __doc__ ,
137346 formatter_class = argparse .RawDescriptionHelpFormatter )
138- ap .add_argument ("--out" , required = True )
347+ ap .add_argument ("--out" )
348+ ap .add_argument ("--population" , action = "store_true" )
349+ ap .add_argument ("--selftest" , action = "store_true" )
139350 args = ap .parse_args (argv )
351+ if args .selftest :
352+ return selftest ()
140353 commit = b1 .frozen (b1 .POPULATION_COMMIT )
141- if b1 .git ("status" , "--porcelain" , "--untracked-files=no" ).strip ():
142- print ("REFUSED: the instrument tree is dirty; commit it first" )
354+ if b1 .git ("status" , "--porcelain" , "--untracked-files=no" ).strip () or not args . out :
355+ print ("REFUSED: needs --out and a clean instrument tree ( commit it first) " )
143356 return 2
357+ if args .population :
358+ return population_run (args .out )
144359 b1_rows = [u for u in json .loads (EVIDENCE .read_text ())["unexplained" ]
145360 if u ["level" ] == "summary" ]
146361 rows : list [dict [str , Any ]] = []
0 commit comments