Skip to content

#131: uniform Result<T, PyError> exception-link lowering (exceptiontransform.py parity) - #313

Merged
youknowone merged 26 commits into
mainfrom
rtyper-legacy
Jul 2, 2026
Merged

#131: uniform Result<T, PyError> exception-link lowering (exceptiontransform.py parity)#313
youknowone merged 26 commits into
mainfrom
rtyper-legacy

Conversation

@youknowone

@youknowone youknowone commented Jun 30, 2026

Copy link
Copy Markdown
Owner

#131

Summary

Ports RPython's shape-agnostic exception transform (rpython/translator/exceptiontransform.py) to pyre's JIT front-end, replacing the shape-specific ?-diamond matcher in majit-translate/src/front/result_exc.rs. The JIT erases the materialised Result<T, PyError> shell that Charon lowers from Rust source, turning it into bare-T returns with out-of-band exception propagation (ExitSwitch::LastException) — matching the residual-call ABI the runtime already uses.

The end state is transform_completely parity: every Result<T, PyError> callee is transformed (the callee-side type gate tyref_is_result_of_pyerror is the only filter), with no per-callee allowlist. Shapes the rewrite does not yet recognise degrade fail-safe to a residual call (no miscompile), exactly as exceptiontransform.py:212 leaves an un-rewritable graph to the residual-call ABI.

What changed

  • Uniform flip (103b32223c): deleted the RESULT_EXC_LOWERING_SCOPE allowlist, in_execute_wrapper_family, the PYRE_RESULT_EXC_UNIFORM env gate, and the strict pure-forward verifier. The exception-link lowering is now unconditional.
  • Flip-blocker fix (151a8ab659): reconcile a direct call's RESULT against a void-declared callee (the caller-side mirror of the callee-side unit-return widening), so a Result<(), PyError> call site whose aggregate front::mir types Ref no longer panics against a Void-declared callee (call.py:220 parity).
  • Consumed-intermediate ctor skip (bc1c240ce5, e4a4f0ac88): a Result ctor consumed locally — including a ctor block whose conditional exit switches on the ctor's own __discriminant (the __new__ wrapper's match { Ok(obj) } { Ok(o)=>o, Err(e)=>.. }) — is left materialised as an ordinary ADT rather than declining the whole callee.
  • Source-parity discard cleanup (1e9cfeca55, 650272596e): replaced let _ = <fallible call> discards (a non-RPython idiom) with the parity-correct shape — infallible setdictvalue for private-slot stores, or ?-propagation where the error must surface (function_setdict also fixes a latent __dict__-key-store bug).
  • Supporting: to_exc_object residual annotation, SSA phi-collapse restrictions pending SSA_to_SSI, dont_look_inside on JIT-driver / slow-path machinery, and a String/str/Wtf8 as_bytes receiver alias.

Result

Whole-program result-exc declines reduced to 2 (sync_python_sys_path, the _io.TextIOWrapper.__exit__ discard-Ok closure), both fail-safe to residual.

Validation

python3 pyre/check.py: dynasm 166/166 + cranelift 166/166 (2/2 backends, zero regression).

Remaining (toward deleting the legacy walker — declines → 0)

  • sync_python_sys_path: needs a direct module-dict infallible store (setdictvalue is a no-op for modules; the setattr_str module branch uses w_module_get_w_dict).
  • _io.TextIOWrapper.__exit__: a legitimate discard-Ok needing the general call-site forward-substitution rule.
  • SSA_to_SSI port for cross-block annotation threading.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for lowering bool.then(...) closures into short-circuit branching, improving closure-based control flow handling.
    • Expanded support for a broader set of Result<_, PyError> call paths during translation.
  • Bug Fixes

    • Improved handling of return values and void-style call results to prevent mismatches in generated code.
    • Reduced silent failures by propagating more runtime write errors and tightening several internal object updates.
    • Kept some optimizations from over-collapsing values, preserving correct behavior in live code paths.
  • Tests

    • Added end-to-end coverage for the new bool.then lowering behavior.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 792fb1f7-7075-40e4-a32b-5db5c8c49ca2

📥 Commits

Reviewing files that changed from the base of the PR and between 126151d and 2a0af06.

📒 Files selected for processing (36)
  • majit/charon-corpus/corpus.ullbc
  • majit/charon-corpus/src/lib.rs
  • majit/majit-charon-reader/tests/corpus.rs
  • majit/majit-translate/src/annotator/annrpython.rs
  • majit/majit-translate/src/annotator/unaryop.rs
  • majit/majit-translate/src/codewriter/call.rs
  • majit/majit-translate/src/codewriter/jtransform.rs
  • majit/majit-translate/src/front/bool_then.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/src/front/mod.rs
  • majit/majit-translate/src/front/result_exc.rs
  • majit/majit-translate/src/lib.rs
  • majit/majit-translate/src/model.rs
  • majit/majit-translate/src/translator/rtyper/cutover.rs
  • majit/majit-translate/tests/test_mir_frontend.rs
  • pyre/pyre-interpreter/src/_structseq.rs
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-interpreter/src/error.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/_collections/mod.rs
  • pyre/pyre-interpreter/src/module/_csv/mod.rs
  • pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs
  • pyre/pyre-interpreter/src/module/pyexpat/mod.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-interpreter/src/objspace/std/mapdict.rs
  • pyre/pyre-interpreter/src/pyopcode.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/gc_interp.rs
  • pyre/pyre-object/src/gc_roots.rs
  • pyre/pyre-object/src/longobject.rs

Walkthrough

This PR adds a bool::then-to-Option short-circuit rewrite pass in majit-translate, removes the allowlist-based scoping for Result<T, PyError> exception lowering in favor of a type-gated approach, seeds annotator return-variable bindings before rtyping, reconciles void call result types, and removes phi all-equal collapse. Separately, it migrates numerous pyre-interpreter attribute writes to direct dict-value calls with propagated errors, and adds JIT dont_look_inside tracing boundaries across pyre-interpreter, pyre-jit, and pyre-object.

Changes

majit-translate compiler changes

Layer / File(s) Summary
bool::then closure short-circuit rewrite
majit/majit-translate/src/front/bool_then.rs, majit/majit-translate/src/front/mod.rs, majit/majit-translate/src/front/mir.rs, majit/charon-corpus/src/lib.rs, majit/majit-charon-reader/tests/corpus.rs, majit/majit-translate/tests/test_mir_frontend.rs
Adds BoolThenSite recognition and rewire_bool_then_call_sites, wires site recording/rewrite into mir.rs, registers the module, and adds a corpus fixture plus e2e/reader tests validating the diamond rewrite.
Result<T, PyError> exception-link scoping removal
majit/majit-translate/src/front/result_exc.rs, majit/majit-translate/src/front/mir.rs
Removes RESULT_EXC_LOWERING_SCOPE/in_result_exc_scope, adds verify_forwards_to_returnblock_general and a use-count-based well-formedness check, and changes the coverage gate to degrade unsupported functions to residual calls.
Annotator return-var seeding
majit/majit-translate/src/annotator/annrpython.rs, majit/majit-translate/src/translator/rtyper/cutover.rs, majit/majit-translate/src/lib.rs
Adds force_return_var_annotation/seed_all_annotated_return_vars, invoked from the rtyper cutover prepass, with related comment updates.
Void call-result reconciliation and PyError to_exc_object
majit/majit-translate/src/codewriter/call.rs, majit/majit-translate/src/codewriter/jtransform.rs, majit/majit-translate/src/annotator/unaryop.rs
Adds declared_result_type_for_target/effective_call_result_ty for residual void reconciliation, and a new pyerror_method_to_exc_object builtin method path.
Phi collapse removal
majit/majit-translate/src/model.rs
Removes all-equal phi collapse from remove_duplicate_inputargs/simplify_phis and updates the related test.

Estimated code review effort: 4 (Complex) | ~60 minutes

pyre-interpreter dict writes and JIT tracing boundaries

Layer / File(s) Summary
Core setdict/setattr error propagation
pyre/pyre-interpreter/src/_structseq.rs, baseobjspace.rs, builtins.rs, call.rs, error.rs, function.rs, importing.rs
Widens setdictvalue visibility and propagates previously-ignored setdict/setattr_str/setitem/delitem errors.
executioncontext.rs frame stub writes
pyre/pyre-interpreter/src/executioncontext.rs
Switches frame-stub field writes from setattr_str to setdictvalue.
Module-level setdictvalue migration
pyre/pyre-interpreter/src/module/_collections/mod.rs, _csv/mod.rs, _weakref/interp__weakref.rs, pyexpat/mod.rs
Switches instance/namespace field writes to setdictvalue and adds a dont_look_inside annotation.
sys module vm.rs migration
pyre/pyre-interpreter/src/module/sys/vm.rs
Switches frame stub, implementation, monitoring, and stream attribute writes to setdictvalue.
dont_look_inside tracer boundaries
pyre/pyre-interpreter/src/objspace/std/mapdict.rs, pyopcode.rs, jit_fnaddr.rs, pyre/pyre-jit/src/call_jit.rs, eval.rs, pyre/pyre-object/src/gc_interp.rs, gc_roots.rs, longobject.rs
Adds dont_look_inside annotations to numerous helpers and registers their fnaddrs/static addresses.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MirLowering as Lowering::lower_call
  participant BoolThenRecognizer as recognize_bool_then_site
  participant BoolThenModule as rewire_bool_then_call_sites
  participant BlockA
  participant ThenArm as then_bb
  participant ElseArm as else_bb
  participant BlockB

  MirLowering->>BoolThenRecognizer: detect bool::then(cond, closure)
  BoolThenRecognizer-->>MirLowering: BoolThenSite
  MirLowering->>BoolThenModule: bool_then_sites at finish()
  BoolThenModule->>BlockA: validate then-call shape
  BoolThenModule->>ThenArm: emit call_once + Option::Some
  BoolThenModule->>ElseArm: emit Option::None
  BoolThenModule->>BlockA: replace call with branch on cond
  ThenArm->>BlockB: goto with Some(payload)
  ElseArm->>BlockB: goto with None
Loading

Possibly related PRs

  • youknowone/pyre#165: Introduces the front::result_exc exception-lowering machinery that this PR directly reworks by removing its scoping allowlist and adding a general forward verifier.

Poem

A rabbit hops through diamond gates,
then becomes Some or None awaits,
Dicts now written straight and true,
No tracer peeks where it shouldn't view,
Hop, hop — the build compiles anew! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the PR’s main change: uniform Result<T, PyError> exception-link lowering.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rtyper-legacy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit e86c757).

1. Regressions to PyPy parity introduced by this patch

  • majit/majit-translate/src/model.rs:3681 ↔ rpython/translator/simplify.py:561: our patch omits the all-equal phi collapse ("is omitted here"), while RPython does if all_equal(new_args) and not isspecialvar(new_args[0]): uf.union(new_args[0], input). upstream/main had this collapse for variable phis, so this regresses parity.

  • majit/majit-translate/src/front/mir.rs:885 ↔ rpython/translator/exceptiontransform.py:212: our patch returns Ok(SemanticProgram { ... }) after unrecognised MIR lowering failures, while RPython’s transform_completely() iterates every graph and calls create_exception_handling(graph). upstream/main failed the build for unrecognised lowering failures; silently degrading them to residual calls is less faithful.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/builtins.rs:4184 ↔ pypy/module/exceptions/interp_exceptions.py:268: our BaseException.add_note now does setattr_str(w_self, "__notes__", fresh)?, so attribute-setting failure propagates. PyPy bypasses attribute protocol and writes the exception dict directly with space.setitem(w_dict, w_attr, space.newlist([w_note])).

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-interpreter/src/baseobjspace.rs:1132 ↔ pypy/objspace/std/typeobject.py:1014: our __set_name__ failure path re-raises the original exception after best-effort add_note (Err(e)), while PyPy creates and raises a new RuntimeError and chains the original cause.

  • pyre/pyre-interpreter/src/builtins.rs:4163 ↔ pypy/module/exceptions/interp_exceptions.py:258: our BaseException.add_note TypeError says "note must be a string", while PyPy reports "note must be a str, not %T" with the offending type.

4. Structural adaptations

  • majit/majit-translate/src/front/bool_then.rs:12 ↔ rpython/flowspace/flowcontext.py:747: our Rust frontend synthesizes a bool::then short-circuit Option diamond because Charon cannot extract core::bool::<Impl>::then; RPython flowspace observes Python bytecode branches directly via guessbool(...). This is a Rust/Charon extraction adaptation, not a PyPy source construct.

  • pyre/pyre-object/src/gc_roots.rs:94 ↔ rpython/memory/gctransform/framework.py:790: our pin_root is marked #[dont_look_inside] around a thread-local Rust shadow stack, while RPython’s GC transform emits push_roots(hop) / pop_roots(hop, livevars) around allocations. The root mechanism is necessarily adapted to pyre’s runtime representation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 650272596e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2791 to +2792
let effective_result_ty =
self.effective_call_result_ty(target, op.result.as_ref(), result_ty);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile void Result calls before inlining

When a transformed Result<(), PyError> callee is a registered graph, guess_call_kind classifies it as Regular (or recursive) and the branch above still calls handle_regular_call(..., result_ty, ...), so it emits/stamps an inline_call_r from the front-end Ref unit shell even though the callee jitcode is later widened to a void return. This new reconciliation only runs in the residual branch, so any inlined foo()?/discarded-unit call to a Result<(), PyError> helper still has a caller/callee result-kind mismatch and can read a non-existent/garbage ref slot; apply the same effective result type before regular/recursive call lowering too.

Useful? React with 👍 / 👎.

@youknowone
youknowone force-pushed the rtyper-legacy branch 2 times, most recently from 3310350 to e8b97e6 Compare July 1, 2026 08:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8b97e6562

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +829 to +833
eprintln!(
"[result-exc] {} function(s) declined the exception-link \
lowering (fail-safe → residual); shape-coverage gap:{detail}",
regressions.len()
)));
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the MIR lowering regression failure

This branch now only logs and then returns Ok even though regressions contains every skipped function whose error is not the known uninitialised-local gap; the skipped bucket is filled from all lower_fun_decl_with_static_addrs failures, not just fail-safe result-exception declines. When an unrelated new lowering error appears, its graph is silently omitted from the SemanticProgram and calls fall back to residual handling, masking translation coverage/perf regressions that this gate previously stopped.

Useful? React with 👍 / 👎.

Comment on lines +738 to +747
if cur != from_block {
for op in &block.operations {
if op_operand_vars(&op.kind).iter().any(|o| o == &v) {
return Err(format!(
"{}: Result shell alias is read by an operation in \
block {cur} on the forwarding path — not a pure forward",
graph.name
));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Err-return forwarding blocks pure

This verifier now accepts forwarding blocks that contain operations as long as those operations do not read the Result shell, but lower_result_exc_returns rewrites an Err shell by calling set_raise_values in the producer block, which replaces the producer's normal exit. In a Result callee where an Err value is threaded through a cleanup/logging/counter block before returning, those accepted downstream operations are skipped and the JIT raises earlier than the interpreter; keep the old pure-forwarder requirement for Err returns or emit the raise at the original return edge.

Useful? React with 👍 / 👎.

Comment on lines +5014 to +5015
if args.len() == 1 && self.is_string_as_bytes_identity(&reg) {
self.local_var[dest_local] = Some(args[0].clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not alias byte-slice views for indexed uses

This aliases every String/str/Wtf8::as_bytes() result to the original string object. For translated code that indexes or casts the byte slice, such as pyre-interpreter/src/type_methods.rs using as_bytes()[0] == b'0', the adapter lowers the later ArrayRead to getitem on StringRepr, whose rstr path returns a Char rather than the u8/Signed value the Rust source expects. That can either fail rtyping or give character semantics to byte arithmetic; gate this identity to known len/equality consumers or lower a real byte-slice representation.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

target: CallTarget::method("from_exc_object", Some("PyError".to_string())),

P1 Badge Type the rewrap conversion for the annotator

When catch_and_rewrap handles a custom match consumer, this synthetic Method call is resolved statically by the codewriter, but the flowspace adapter still lowers CallTarget::Method for annotation as getattr(args[0], "from_exc_object") followed by simple_call. Here args[0] is the caught W_BaseException, and this patch only adds an annotator override for PyError.to_exc_object, so rewrapped sites have no from_exc_object attribute to annotate and will block/skip instead of preserving the custom match path; add a matching annotator special case or emit a target the annotator can resolve.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +401 to +402
if !well_formed_return || !forwards_ok {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject malformed Result returns before partial rewrite

When one real Result<T, PyError> return has any extra use of the shell before it reaches returnblock (for example the temporary is passed to a helper/logging path and then returned) and another return in the same callee rewrites cleanly, this continue leaves that shell-return path unlowered while rewritten > 0 makes the callee succeed as transformed. Callers are then rewired to receive T/exception instead of a Result, so the skipped path can return a Result object where the caller expects the unwrapped payload; only skip ctors after proving they do not reach returnblock, otherwise decline/fail the whole callee.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73eb77b454

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

kind: OpKind::Call {
target: CallTarget::method("call_once", Some(site.call_once_owner.clone())),
args: vec![env_in_then, unit],
result_ty: site.payload_ty.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle unit bool::then payloads as void

When the lifted call is cond.then(|| ()) (Option<()>), site.payload_ty is derived through tyref_to_value_type, which intentionally treats unit in operand position as Ref(None), but the synthesized call_once here is a function call whose unit return should be Void like the ordinary lower_call is_unit_type special case. This makes the new diamond emit a ref-returning call_once and then build Some from that non-existent ref slot, so unit-returning closures can produce a bogus payload/register instead of the payload-less Some(()) shape; special-case unit payloads before stamping the call result and Some::__pos_0 write.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 126151dbd8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

graph.name
));
}
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject conditional return shells before skipping

When a Result shell has an extra use and a conditional exit but one arm still forwards it to returnblock, well_formed_return is false so this continue runs before the reachability guard below. Fresh evidence is this earlier conditional-exit skip: in a shape like let r = Ok(x); if cond { log(&r) }; r, another return in the same callee can still be rewritten, leaving this path returning a materialized Result after callers were rewired to expect the unwrapped payload/exception. Treat non-well-formed conditional shells that reach returnblock as a whole-callee decline instead of a consumed intermediate.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pyre/pyre-interpreter/src/module/sys/vm.rs (1)

57-150: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Discarded setdictvalue boolean across sys.namespace initialization.

Every setdictvalue call in build_frame_stub_chain, make_traceback_frame_stub, and register_module (e.g. Lines 86-106, 131-144, 530-596, 618-686) discards the returned bool. This is currently safe because sys_namespace_type() explicitly sets hasdict = true (Lines 19-28), but there's no defensive check tying the two together — a future change to sys_namespace_type (or reuse of these helpers with a different type) would silently drop sys-module fields instead of failing loudly.

🛡️ Example defensive pattern
-crate::baseobjspace::setdictvalue(stub, "f_code", pycode);
+debug_assert!(
+    crate::baseobjspace::setdictvalue(stub, "f_code", pycode),
+    "sys.namespace stub must have a dict slot"
+);

Also applies to: 259-1018

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/sys/vm.rs` around lines 57 - 150, The
sys.namespace initialization code is ignoring the boolean result from
setdictvalue in build_frame_stub_chain, make_traceback_frame_stub, and
register_module, which can silently drop fields if the object stops supporting
dict storage. Update these helpers to check the return value of each
setdictvalue call and surface an explicit failure path instead of assuming
success, using the existing symbols build_frame_stub_chain,
make_traceback_frame_stub, register_module, and sys_namespace_type to keep the
behavior tied to the namespace type contract.
majit/majit-translate/src/front/mir.rs (1)

862-883: 📐 Maintainability & Code Quality | 🔵 Trivial

Coverage gate now degrades silently — consider an opt-in strict mode for CI.

The regression bucket switched from build-failure to an unconditional eprintln + continue, so a genuinely unrelated new lowering decline degrades silently to a residual call (correctness is preserved; only JIT coverage/perf regresses). The comment already delegates the safety net to check.py. As defense-in-depth, consider gating a hard failure behind an opt-in env var mirroring the existing PYRE_MIR_FRAMESTATE_STRICT pattern, so CI can fail loud on new regressions while production keeps degrading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@majit/majit-translate/src/front/mir.rs` around lines 862 - 883, The
regression handling in mir.rs now only logs with eprintln! and continues, so add
an opt-in strict failure mode for CI while keeping the current fail-safe
fallback for production. Update the regressions block in the MIR coverage path
to check a dedicated environment variable, mirroring the existing
PYRE_MIR_FRAMESTATE_STRICT pattern, and when enabled, turn the unrecognised MIR
shape regression into a hard error instead of degrading silently to residual.
Keep the current diagnostics and continue behavior unchanged when the strict
flag is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-interpreter/src/builtins.rs`:
- Around line 7276-7282: The `seek` path is still updating the `__file_pos__`
slot through a separate `setattr_str(...)` write instead of the shared
`file_set_pos` helper. Update `seek` to call `file_set_pos` for setting the file
position so the `__file_pos__` state uses one consistent code path and
semantics, matching the newly documented store behavior in `file_set_pos`.

---

Outside diff comments:
In `@majit/majit-translate/src/front/mir.rs`:
- Around line 862-883: The regression handling in mir.rs now only logs with
eprintln! and continues, so add an opt-in strict failure mode for CI while
keeping the current fail-safe fallback for production. Update the regressions
block in the MIR coverage path to check a dedicated environment variable,
mirroring the existing PYRE_MIR_FRAMESTATE_STRICT pattern, and when enabled,
turn the unrecognised MIR shape regression into a hard error instead of
degrading silently to residual. Keep the current diagnostics and continue
behavior unchanged when the strict flag is absent.

In `@pyre/pyre-interpreter/src/module/sys/vm.rs`:
- Around line 57-150: The sys.namespace initialization code is ignoring the
boolean result from setdictvalue in build_frame_stub_chain,
make_traceback_frame_stub, and register_module, which can silently drop fields
if the object stops supporting dict storage. Update these helpers to check the
return value of each setdictvalue call and surface an explicit failure path
instead of assuming success, using the existing symbols build_frame_stub_chain,
make_traceback_frame_stub, register_module, and sys_namespace_type to keep the
behavior tied to the namespace type contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f3ec32b4-b097-46d0-aacd-8bd7f882edec

📥 Commits

Reviewing files that changed from the base of the PR and between 502c4f1 and 126151d.

📒 Files selected for processing (36)
  • majit/charon-corpus/corpus.ullbc
  • majit/charon-corpus/src/lib.rs
  • majit/majit-charon-reader/tests/corpus.rs
  • majit/majit-translate/src/annotator/annrpython.rs
  • majit/majit-translate/src/annotator/unaryop.rs
  • majit/majit-translate/src/codewriter/call.rs
  • majit/majit-translate/src/codewriter/jtransform.rs
  • majit/majit-translate/src/front/bool_then.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/src/front/mod.rs
  • majit/majit-translate/src/front/result_exc.rs
  • majit/majit-translate/src/lib.rs
  • majit/majit-translate/src/model.rs
  • majit/majit-translate/src/translator/rtyper/cutover.rs
  • majit/majit-translate/tests/test_mir_frontend.rs
  • pyre/pyre-interpreter/src/_structseq.rs
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-interpreter/src/error.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/_collections/mod.rs
  • pyre/pyre-interpreter/src/module/_csv/mod.rs
  • pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs
  • pyre/pyre-interpreter/src/module/pyexpat/mod.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-interpreter/src/objspace/std/mapdict.rs
  • pyre/pyre-interpreter/src/pyopcode.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/gc_interp.rs
  • pyre/pyre-object/src/gc_roots.rs
  • pyre/pyre-object/src/longobject.rs

Comment on lines 7276 to 7282
fn file_set_pos(self_obj: PyObjectRef, pos: usize) {
let _ = crate::baseobjspace::setattr_str(self_obj, "__file_pos__", w_int_new(pos as i64));
// Private storage slot on a fresh hasdict file wrapper (no custom
// `__setattr__`, `__file_pos__` is not a descriptor), so the write is
// the infallible instance-dict store `W_Root.setdictvalue`
// (baseobjspace.py:51) that `setattr_str` would itself reach.
crate::baseobjspace::setdictvalue(self_obj, "__file_pos__", w_int_new(pos as i64));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse file_set_pos in seek for consistency.

seek's handling of the same __file_pos__ slot (line ~7209, unchanged) still writes via setattr_str(...) and discards the result with let _ =, duplicating and diverging from this newly-documented "infallible instance-dict store" pattern. Consider routing seek through file_set_pos so both writers share one code path/semantics.

♻️ Proposed consolidation
-            if args.len() >= 2 {
-                let _ = crate::baseobjspace::setattr_str(args[0], "__file_pos__", args[1]);
-            }
+            if let Some(&pos) = args.get(1) {
+                if let Ok(v) = unsafe { crate::pyre_object::w_int_get_value_checked(pos) } {
+                    file_set_pos(args[0], v as usize);
+                }
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/builtins.rs` around lines 7276 - 7282, The `seek`
path is still updating the `__file_pos__` slot through a separate
`setattr_str(...)` write instead of the shared `file_set_pos` helper. Update
`seek` to call `file_set_pos` for setting the file position so the
`__file_pos__` state uses one consistent code path and semantics, matching the
newly documented store behavior in `file_set_pos`.

youknowone added 17 commits July 2, 2026 18:12
is_string_as_bytes_identity recognizes as_bytes on a string-family impl
owner (String/str/Wtf8/Wtf8Buf) and binds the call destination to the
receiver, mirroring the as_str/as_ref/borrow identity, so the byte view
lowers as the string value instead of an as_bytes getattr the rtyper
cannot route on the classdef-less string receiver.

Assisted-by: Claude
… machinery

Mark 18 pyre_jit functions #[majit_macros::dont_look_inside]: the driver /
merge-point / run path (maybe_compile_and_run, jit_merge_point_hook,
bound_reached, execute_assembler, build_jit_state, handle_fail,
handle_jit_outcome, driver_pair, call_depth, debug_first_arg_int,
jit_suppressed_by_unsupported_frame, make_green_key), the deopt / blackhole
resume path (decode_and_restore_guard_failure, decode_exit_layout_values,
resume_in_blackhole_from_exit_layout), and the bridge-compile path
(trace_and_compile_from_bridge, bridge_source_identity_from_descr,
take_ca_exception).

find_all_graphs pulled these in as look-inside candidates, but they read
JIT-internal TLS / state / statics with no registry-resolvable accessor and
failed the two-phase prepass annotate. look_inside_graph now returns false
for them so callers emit residual Calls instead of inlining. Removes the
macro's #[inline(never)]. Takes effect after LLBC re-extraction.

Prepass phaseA 496->474 distinct (the 18 graphs plus transitive callees
reachable only through them); phaseB unchanged at 0. check.py dynasm
158/158, cranelift 158/158.

Assisted-by: Claude
…d-opcode slow-path handlers

Mark descr_not_settable_error, raiseattrerror (baseobjspace) and
execute_unsupported (pyopcode) #[majit_macros::dont_look_inside]. These
are AttributeError construction (read-only descriptor / attribute miss)
and the unsupported-opcode handler. Residualizing them drops them plus
their transitive callees attribute_error_with_context and
OpcodeStepExecutor::unsupported from the codewriter candidate set.

Measured against a fresh no-marks pyre-interpreter extraction: phaseA
distinct 502 -> 497 (-5), zero added fails. check.py dynasm 158/158 +
cranelift 158/158.

Assisted-by: Claude
…redecessor merges

simplify_phis collapsed an all-equal phi column for any arity, including a
single-predecessor block's one-element column (a one-element slice is
trivially all-equal). Upstream simplify.py:561 is safe because cleanup_graph
runs join_blocks first, folding every single-predecessor block into its
predecessor. The charon-MIR front-end makes every Call a block terminator and
threads the result as the next block's inputarg, so single-predecessor
non-empty blocks persist here — a call block must keep its single exit, so
join_blocks cannot fold them. Collapsing such a one-element column unions the
inputarg with a Variable defined in the predecessor and renames the body
reference across the block boundary, leaving the flowspace adapter an operand
defined in no reachable predecessor ("undefined operand").

Restrict the all-equal collapse to genuine multi-predecessor merges
(new_args.len() > 1). A single-predecessor block's duplicate slots are still
folded by the unique_phis equivalence, keeping the surviving inputarg.

PREPASS phaseA failures 495 -> 473; undefined-operand 54 -> 28.
check.py dynasm 165/165 + cranelift 165/165.

Assisted-by: Claude
…o_SSI repair

The all-equal collapse (simplify.py:561-563) removes a merge block's
inputarg and renames body references to a value defined in a predecessor,
producing cross-block references. RPython repairs these with the next
all_passes entry, SSA_to_SSI (backendopt/ssa.py:135-196), which re-threads
each used-but-undefined variable as a fresh inputarg. remove_identical_vars_SSA
runs here on the crate::model front-end graph, which has no SSA_to_SSI port
(the ssa_to_ssi port operates on crate::flowspace::model, a distinct IR), so
the collapse left body references the flowspace adapter rejects as undefined
operands.

prune_dead_phis already drops dead inputargs, so every surviving column is
used and SSA_to_SSI would re-thread each collapsed column; skipping the
collapse yields the same graph with the column left threaded. The
duplicate-column dedup (simplify.py:565-568) is the unique_phis branch, which
is unaffected. Removes the all_equal/isspecialvar helpers and the earlier
new_args.len() > 1 guard this subsumes.

[PREPASS phaseA fail] undefined-operand distinct 28 -> 0; phaseA distinct
473 -> 472. check.py dynasm+cranelift 165/165.

Assisted-by: Claude
collapse_pos0_read is the only fallible mutation in the ?-diamond caller
rewrite; with two or more continue-arm payload positions it can mutate one
target before erring on a later one, leaving a partially-rewritten graph.
Decline that shape up front, mirroring iter_next::rewire_one_next_site.

Assisted-by: Claude
A `SomeInstance(PyError)` receiver reaching `getattr(recv, "to_exc_object")`
Blocked: PyError's synthesized class dict carries no inherent-impl methods,
so `s_getattr` returns Impossible. The result_exc ?-lowering mints
`to_exc_object` as a `Method{receiver_root: "PyError"}` residual that the
codewriter already resolves via `for_impl_method` to the real conversion
graph. Surface it in the SomeInstance.getattr path as a residual builtin
method (mirroring the `is_null` ptr-method arm) typed `PyObjectRef`, so the
annotator agrees with the codewriter's real fnaddr conversion.

phaseA prepass distinct failures 472 -> 459 (26 to_exc_object blocks clear).

Assisted-by: Claude
The exception-link lowering is gated on the RESULT_EXC_LOWERING_SCOPE
allowlist plus the execute_* wrapper family; RPython's exceptiontransform
(transform_completely, exceptiontransform.py:212) instead transforms every
graph uniformly with a shape-agnostic per-op check. Add a default-off
switch (PYRE_RESULT_EXC_UNIFORM=1) that makes in_result_exc_scope answer
true for every callee, so both gate sites (mir.rs callee rule + caller
diamond record) fall back to the structural tyref_is_result_of_pyerror
type filter alone — the no-allowlist model.

Under the flag the shape-specific rewrite declines (fail-safe → residual,
no miscompile) every caller/callee shape it does not recognise; gate the
MIR-lowering coverage-regression guard so the experiment reports those
declines and proceeds instead of failing the build. The production gate
(flag off) is unchanged: byte-identical lowering, phaseA 449, the guard
still fails loud on any unrecognised error.

Measured Stage-3 shape-coverage gap (flag on): 192 function(s) decline the
exception-link rewrite and 350 cachedgraph lifts hit unregistered callees
(bigint::to_i64/to_f64, bool::then, slice methods, __dyn_call, …) — the
allowlist drop is coupled to the unregistered-callee registration lever.

Assisted-by: Claude
… ctors and generalize the return-forwarding check

lower_result_exc_returns iterates every Result::Ok/Err ctor and lowers
each into a value return / raise, failing the whole callee on any ctor
whose value does not pure-forward to returnblock. Under
PYRE_RESULT_EXC_UNIFORM, a ctor whose value is consumed inside the graph
(an inlined callee's return that the graph then matches on or passes to a
call) is now skipped, leaving it materialized, rather than failing the
callee; only ctors that flow to returnblock are lowered.

verify_forwards_to_returnblock dispatches under the gate to a new
verify_forwards_to_returnblock_general: a worklist over (block, alias)
states that accepts forwarding chains whose intermediate blocks carry
unrelated operations and conditional exits, as long as the tracked value
is never read by an operation nor used as an exitswitch operand.

The flag-off path is unchanged. With PYRE_RESULT_EXC_UNIFORM=1 the prepass
declines drop from 192 to 26.

Assisted-by: Claude
…carrying shapes

Convert the `let _ = <Result>` discard idiom in the functions that declined
the result_exc uniform exception-link lowering:

- propagate via `?` where the enclosing fn returns Result: file_flush_dirty,
  file_method_close, textio_configure, make_exc_type add_note closure,
  init_file_wrapper __exit__ closure, normalize_exception, build_class_inner
  prepared-dict setitem.
- infallible instance-dict store via setdictvalue (promoted to pub(crate))
  for fresh builtin-object construction: wrap_trace_frame, pyexpat
  init/init_parser_slots/make_namespace, sys build_frame_stub_chain,
  make_traceback_frame_stub, make_std_stream, register_module.
- explicit best-effort catch where a failure must not mask the original
  result: set_name add_note, _csv reader_next finally-reset, build_class
  scaffolding delitem (KeyError-only).
- _structseq new_instance_with_extra setdict: .expect on the infallible
  fresh-dict install.

Assisted-by: Claude
…callee

`rewrite_op_direct_call`'s residual arm resolved the call's result kind
from `front::mir`'s `result_ty`, which types a `Result<(), PyError>`
callee's result `Ref` (every aggregate, including the unit `()`, lowers
to `Ref`). `getcalldescr` then hard-fails the call's `RESULT` against the
callee's declared `void` `FUNC.RESULT` (call.py:230 `RESULT == FUNC.RESULT`).

Add `CallControl::declared_result_type_for_target` — the callee's post-`?`
projected `RESULT`, the same value getcalldescr derives as
`expected_result` — and `Transformer::effective_call_result_ty`, which
substitutes `void` for the call's result when the callee declares `void`
and the front resolved `Ref`. The residual_call then emits a `_v` opname
with no result slot (`emit_call_result_arg` writes no slot for kind `v`),
the caller-side mirror of the returnblock widen in
`finalize_rewritten_graph_to_jitcode`.

Assisted-by: Claude
Drop the allowlist-gated exception-link lowering for the whole-program
transform (exceptiontransform.py:212 transform_completely):

- Delete RESULT_EXC_LOWERING_SCOPE, in_execute_wrapper_family,
  result_exc_uniform_gate, in_result_exc_scope, and the
  PYRE_RESULT_EXC_UNIFORM env switch. The callee-side type gate
  tyref_is_result_of_pyerror is the sole scope filter.
- mir.rs: result_exc_callee and the caller-capture guard drop the
  in_result_exc_scope conjunct.
- mir.rs lowering-coverage gate reports an unrecognised shape and
  proceeds (fail-safe residual) instead of failing the build.
- lower_result_exc_returns and the rewrap forward check use
  verify_forwards_to_returnblock_general unconditionally; delete the
  strict verify_forwards_to_returnblock.

Assisted-by: Claude
…ches on its own discriminant

A Result-ctor block whose conditional exit is driven by the ctor's own
`__discriminant` read is a consumed-intermediate `match` on the freshly
built shell (the `__new__` wrapper's `match { Ok(obj) } { Ok(o) => o,
Err(e) => return Err(e) }`), not a return shell.

lower_result_exc_returns now computes `well_formed_return` before the
conditional-exit check and skips such a block — leaving the shell
materialised for the local `match` — instead of declining the whole
callee. The hard decline is kept only for a well-formed return shell that
has a conditional exit, where an `Err` rewrite's set_raise_values →
set_goto would discard the other arm.

Clears the 8 `__pyre_wrap___new__` conditional-exit declines (_random,
struct, __pypy__::interp_buffer, _pickle::pickler+unpickler,
select::interp_kevent+kqueue+select); whole-program result-exc declines
14 → 6.

Assisted-by: Claude
…rding setattr_str

file_set_pos (__file_pos__), deque_class::store (__data__), and
deque_class::modified (__state__) write a private non-descriptor slot on a
hasdict instance with no custom __setattr__, where setattr_str's terminal
path is the instance-dict write setdictvalue; call setdictvalue directly so
the helper no longer discards a Result.

function_setdict stored `func.__dict__ = value` through
setattr_str(obj, "__dict__", value) — a literal "__dict__" dict entry with
the result discarded — instead of replacing the function's dict. Route it
through baseobjspace::setdict (wholesale dict replacement, raising TypeError
on a non-dict per Function.setdict) and propagate; function_setdict and
setdict now return Result<(), PyError>.

Whole-program result-exc declines 6 -> 2.

Assisted-by: Claude
`sync_python_sys_path` replaced a discarded `let _ = setattr_str(sys_mod,
"path", ..)` with the infallible direct dict store the `setattr_str`
module branch reaches — `w_module_get_w_dict` + `w_dict_setitem_str` —
dropping the discarded `Result`.

Assisted-by: Claude
The two-phase rtyper prepass drives annotation per subject and defers
`RPythonAnnotator::complete()`, so complete()'s return-var seeding tail
(annrpython.py:258-261) never runs. A graph all of whose reachable paths
end at the exceptblock leaves the returnblock unreachable and its return
var unannotated; Phase B's `setconcretetype(graph.getreturnvar())` then
panics on the missing binding.

Extract the per-graph seeding from `complete()` into
`force_return_var_annotation`; add `seed_all_annotated_return_vars`
over the annotated graphs; call it at the two-phase barrier after
`assign_inheritance_ids`, alongside the other stand-ins for the deferred
`complete()`. Unbound return vars bind to `s_ImpossibleValue`.

phaseB return-var "no binding" panics: 172 -> 13. The other graphs now
proceed past the return var and fail rtype with a caught error instead
of a panic. check.py 166/166 dynasm + 166/166 cranelift.

Assisted-by: Claude
…ect two degrade-to-residual comments

result_exc: split the intervening-block forward check by shell kind. The
Ok rewrite only edits the producer exit link args, so the payload still
threads through an intervening op-bearing block to returnblock — keep the
generalized verify_forwards_to_returnblock_general check. The Err rewrite
calls set_raise_values, which replaces the producer block exit with a jump
to exceptblock and bypasses any intervening block, dropping its operations
and raising earlier than the interpreter. Require the strict
forwards_to_returnblock (empty unconditional intervening blocks only) for
is_err shells; otherwise decline to a residual call.

mir: correct the coverage-gate comment to state that a non-tracked lowering
failure degrades to a residual call (matching exceptiontransform.py:212)
rather than failing the build, and that the regressions bucket covers every
non-tracked skip. Extend the as_bytes-identity-alias comment to record that
the alias is sound only for len/equality/iteration; a scalar index yields a
Char, which currently fails rtype and fail-safe residualizes.

Assisted-by: Claude
…nstance-dict set accessors

pin_root reads the thread-local SHADOW_STACK, dereference reads the weakref
w_obj_weak slot (@jit.dont_look_inside in interp__weakref.py:168), and
_obj_setdict writes the per-instance INSTANCE_DICT side table — each through a
closure with no extractable graph, so as look-inside candidates the three
subjects failed the prepass lift on an unregistered static
(SHADOW_STACK / ATTR_W_OBJ_WEAK / INSTANCE_DICT) FunctionPath. Mark each
#[majit_macros::dont_look_inside] so callers emit a residual Call, and bind
their fnaddrs in jit_trace_fnaddrs (pin_root twins shadow_stack_len, dereference
twins proxy_type). pin_root drops its #[inline]; the macro forces
#[inline(never)] for a stable residual symbol.

Assisted-by: Claude
Add `bool_then_closure` (`c.then(|| x + 1)`) exercising the
`core::bool::<Impl>::then` opaque combinator with a `FnOnce` closure that
captures an enclosing value by reference. Charon extracts the closure's
`call_once` as a transparent inherent method of the closure type. Re-extract
the checked-in corpus.ullbc.

Assisted-by: Claude
core::bool::<Impl>::then is a foreign combinator with an Opaque body, so
its caller emitted a residual `then` call — an unregistered callee the
rtyper prepass Skips. Recognize `bool::then(cond, closure_env)` during MIR
lowering and rewrite it, in a post-pass, into the branch its semantics
imply: `if cond { Some(closure()) } else { None }`. The branch is
mandatory — the closure must not run on the false arm — so a single-block
always-compute encoding is unsound. The closure body reaches the graph as
the closure type's transparent `call_once` inherent method; the then arm
calls it directly.

- front/bool_then.rs: BoolThenSite + rewire_bool_then_call_sites post-pass,
  modeled on front::iter_next; fail-safe (a structural mismatch leaves the
  residual call, keeping the prepass Skip).
- front/mir.rs: recognize and record sites during lower_call, resolving the
  Option ctor owners, the closure call_once owner, and the payload type;
  drive the post-pass after the next-diamond rewrite.
- test_mir_frontend: bool_then_closure lifts to the diamond (call_once +
  bool branch + Some/None).

core::bool::<Impl>::then prepass phaseA failures: 26 -> 0, no new
unregistered callee. check.py dynasm 139/139 + cranelift 139/139.

Assisted-by: Claude
…he gc-enabled / bigint-type-id atomic reads

py_repr reads the const bool CAN_BE_TAGGED and is_array/memoryview_backing_slice
read the static PyType ARRAY_TYPE — both surface as opaque global-read
FunctionPaths the prepass cannot resolve. Register CAN_BE_TAGGED in
jit_static_int_values (bake the build-time value) and ARRAY_TYPE in
jit_static_pytype_addrs (bake the singleton address).

enabled reads (and lazily initialises) the gc_interp STATE atomic and
bigint_gc_type_id reads the init-assigned BIGINT_GC_TYPE_ID atomic — neither is
a build-time constant, so mark both #[majit_macros::dont_look_inside] so callers
emit a residual Call and bind their fnaddrs in jit_trace_fnaddrs.
bigint_gc_type_id becomes pub for the cross-crate fnptr; both drop #[inline] for
the macro's #[inline(never)].

Assisted-by: Claude
…ebase

`loads_fixture_corpus` asserted 6 `charon_corpus::` local fns; the
`bool_then_closure` fixture (9c3463d) added the closure fn plus its
`<Impl>::call_once` inherent method, so `iter_local_fns` now counts 9.

`prune_dead_phis_collapses_single_source_phi_with_reader` (from #274,
merged via the rebase) asserted the all-equal single-source phi collapse.
`remove_duplicate_inputargs` on this branch omits that collapse (documented
PRE-EXISTING-ADAPTATION: no `SSA_to_SSI` repair on `crate::model`, so the
collapse would strand a cross-block reader as an undefined operand). Rename
and invert the test to pin the retained-live-phi shape.

Assisted-by: Claude
…aches returnblock

`lower_result_exc_returns` skipped a Result ctor it could not lower cleanly
(`!well_formed_return || !forwards_ok`) unconditionally. A ctor that is a
genuine return shell — its value reaches `returnblock` on some path — but is
not a pure forward (an extra shell use, an intervening read, a non-pure `Err`
forward) was thereby left materialised while another return in the same callee
rewrote cleanly. `rewritten > 0` then reported the callee transformed and
rewired its callers to receive the unwrapped `T`/exception, yet the skipped
path still returned a `Result` object.

Guard the skip with `shell_reaches_returnblock` (a purity-agnostic forward
reachability check): only leave a ctor materialised when its value never
reaches `returnblock` (a consumed intermediate); otherwise decline the whole
callee (fail-safe → residual call) instead of emitting the partial rewrite.

Assisted-by: Claude
…ssage

The `is_known_lowering_gap` doc comment claimed that an unrecognised
lowering failure "fails the whole-program build"; the code instead
degrades every un-lowerable function to a residual call (the coverage
gate only warns), matching `exceptiontransform.py:212`
`transform_completely`, which transforms every graph and leaves an
un-rewritable one to the residual-call ABI. Rewrite the doc comment to
describe that fail-safe behaviour and cite the upstream analog.

The regressions-bucket eprintln labelled every non-tracked skip
"[result-exc] declined the exception-link lowering", but the bucket also
holds non-result-exc shapes (e.g. `init_file_wrapper_type::<Impl>::call`,
a call block exit that does not carry the tracked value). Relabel to
"[mir-coverage] ... unrecognised MIR shape degraded to residual".

Assisted-by: Claude
`effective_call_result_ty` runs only in the Residual call-dispatch arm.
Applying it to the Regular / Recursive (inlined) arms retypes an inlined
`Result<(), PyError>` callee's unit `()` result to `Void`, but that
result is a live `Ref`-carried block inputarg / link arg: regalloc
`lookup_coloring` (assembler.rs:2403) rejects an uncoloured `Void`
carried variable and the build-script worker panics
(`eval_loop_jit`). Record on both inlined arms why the reconciliation is
Residual-only (there `residual_call_v` genuinely drops the result slot).

Assisted-by: Claude
…_reaches_returnblock

The conditional-exit branch in lower_result_exc_returns skipped a
non-well-formed Result shell via `continue` without checking whether the
shell reaches returnblock. A shell that reaches returnblock on a
conditional arm, left materialised while another return in the same
callee rewrites cleanly, keeps rewritten > 0: the callee is reported
transformed and its callers are rewired to the unwrapped T/exception, yet
that path still returns a Result object. Add the shell_reaches_returnblock
guard (mirroring the unconditional non-well-formed path) to decline the
whole callee to a residual call.

Assisted-by: Claude

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a0af068f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +188 to +194
graph.block_mut(then_bb).operations.push(SpaceOperation {
result: Some(payload.clone()),
kind: OpKind::Call {
target: CallTarget::method("call_once", Some(site.call_once_owner.clone())),
args: vec![env_in_then, unit],
result_ty: site.payload_ty.clone(),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Result payloads in bool::then

When the closure passed to bool::then itself returns Result<T, PyError> (for example cond.then(|| fallible())), that call_once callee is still transformed by the uniform Result-exception pass, but this synthesized call site is emitted after rewire_result_exc_call_sites, so it never gets the catch-and-rewrap handling needed for a non-? consumer. In that scenario the true arm wraps a bare T or raises on Err instead of producing Some(Ok(T)) / Some(Err(e)), so either decline Result payloads here or synthesize the same rewrap shape around the call_once.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant