Replace per-edit unsafety ratchet with reviewed net-decrease policy - #167
Closed
thedataking wants to merge 23 commits into
Closed
Replace per-edit unsafety ratchet with reviewed net-decrease policy#167thedataking wants to merge 23 commits into
thedataking wants to merge 23 commits into
Conversation
thedataking
force-pushed
the
perl/optimize-agent-loop
branch
from
July 29, 2026 11:42
23e8371 to
af680f2
Compare
thedataking
force-pushed
the
perl/optimize-agent-loop
branch
from
July 29, 2026 12:03
af680f2 to
4552934
Compare
thedataking
changed the base branch from
perl/find-unsafe2-hardening
to
perl/ffi-pregate-recovery
July 29, 2026 12:16
Each case is a rewrite that lowers or preserves the unsafe count without making the code safer: laundering helpers, count compression, export-set edits, closure rekeying, and similar shapes. Several were observed from the rewriter during safety-loop runs; the rest close channels found by review.
… inventory A crate with no baseline JSON is held to an all-zero inventory rather than skipped, so unsafe code can't be hidden in a newly added crate.
… set Only a per-function entry-point boolean was recorded, so deleting an exported fn or static, demoting it, or editing an #[export_name] value was invisible to the checker. The exported symbol set is the ABI contract; record each item's symbol and reject any change.
Entry points are exempt from the unsafety metrics, so routing work through an exported wrapper hides unsafe code behind the exemption. The FFI rules already forbid such calls; tracking them per function lets the checker reject the move deterministically instead of relying on the LLM review to notice.
Inline asm can access arbitrary memory but fed no metric, so rewriting unsafe operations into asm! blocks would lower the count without making anything safer.
An `unsafe impl` (e.g. `unsafe impl Send`) is a zero-code soundness assertion no body metric sees, so a rewrite could trade counted unsafety for an uncounted promise. c2rust never emits one, so any occurrence was added by refactoring; reject new ones until a review-gated relaxation exists.
rustc reports closures as synthetic function entries. Treating those entries as independent baseline items caused safety-preserving zlib rewrites to be rejected: closures introduced inside FFI entry points did not inherit the entry-point exemption, and raw-pointer types in local closure signatures were treated as new named signature surface. Fold closure-like entries into their enclosing named item before comparison. Sum body-derived metrics (derefs, unsafe calls, asm, uses, and casts), so moving unsafe operations into a closure does not hide them. Keep declaration properties (qualifiers, signature pointer counts, and export status) from the enclosing item only. This lets closures inside FFI entry points inherit their exemption and avoids charging local closure signatures as named API surface.
casts_int_to_ptr only counted `as` casts, so the same usize round-trip spelled as with_exposed_provenance() and friends went uncounted. Charge the std APIs that materialize a pointer from an address the same as the cast they replace.
…natures sig_contains_raw_ptr is non-increasing, so wrapping a pointer in a local struct and passing that would zero the count with no real progress. Descend local Adts (and charge NonNull directly); std types are not descended since they reach NonNull internally without being the function's own unsafety surface.
thedataking
force-pushed
the
perl/ffi-pregate-recovery
branch
from
August 5, 2026 09:14
a935a7f to
e2478e5
Compare
thedataking
force-pushed
the
perl/optimize-agent-loop
branch
from
August 5, 2026 09:23
4552934 to
774b4ca
Compare
Rejected-review findings previously lived only in process memory, so a restarted run repeated attempts the reviewer had already rejected. Rebuild the sticky reminder list from FAIL reviews in the op_history reflog at startup, and record the deterministic pre-gate's rejections the same way, so both kinds of rejection are equally durable.
thedataking
force-pushed
the
perl/optimize-agent-loop
branch
from
August 6, 2026 09:59
774b4ca to
7f18616
Compare
thedataking
force-pushed
the
perl/ffi-pregate-recovery
branch
from
August 6, 2026 09:59
e2478e5 to
17eb005
Compare
…oint All 30 FFI-review rejections in the 2026-08-06 zlib run were one move: unsafe work relocated into an exported wrapper, where the count exemption hides it. The review catches this only after the full step is spent (agent run, tests, review: ~3.5 min discarded each time). The relocation is mechanically visible in the metrics, so say so at check time: the agent runs check-unsafe2 mid-turn and can self-correct before the step is judged. Warning only -- legitimate refactors also grow wrapper conversion code -- and phrased to not match the tolerated-warning regexes in crisp, so it alerts the agent without also triggering a tolerated-move review; the FFI review stays the judge.
…oints Charging every new function for struct-carried pointer signatures (3cd5cde) made the grouped raw-free state migration a serial prerequisite for all decomposition: 25 of run 6's 34 blocked steps cite it, and throughput fell to ~54 ops/h against run 3's ~300 at the same count depth. An unsafe fn self-counts, so letting it carry raw-pointer signatures cannot create a terminal hiding place; requiring a pointer-free signature to create a safe fn or drop an unsafe qualifier closes the create-then-flip laundering route that a plain revert would reopen. Revert if the next run does not at least double ops/attempt.
thedataking
force-pushed
the
perl/optimize-agent-loop
branch
from
August 7, 2026 08:05
7f18616 to
25500fb
Compare
The step prompt names only the crate-wide total; the per-function inventory sits in the sandbox JSON, which the zlib run-7 agent consulted once in 430 iterations. Its final 20 iterations all worked a gzip cluster holding 78 of 2139 remaining operations while 95% of them sat in four files it never opened. Summarize per-file totals and the largest functions into the prompt so each iteration's target choice starts from the mass distribution rather than the plan's current fixation.
A failure streak means the agent's own target choice is not working: zlib run 7 ended with twenty consecutive no-progress iterations inside a 78-op gzip cluster because the plan's dead-end notes had fenced off everything else, while 95% of the remaining operations sat in files the agent never opened. After retarget_after consecutive failures, override plan-driven choice with an explicit target: the largest function in the file carrying the most unsafe operations. Ranking files before functions matters because unsafety mass and function size diverge: deflate.rs holds the most remaining operations (947) spread across many mid-size functions (largest 186), and a per-function argmax would starve it indefinitely behind inflate::inflate (503). The file is also the unit where reductions compound -- helpers and slice views built for one function are reused by its neighbors -- so successive retargets should land in one neighborhood rather than chase the momentary biggest function across files. An accepted reduction resets the streak and returns the choice to the agent; waived functions are settled and never targeted.
The stall suffix offers a net-neutral preparatory step as the fallback, and the loop counts such steps as failures; in zlib run 7 the agent farmed that escape hatch for 16 of the 20 green, reviewed, accepted iterations that ended the run. Preparation is the right call early in a streak, so allow neutral_steps_per_streak of them; past that the suffix withdraws the invitation and demands a removal or an explicit BLOCKED, so a genuine dead end surfaces in a few iterations instead of grinding to the stop.
zlib run 7's plan merged four clusters into one prohibition -- do not make helpers over ABI-shaped state safe -- although the checker had only rejected specific gz_state/z_stream probes, and the hygiene rewrite preserved the overreach while the plan's four identically worded Next rows never moved. Require dead-end notes to stay scoped to the functions and types their gate actually fired on, at both the step that writes them and the rewrite that merges them, and give the hygiene rewriter the current inventory so remaining work is reordered by where the operations are.
thedataking
force-pushed
the
perl/ffi-pregate-recovery
branch
from
September 5, 2026 00:35
17eb005 to
2bf487a
Compare
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
check-unsafe2enforced a ratchet per function and per metric: no unsafety metric could increase anywhere, even when the crate-wide result improved. That rule could not distinguish adding unsafe code from relocating it into a smaller, explicit boundary, so legitimate decompositions were rejected in flight and the rewriter often spent its budget reverting them or landing no-ops.The new policy judges the bounded net effect, reviews what counting cannot decide, and gives stalled runs an honest way to stop:
unsafe fn, while a new or newly-safe function must have a pointer-free signature.BLOCKED:result instead of pressuring the rewriter into cosmetic or rule-breaking edits. Dead-end notes must cite the exact checker diagnostic, review finding, or failing test, and oversized plans can be compacted without changing their remaining work.