Skip to content

majit-translate, majit, jit: a payload enum's variant fields seated after the inherited discriminant, PCSEQ at every frame depth, and the portal's can_enter_jit tallies - #1667

Merged
youknowone merged 5 commits into
mainfrom
import
Sep 3, 2026

Conversation

@youknowone

Copy link
Copy Markdown
Owner

The portal metatrace walk stopped reporting Continue on a back edge. The
cause was not in the portal: a payload enum's variant fields were packed from
byte 0, on top of the discriminant the variant inherits from its base.

The defect

majit-translate models a Rust payload enum the way rclass.py:499-518 does —
a base carrying only __discriminant, and one subclass per variant carrying
that variant's fields and inheriting the base's byte 0. Seating the payload
after the tag was gated on a two-name allowlist (Result, Option). Every
other payload enum fell through to default packing, which puts field i at
i*8 — field 0 on the tag's byte.

StepResult, what every opcode handler returns, is one of them. Measured descrs
before the fix:

descr kind size fields
StepResult.__discriminant base 8 __discriminant @0
StepResult::CloseLoop variant 16 jump_args @0, loop_header_pc @8
StepResult::Return variant 8 __pos_0 @0

So close_loop wrote tag 2 to byte 0 and the next op overwrote it with
jump_args (a None, i.e. null). The reader saw tag 0Continue. Backend
independent; it is in the jitcode, not in any assembler.

The fix

Replace the allowlist with the question it was standing in for: does the base
register a __discriminant row that this variant inherits, and does any
variant's field land inside the tag's byte range?

tag_recorded_but_unspellable registers no base row at all, so a payload at 0
there aliases nothing — that is the base_has_discriminant term. An unrecorded
tag width reads as a machine word, and an enum for which Charon recorded no
layout answers "yes, seats on the tag" outright: that is precisely the case
that hid this bug, because the variant offsets then come from default packing.

An earlier attempt spelled the overlap test as
enum_layout.is_some_and(...), which did nothing — StepResult has no Charon
layout, so the guarded case and the broken case were disjoint.

After

MAJIT_PCSEQ on the portal walk, with the same script:

execute_opcode_step    pc=3    value=75  -> 1160
execute_jump_backward  pc=38   value=2   -> 75     (was 0)
eval_loop_jit_portal   pc=436  value=2   -> 791    (was 0 — the CloseLoop arm, first time)

and the run answers correctly instead of aborting.

pyre/check.py --backend dynasm: ALL PASSED 536/536. The predicate moves
every payload enum in the LLBC set that Charon recorded no layout for, not just
StepResult, so that is the number that matters here.

The instrument that found it, and one for wasm

pcseq_branch returned early unless frames.len() == 1, so only the root
jitcode's branches printed. The line that settled this — execute_jump_backward's
own switch on the StepResult discriminant — is three frames down. It now
prints at every depth with d=<depth> and the owning jitcode's name.

Every other portal instrument is a std::env::var read (PYRE_PORTAL_METATRACE,
..._ENTRY, ..._SKIP, MAJIT_PCSEQ), and the wasm guest has no
environment
, so none of them can fire there. PORTAL_DIAG is two counters
read through one pyre_jit::eval::portal_diag(slot) by both consumers: the
native [jit-stats] portal_diag line, and the pyre_jit_portal_diag /
pyre_jit_portal_diag_len exports the wasm runner prints as the same line.
Exported rather than imported, as pyre_jit_bridge_diag already is — an import
shifts the JIT's function-index space.

Both bumps sit inside can_enter_jit's body, which jtransform rewrites into
the loop_header operation and so never traces. That placement is the whole
design: pyre-jit is one of the four LLBC crates, so a counter in
eval_loop_jit's CloseLoop arm gets lowered into the portal jitcode, and
AtomicU64::fetch_add is outside the LLBC set — it would become a symbolic
residual sitting between the arm and its loop_header, which no pyre-jit
path can bind. The instrument would have blocked the thing it was measuring.

The runner reports a slot the guest exports but its own legend does not name as
slot<N>, rather than dropping it the way the bridge_diag positional mirror
would. Neither counter is in JITSTATS_SNAPSHOT_FIELDS, so no recorded
baseline carries them.

The reading it buys

First portal measurement ever taken from inside the wasm guest, hot.py
(a 200k-iteration while), against the native dynasm binary on the same
script:

can_enter_jit can_enter_jit_taken loops_compiled guard_failures
dynasm 1930 2 1 1
wasm 1041 2 1 1

Both compile the one loop and take it twice — once on the initial trace, once
after the guard failure. The counts differ in the untaken polls, i.e. the
interpreted warm-up before the loop goes hot, which is a threshold-accounting
difference and not a portal one. back_edge_polls reads 0 in both runs while
can_enter_jit reads four figures, which is worth knowing before anyone uses
back_edge_polls as the portal-entry count.

The runner also prints, unprompted, the reason this line had to exist at all:

[pyre-wasm-runner] ignoring PYRE_JIT_TRACE_CACHE_DIR, PYRE_JIT_TRACE_CACHE_ENTRIES:
the wasm guest has no environment, so guest-side knobs and probes do nothing here

Also here

Three comments that named the wrong thing. pcseq_branch's doc still described
the depth gate this branch removes. get_jitcode_calldescr reads as if the
(fnaddr, calldescr) it stamps on every CodeObject jitcode were how pyre calls
a Python function — it is never dispatched (bhimpl_recursive_call_* goes
through get_portal_runner; pyre's inline_call handlers read a build-time
descr pool a runtime jitcode is never in), and the descr beside it describes
bh_portal_runner_c, not the slice-taking bh_portal_runner whose address it
takes. executor.rs's Float arm named the same wrong function as
portal_runner_adr's value.

`PyFrame::close_loop` filled the field with `vec![]`.  In a jitcode that
`Vec::new` is a residual call whose path the translator has no binding for, so
a walk that reaches a back edge through `close_loop` stops there; the field is
written at five sites and read at none.

Assisted-by: Claude
…ited discriminant

A payload enum is modelled as RPython's sum-type subclass layout: the base
carries `__discriminant`, each variant subclass carries its own fields and
inherits the base's byte 0. When `layout_for_target` returns `None` the
variant's field offsets fall back to default packing from byte 0, so the first
payload field is placed on the discriminant's byte.

`pyre_interpreter::pyopcode::StepResult` has no recorded layout. Its
`CloseLoop` variant was `{jump_args@0, loop_header_pc@8}` and `Return` was
`{__pos_0@0}`, so `close_loop`'s jitcode wrote discriminant 2 to byte 0 and
overwrote it with `jump_args` on the next op; `execute_jump_backward` read the
null back as tag 0 and reported `Continue`.

The explicit sum shell that seats the tag at 0 and the payload from 8 was
reached by an allowlist of two type names, `core::result::Result` and
`core::option::Option`. Compute the condition those two share instead: apply
the shell when the base registered a `__discriminant` row the variant inherits
and the host layout would seat a payload field inside the tag's bytes, taking
an absent layout as overlapping since default packing starts at byte 0. The
guard reuses `!fieldless && !tag_recorded_but_unspellable`, the same test the
base row's own registration uses, so an `I128`/`U128` tag — which registers no
base row — keeps byte 0 for its payload.

`int_type_byte_width` maps the tag spelling Charon records beside the offset to
a byte count; an unrecorded width answers a machine word.

Assisted-by: Claude
…he jitcode

`pcseq_branch` returned early unless `frames.len() == 1`, so only the root
jitcode's own switches and branches printed and the whole descent was silent.
A portal walk reporting `Continue` on a back edge then gave no reading between
the portal's arm and the opcode handler, while a static decode of that chain
read correct at every hop.

Drop the depth gate and add `d=<depth>` to each line. The line that named the
divergence — `execute_jump_backward`'s own `switch` on the `StepResult`
discriminant — sits three frames down.

Assisted-by: Claude
…nter export

Every portal instrument is a `std::env::var` read — `PYRE_PORTAL_METATRACE`,
`PYRE_PORTAL_METATRACE_ENTRY`, `_SKIP`, `MAJIT_PCSEQ` — and the wasm guest has
no environment, so none of them can fire there. Add `PORTAL_DIAG`, two counters
read through one `pyre_jit::eval::portal_diag(slot)` by both consumers: the
native `[jit-stats] portal_diag` line from `maybe_print_jit_stats`, and the
`pyre_jit_portal_diag` / `pyre_jit_portal_diag_len` exports the wasm runner
prints as the same line. Exported rather than imported, as
`pyre_jit_bridge_diag` already is, because an import shifts the JIT's
function-index space.

Both bumps sit inside `can_enter_jit`'s body, which `jtransform` rewrites into
the `loop_header` operation and so never traces. `pyre-jit` is one of the four
LLBC crates, so a counter in `eval_loop_jit`'s `CloseLoop` arm is lowered into
the portal jitcode, and `AtomicU64::fetch_add` is outside the LLBC set: it
becomes a symbolic residual between the arm and its `loop_header`, which no
`pyre-jit` path can bind. `portal_activation_bracketed` is reached from
`funccall_valuestack` with no `dont_look_inside` or may-force boundary, so it
carries no counter either.

The runner reports a slot the guest exports but its own legend does not name as
`slot<N>`, rather than dropping it as the `bridge_diag` mirror would.
`can_enter_jit` and `can_enter_jit_taken` are absent from
`JITSTATS_SNAPSHOT_FIELDS`, so no recorded baseline carries them.

Assisted-by: Claude
…q_branch's root-frame claim

`pcseq_branch`'s doc still described the depth gate the previous commit
removed ("Print one control-flow edge of the root jitcode frame", "Silent for
an inlined callee").

`get_jitcode_calldescr` takes the address of `bh_portal_runner(&[i64], &[i64],
&[i64]) -> i64` and pairs it with a `"r"` -> `'r'` descr, and its comment reads
as if that pair were how pyre calls a Python function. It is not dispatched:
`bhimpl_recursive_call_*` reaches the portal through `get_portal_runner`
(`bh_portal_runner_c`, `"iirrr"`), and pyre's `inline_call` handlers read their
callee from a build-time descr pool whose `fnaddr` comes from
`JitCodeBuilder::set_native_entry`, which a runtime CodeObject jitcode is never
in. Say that, and say which of the two functions the descr describes.

`executor.rs`'s Float arm named `bh_portal_runner(all_i, all_r, all_f)` as what
`portal_runner_adr` holds; `call_jit.rs:2819` sets it to `bh_portal_runner_c`.
The i64-return contract the comment exists to pin holds for both.

Assisted-by: Claude
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T23:36:57.000074Z b552ab5 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 4465c964-9734-4d26-a73e-373a328050b2

📥 Commits

Reviewing files that changed from the base of the PR and between 7850166 and b552ab5.

📒 Files selected for processing (12)
  • majit/gate-triage.md
  • majit/majit-metainterp/src/executor.rs
  • majit/majit-metainterp/src/lib.rs
  • majit/majit-metainterp/src/pyjitpl/dispatch.rs
  • majit/majit-translate/src/front/mir.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/pyopcode.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/call.rs
  • pyre/pyre-wasm-runner/src/main.rs
  • pyre/pyre-wasm/src/lib.rs
  • pyre/pyrex/src/lib.rs

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.

@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

https://github.com/youknowone/pyre/blob/b552ab5a9acd37f36d885148365d88d5c0d432d4/pyre-interpreter/src/pyopcode.rs#L137
P1 Badge Fix Vec::new lowering instead of changing StepResult

For the ordinary PyFrame::close_loop path, this changes the interpreter's exact empty argument list into None solely because the translator lacks a binding for Vec::new. That accommodates a generation defect by changing interpreter structure rather than making the generated JIT preserve the source; restore the Vec<V> payload and teach the translator to lower or bind the empty-vector construction.

AGENTS.md reference: AGENTS.md:L29-L32

ℹ️ 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 +1875 to +1877
let explicit_sum_shell = name == "core::result::Result"
|| name == "core::option::Option"
|| (base_has_discriminant && payload_seats_on_tag);

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 Register every newly explicit shell with the codewriter

When this new arm selects a non-Result/Option enum such as StepResult, the front end assigns its tag to offset 0 and payloads from offset 8, but is_explicit_shell_variant_owner still recognizes only Result::{Ok, Err} and Option::Some. Consequently, bh_size_spec_from_callcontrol omits the inherited __discriminant from the variant's all_fielddescrs; when such a value is virtualized and later forced or resumed, the tag is not restored and the following match can again observe Continue. Carry the explicit-shell designation into the codewriter instead of leaving its nominal allowlist unchanged.

AGENTS.md reference: AGENTS.md:L29-L32

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit b552ab5).
Updated: 2026-09-02T23:57:44.963Z

Files in the reviewed diff
majit/gate-triage.md
majit/majit-metainterp/src/executor.rs
majit/majit-metainterp/src/lib.rs
majit/majit-metainterp/src/pyjitpl/dispatch.rs
majit/majit-translate/src/front/mir.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/pyopcode.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/call.rs
pyre/pyre-wasm-runner/src/main.rs
pyre/pyre-wasm/src/lib.rs
pyre/pyrex/src/lib.rs

Codex did not produce a report (exit 1). Last log lines:

    run at the pinned version, or C read at that tag in a named checkout. Not
    docs, not a PEP, not a comment in pyre's own source;
(c) the PyPy `file:line` that decides, showing the two upstreams actually
    differ (if PyPy contradicts itself, pyre following PyPy's own declaration
    is section 4 as ordinary parity);
(d) no PyPy-side JIT/GC/annotator hint governing the value being changed —
    `@jit.*`, `_immutable_*`, `_attrs_`, `make_sure_not_resized`,
    `unrolling_iterable`, `rgc.*`, on the function, its helpers, or the class-
    and module-level bindings they read.
Missing any of (a)-(d), or leaving pyre matching NEITHER upstream on an
adjacent observable of the same decision, keep it in section 1 or 2 and say
which test it failed. Full rule: AGENTS.md "Spec follows CPython 3.14;
implementation follows PyPy".

Scope discipline: before writing the report, run
`git diff upstream/main --name-only -- . ':(exclude)*.jitstats'` and treat that
file list as the authoritative definition of "this patch" (when an authoritative
changed-file list is appended below, use that instead of re-deriving it). The
excluded `*.jitstats` files are `pyre/check.py`'s recorded jit-stats baselines —
generated golden data with no RPython/PyPy counterpart, so no parity finding can
cite one, and a bulk re-record of them is not a change to review. Findings under
sections 1 and 2 MUST cite our-side files from that list; a divergence in any
file NOT in the list is by definition not introduced by this patch — report
it under section 3 instead, or omit it. Verify every section-1/2 citation
against the list before finalizing the report.

---

Output format requirements (so the report can be parsed mechanically and
posted/triaged automatically). Use these four headings VERBATIM, in this
order, and nothing else at heading level 2:

## 1. Regressions to PyPy parity introduced by this patch
## 2. Other mismatches introduced by this patch
## 3. Pre-existing mismatches (already present before this patch)
## 4. Structural adaptations

Under each heading, list every finding as a bullet. For each finding cite the
concrete `our_file.rs:line ↔ rpython_or_pypy_file.py:line` pair and quote the
divergence concisely. If a section has no findings, still emit the heading
followed by a single line `None.` so all four sections are always present.
Do not modify any files; produce the report only.

Authoritative changed-file list for this patch (git diff upstream/main --name-only,
minus 0 generated `*.jitstats` baseline file(s)):
majit/gate-triage.md
majit/majit-metainterp/src/executor.rs
majit/majit-metainterp/src/lib.rs
majit/majit-metainterp/src/pyjitpl/dispatch.rs
majit/majit-translate/src/front/mir.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/pyopcode.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/call.rs
pyre/pyre-wasm-runner/src/main.rs
pyre/pyre-wasm/src/lib.rs
pyre/pyrex/src/lib.rs
warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the bundled bubblewrap in the meantime.
ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Sep 7th, 2026 2:28 AM.
ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Sep 7th, 2026 2:28 AM.

@codspeed-hq

codspeed-hq Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 10 untouched benchmarks
⏩ 6 skipped benchmarks1


Comparing import (b552ab5) with main (7850166)

Open in CodSpeed

Footnotes

  1. 6 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@youknowone
youknowone merged commit 579580a into main Sep 3, 2026
27 of 36 checks passed
@youknowone
youknowone deleted the import branch September 3, 2026 05:49
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