Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ Grep RPython:
rg -t py 'lenbound|getlenbound|_x86_arglocs|_ll_loop_code' rpython/jit/
```

For a *behavioural* question rather than a structural one — "does upstream
really do this?" — grepping is the second step; see "The PyPy oracle" below.


### Workflow guideline

Expand All @@ -191,6 +194,40 @@ dict-like container in that exact spot. Apply the same test to TLS: locate the
upstream owner, then preserve whether it is global, interpreter-local,
execution-context-local, or genuinely thread-local.

## The PyPy oracle: run it before you argue about orthodoxy

Reading `rpython/` tells you what upstream *says*. Running a real `pypy3` tells
you what upstream *does*. When the question is "is this JIT behaviour orthodox,
or is it our deviation?", run the oracle FIRST — before reading source, before
forming a theory, and certainly before recording a verdict.

```
PYPYLOG=jit-summary:- pypy3 pyre/bench/synth/<fixture>.py
```
Comment on lines +204 to +206

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 | 🟡 Minor | ⚡ Quick win

Specify the fenced code-block language.

Line 204 starts a shell command block without a language identifier. Add sh to satisfy markdownlint.

Proposed fix
-```
+```sh
 PYPYLOG=jit-summary:- pypy3 pyre/bench/synth/<fixture>.py
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 204-204: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@AGENTS.md` around lines 204 - 206, Specify the shell language on the fenced
code block containing the PYPYLOG command by changing its opening fence to use
the sh identifier, while leaving the command and closing fence unchanged.

Source: Linters/SAST tools


Most `pyre/bench/synth` fixtures use no stdlib and run unmodified under the
real interpreter, so this costs one command. Read these keys: `Total # of
loops` / `Total # of bridges`, `forcings`, `virtualizables forced`, every
`abort: *`, `nvirtuals`. Compare against `MAJIT_STATS=1` on the same file.

**A counter that differs is a pointer, not the answer.** Go find the upstream
line that produces it — the decision is usually one JIT hint
(`@jit.look_inside_iff`, `@jit.dont_look_inside`, `@jit.elidable`,
`@jit.unroll_safe`) sitting on the function in question. Cite it. If the
summary is too coarse, `PYPYLOG=jit-log-opt:FILE` dumps the optimized trace.

Worked example (2026-08-03). `getframe_inline_subwalk_multiframe` failed 8948
GUARD_NOT_FORCED, and the standing conclusion was "a GUARD_NOT_FORCED never
compiles a bridge (`compile.py:950-953`), so this is unfixable by construction."
The oracle reported one loop, no bridges, **`forcings: 0`**, **`virtualizables
forced: 0`**, no aborts — PyPy never forces here at all, so the guard should not
exist. `pypy/module/sys/vm.py:41` then names the decision in one line:
`@jit.look_inside_iff(lambda space, depth: jit.isconstant(depth))` on
`getframe`, which pyre folds into a single opaque builtin. A verdict that had
stood for weeks was overturned, and an unbounded "epic" turned into a named
port, by one command. Note also `pypy3` is 3.11 — a fixture using newer syntax
needs trimming to the subset that runs.

## RPython Parity Rules
- When porting from RPython/PyPy, do STRICT line-by-line structural parity. Do NOT take shortcuts, reimplement from scratch, or declare phases 'complete' without the literal refactor.
- If a parity fix causes regressions, investigate root cause before reverting. Do not declare success if structural alignment was skipped, even if benchmarks pass.
Expand Down
115 changes: 115 additions & 0 deletions pyre/bench/frame_inlined_callee_own_image_regression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Self-checking regression guard for the image an INLINED CALLEE's own frame
# reports (registered via check.py run_selfcheck, NOT the synthetic suite).
#
# Sibling of `frame_lineno_mid_replay_regression`, which reads the coordinate
# through `sys._getframe(1)` from a callee whose caller is the portal — the
# frame the JIT treats as the virtualizable, and whose fields the walk keeps
# current. The shape here is the other one: the frame handed out is the
# INLINED CALLEE's own, materialised through a `jit.virtual_ref` rather than
# being the virtualizable. Such a frame is built with `last_instr = -1`
# (`pyre-jit-trace/src/helpers.rs`) and nothing updates it through the inlined
# body, and its locals region is not what the escape flush writes — that flush
# is keyed on the virtualizable. So the image has to come from the force, and
# `offset2lineno(code, -1)` answers the `def` line when it does not.
#
# What routes these reads to a correct answer today is the abort:
# `sys._getframe` forces the virtualizable, `vable_after_residual_call` reads
# the cleared token as a callee escape, and the interpreter finishes the
# iteration. That makes this guard load-bearing in an unusual way — it pins
# the ANSWER, not the mechanism, so a change that removes the abort in favour
# of a consumer-side force has to keep the answer. Removing the two forces in
# `sys._getframe` and forcing from the `f_lineno` / `f_lasti` getsets instead
# left the whole 370-fixture corpus green while making `own_lineno` below
# report the `def` line, `own_locals` grow a second entry, and `own_combined`
# segfault. None of the three had a fixture; that is why this one exists.
#
# Splitting each survey across several calls is what makes it a test, for the
# same reason the sibling guard does it: the loop compiles part-way through, so
# a set over the rounds holds the interpreted answer and the compiled one
# together, and a divergence appears as a SECOND element rather than a shifted
# single value.
#
# Offsets are relative to `co_firstlineno` so the expectations survive an edit
# above them. It asserts rather than diffing against pypy3 because the shape
# it pins is a pyre-internal one; cpython and pypy3 both answer the same values
# and agree with `PYRE_NO_JIT=1`.
import sys

N = 4000
ROUNDS = 8


def own_lineno(x): # +0
f = sys._getframe(0) # +1
return f.f_lineno - f.f_code.co_firstlineno # +2


def own_locals(x): # +0
f = sys._getframe(0) # +1
return tuple(sorted(f.f_locals)) # +2


def own_combined(x): # +0
f = sys._getframe(0) # +1
return (f.f_lineno - f.f_code.co_firstlineno, tuple(sorted(f.f_locals)), f.f_lasti >= 0)


def drive_lineno(n):
seen = set()
i = 0
while i < n:
seen.add(own_lineno(i))
i += 1
return seen


def drive_locals(n):
seen = set()
i = 0
while i < n:
seen.add(own_locals(i))
i += 1
return seen


def drive_combined(n):
seen = set()
i = 0
while i < n:
seen.add(own_combined(i))
i += 1
return seen


def main():
each = N // ROUNDS
failures = []

def check(label, got, want):
if got != want:
failures.append(f"{label}: got {got!r}, want {want!r}")

seen = set()
for _ in range(ROUNDS):
seen |= drive_lineno(each)
check("own_lineno", sorted(seen), [2])

seen = set()
for _ in range(ROUNDS):
seen |= drive_locals(each)
check("own_locals", sorted(seen), [("f", "x")])

seen = set()
for _ in range(ROUNDS):
seen |= drive_combined(each)
check("own_combined", sorted(seen), [(2, ("f", "x"), True)])

if failures:
for f in failures:
print("FAIL", f)
return 1
print("PASS inlined-callee own frame image")
return 0


sys.exit(main())
12 changes: 12 additions & 0 deletions pyre/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,18 @@ def main():
f"{B}/frame_lineno_mid_replay_regression.py",
20,
)
# The sibling shape: the frame handed out is the INLINED CALLEE's own,
# not the virtualizable its caller runs as. That frame carries the `-1`
# `last_instr` sentinel and is not what the escape flush writes, so its
# `f_lineno` / `f_locals` are only right because `sys._getframe` forces
# and the resulting abort finishes the iteration interpreted. Pins the
# answer so a change that retires that abort has to keep it -- the whole
# synthetic corpus stayed green while these three reads broke.
chk.run_selfcheck(
"frame_inlined_callee_own_image",
f"{B}/frame_inlined_callee_own_image_regression.py",
20,
)
# The branchy-inlined-callee guard (gh#343) lives in the synthetic parity
# suite as bridge_branchy_callee.py, gated against pypy by
# `# pyre-check: max-pypy-ratio`; a decline that keeps every crossing
Expand Down
Loading
Loading