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
42 changes: 42 additions & 0 deletions .github/workflows/pyre-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ on:
- "lib-python/**"
- "scripts/install-charon.py"
- "scripts/extract-llbc.py"
# The engine `extract-llbc.py` imports; most of the extraction logic
# lives here, so a change to it alone must still trigger this workflow.
- "scripts/llbc_extract.py"
- ".github/workflows/pyre-ci.yml"
pull_request:
branches: [ "main", "py3.[0-9]+" ]
Expand All @@ -22,6 +25,7 @@ on:
- "lib-python/**"
- "scripts/install-charon.py"
- "scripts/extract-llbc.py"
- "scripts/llbc_extract.py"
- ".github/workflows/pyre-ci.yml"
workflow_dispatch:

Expand Down Expand Up @@ -231,6 +235,10 @@ jobs:
with:
name: charon-${{ runner.os }}-${{ runner.arch }}
path: .pyre-build/charon
- name: Restore Charon executable permissions (Unix)
if: runner.os != 'Windows'
shell: bash
run: chmod +x .pyre-build/charon/*/charon .pyre-build/charon/*/charon-driver
- name: Download LLBC artifact
# Run-scoped handoff from prepare-charon-llbc. Unlike actions/cache,
# an artifact is exempt from the repo-wide 10 GB cache budget and its
Expand Down Expand Up @@ -357,6 +365,10 @@ jobs:
with:
name: charon-${{ runner.os }}-${{ runner.arch }}
path: .pyre-build/charon
- name: Restore Charon executable permissions (Unix)
if: runner.os != 'Windows'
shell: bash
run: chmod +x .pyre-build/charon/*/charon .pyre-build/charon/*/charon-driver
- name: Download LLBC artifact
# Run-scoped handoff from prepare-charon-llbc. Unlike actions/cache,
# an artifact is exempt from the repo-wide 10 GB cache budget and its
Expand All @@ -374,6 +386,28 @@ jobs:
test -s build/llbc/pyre-object.ullbc
test -s build/llbc/pyre-interpreter.ullbc
test -s build/llbc/pyre-jit.ullbc
- name: Only frame construction accesses PyFrame virtualizable fields without a pointer (Linux only)
# A red here means a virtualizable field of `PyFrame` is now reached off
# a non-dereferenced local aggregate somewhere the census does not expect
# — or that frame construction stopped doing so. The codewriter treats
# that shape as not reaching a live virtualizable and keeps it off the
# `getfield_vable_*` / `setfield_vable_*` path, so the set of places with
# that shape is a soundness-relevant invariant rather than a style rule.
# `FrameBox::new` taking its frame by value is what puts its twelve
# projections there; `pyframe.rs` carries a compile-time guard on that
# signature, and this is the corpus-level half.
#
# Reads the extracted LLBC that the steps above already downloaded — no
# corpus build.
#
# The Linux gate is required, not an optimization. This step sits in
# `&pyre-check-steps`, which the macOS and Windows legs alias, so it is
# copied into all three jobs; the census resolves charon per platform and
# has no Windows mapping, and would hard-fail that leg ungated. Running
# once is also sufficient — the property is platform-independent.
if: runner.os == 'Linux'
shell: bash
run: python3 pyre/scripts/vable-projection-census.py build/llbc/pyre-object.ullbc build/llbc/pyre-interpreter.ullbc build/llbc/pyre-jit.ullbc
Comment on lines +408 to +410

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 | 💤 Low value

Use the pinned interpreter instead of the system python3.

Every other Python step in this job runs ${{ steps.cpython.outputs.python-path }}. This step runs bare python3, so it binds to the runner image's system interpreter rather than the pinned 3.14. The script uses only the standard library today, so behavior does not change; the inconsistency is what invites drift when the image's default moves.

♻️ Proposed change
-      run: python3 pyre/scripts/vable-projection-census.py build/llbc/pyre-object.ullbc build/llbc/pyre-interpreter.ullbc build/llbc/pyre-jit.ullbc
+      run: ${{ steps.cpython.outputs.python-path }} pyre/scripts/vable-projection-census.py build/llbc/pyre-object.ullbc build/llbc/pyre-interpreter.ullbc build/llbc/pyre-jit.ullbc
📝 Committable suggestion

‼️ 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.

Suggested change
if: runner.os == 'Linux'
shell: bash
run: python3 pyre/scripts/vable-projection-census.py build/llbc/pyre-object.ullbc build/llbc/pyre-interpreter.ullbc build/llbc/pyre-jit.ullbc
if: runner.os == 'Linux'
shell: bash
run: ${{ steps.cpython.outputs.python-path }} pyre/scripts/vable-projection-census.py build/llbc/pyre-object.ullbc build/llbc/pyre-interpreter.ullbc build/llbc/pyre-jit.ullbc
🤖 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 @.github/workflows/pyre-ci.yml around lines 408 - 410, Update the run command
for the vable-projection census step to invoke the pinned interpreter from
steps.cpython.outputs.python-path instead of bare python3, while preserving the
existing script and arguments.

- name: Add wasm32 target (Linux only)
# check.py adds wasm to its default backends when this target is
# installed, so this is what makes the Linux leg build and run the wasm
Expand Down Expand Up @@ -530,6 +564,10 @@ jobs:
with:
name: charon-${{ runner.os }}-${{ runner.arch }}
path: .pyre-build/charon
- name: Restore Charon executable permissions (Unix)
if: runner.os != 'Windows'
shell: bash
run: chmod +x .pyre-build/charon/*/charon .pyre-build/charon/*/charon-driver
- name: Download LLBC artifact
# Run-scoped handoff from prepare-charon-llbc. Unlike actions/cache,
# an artifact is exempt from the repo-wide 10 GB cache budget and its
Expand Down Expand Up @@ -581,6 +619,10 @@ jobs:
with:
name: charon-${{ runner.os }}-${{ runner.arch }}
path: .pyre-build/charon
- name: Restore Charon executable permissions (Unix)
if: runner.os != 'Windows'
shell: bash
run: chmod +x .pyre-build/charon/*/charon .pyre-build/charon/*/charon-driver
- name: Download LLBC artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand Down
43 changes: 43 additions & 0 deletions majit/majit-metainterp/src/jit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,49 @@ pub fn hint<T>(x: T) -> T {
x
}

// ── access_directly / fresh_virtualizable ──
// rlib/jit.py:88-93

/// Access a virtualizable directly as a structure, without treating it
/// as a virtualizable.
///
/// rlib/jit.py:88 — `hint(x, access_directly=True)`
#[inline(always)]
pub fn hint_access_directly<T>(x: T) -> T {
hint(x)
}

/// Declare that the virtualizable was just allocated, so storing
/// directly on it is what is wanted — `Frame.__init__` is the motivating
/// case (`pypy/interpreter/pyframe.py:99`). Its redirected fields are
/// not under the virtualizable protocol yet, so the codewriter leaves
/// them as ordinary struct fields instead of lowering them to
/// `getfield_vable_*` / `setfield_vable_*` and instead of admitting an
/// array field to `vable_array_vars`
/// (`rpython/jit/codewriter/jtransform.py:990-993`).
///
/// Upstream this hint "has to come with access_directly=True"
/// (`rlib/jit.py:92-93`, asserted at `rlib/jit.py:313`), because there
/// the two flags ride one `SomeInstance` and `access_directly` is what
/// suppresses the `jit_force_virtualizable` call
/// (`rpython/rtyper/rvirtualizable.py:68-69`). Pyre dispatches one
/// helper per kwarg and has no `hook_access_field`, so this helper alone
/// carries the whole suppression; pair it with [`hint_access_directly`]
/// to keep the call sites readable as the upstream pair.
///
/// Because of that, placement matters here in a way it does not upstream.
/// The suppression is recorded per basic block, and upstream re-establishes
/// it in front of every redirected access, so its one `hint()` covers the
/// whole function. This helper covers only the block it sits in. Accesses
/// on the far side of a branch — an early return, a loop body, an assertion
/// behind a null test — are not covered, and each needs its own call.
///
/// rlib/jit.py:90 — `hint(x, fresh_virtualizable=True)`
#[inline(always)]
pub fn hint_fresh_virtualizable<T>(x: T) -> T {
hint(x)
}

// ── promote ──
// rlib/jit.py:100-124

Expand Down
37 changes: 37 additions & 0 deletions majit/majit-translate/src/codewriter/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2805,6 +2805,43 @@ mod tests {
);
}

/// `format.py:45-50 getlabelname` — labels are numbered lazily, in
/// order of their first *printed* mention, and a label line counts as
/// a mention just as much as a jump operand. A loop is the shape
/// that separates the two orders: the header's label line prints
/// before the back edge that targets it, so the header takes L1 even
/// though the forward exit is the first target the instruction stream
/// mentions.
#[test]
fn parity_back_edge_labels_number_by_first_printed_mention() {
let mut graph = FunctionGraph::new("loop");
let entry = graph.startblock;
let header = graph.create_block();
let exit = graph.create_block();

graph.set_goto(entry, header, vec![]);
let cond_var = graph
.push_op_var(header, OpKind::ConstInt(1), true)
.unwrap();
graph.set_branch(header, cond_var, header, vec![], exit, vec![]);
graph.set_return(exit, None);

let text = flat_to_text(&graph);
// Per-op result registers render `Variable.id()` (the
// PRE-EXISTING-ADAPTATION documented on `format_assembler`), a
// process-wide counter, so pin the label-bearing lines rather
// than the whole body.
let label_lines: Vec<&str> = text
.lines()
.filter(|line| line.contains("L1") || line.contains("L2"))
.collect();
assert_eq!(
label_lines,
vec!["L1:", "goto_if_not %i1, L2", "goto L1", "L2:"],
"loop header must be L1 and the forward exit L2: {text}",
);
}

/// `flatten.py:108-109` — a `make_link` whose target is the final
/// returnblock collapses into `make_return(link.args)`, so the
/// flattened text never carries a separate `Jump` to the final
Expand Down
101 changes: 78 additions & 23 deletions majit/majit-translate/src/codewriter/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
//!
//! - Registers print as `%i<n>`, `%r<n>`, `%f<n>`.
//! - Constants print as `$<value>` (matching `format.py:23`).
//! - Labels print as `L<index>` (assigned in textual order, matching
//! `format.py`'s `getlabelname`).
//! - Labels print as `L<index>`, numbered by first printed mention —
//! label line or jump operand, whichever comes first (matching
//! `format.py:45-50 getlabelname`).
//! - `ListOfKind` argument groups print as `I[…]`, `R[…]`, `F[…]`
//! (matching `format.py:27`).
//! - Call descriptors print via their `Debug` repr (matching
Expand Down Expand Up @@ -51,6 +52,28 @@ fn regorconst_repr(arg: &RegOrConst) -> String {
}
}

/// `format.py:45-50 getlabelname(lbl)`.
///
/// Numbering is lazy: a label takes the next ordinal the first time it is
/// *printed*, whether that mention is a jump operand (`format.py:24-25`
/// `repr(TLabel)`) or the label line itself (`format.py:61-63`). In a
/// graph with a back edge the two orders differ — the loop header's label
/// line prints before the back edge that targets it. `None` is the
/// `-1` sentinel the marking pass stores.
fn getlabelname(
label: Label,
seenlabels: &mut HashMap<Label, Option<usize>>,
labelcount: &mut usize,
) -> usize {
let slot = seenlabels.entry(label).or_insert(None);
if let Some(num) = *slot {
return num;
}
*labelcount += 1;
*slot = Some(*labelcount);
*labelcount
}

/// `format.py:12-81 format_assembler(ssarepr)`. Per-arg kinds for
/// `OpKind::Call` argument lists resolve via `getkind(v.concretetype)`
/// read directly from each operand `Variable`'s `concretetype` cell
Expand All @@ -70,17 +93,11 @@ fn regorconst_repr(arg: &RegOrConst) -> String {
/// `SSARepr` becomes self-contained and this formatter reads
/// `Register.index` with neither a graph nor the regalloc result.
pub fn format_assembler(ssarepr: &SSARepr) -> String {
// First pass: collect every label that appears as a target so the
// numbering matches format.py's getlabelname (labels are numbered in
// first-seen order).
let mut seenlabels: HashMap<Label, usize> = HashMap::new();
let mut next_label = 0usize;
let name_label = |label: Label, seen: &mut HashMap<Label, usize>, next: &mut usize| {
*seen.entry(label).or_insert_with(|| {
*next += 1;
*next
})
};
// format.py:37-44: the first pass only *marks* every label that
// appears as a jump target (`seenlabels[x.name] = -1`). It assigns
// no numbers — those come from `getlabelname` during the output loop
// below.
let mut seenlabels: HashMap<Label, Option<usize>> = HashMap::new();
for op in &ssarepr.insns {
match op {
FlatOp::Jump(label)
Expand All @@ -89,16 +106,17 @@ pub fn format_assembler(ssarepr: &SSARepr) -> String {
| FlatOp::IntBinOpJumpIfOvf { target: label, .. }
| FlatOp::GotoIfNot { target: label, .. }
| FlatOp::GotoIfNotOp { target: label, .. } => {
name_label(*label, &mut seenlabels, &mut next_label);
seenlabels.insert(*label, None);
}
FlatOp::Switch { targets, .. } => {
for (_, label) in targets {
name_label(*label, &mut seenlabels, &mut next_label);
seenlabels.insert(*label, None);
}
}
_ => {}
}
}
let mut labelcount = 0usize;

// format.py:53-55:
// insns = ssarepr.insns
Expand All @@ -120,8 +138,12 @@ pub fn format_assembler(ssarepr: &SSARepr) -> String {
None => String::new(),
};
match op {
// format.py:61-63 — only a label the marking pass saw as a
// jump target gets a line, and printing that line is itself a
// first mention that can claim the next ordinal.
FlatOp::Label(label) => {
if let Some(num) = seenlabels.get(label) {
if seenlabels.contains_key(label) {
let num = getlabelname(*label, &mut seenlabels, &mut labelcount);
let _ = writeln!(out, "{prefix}L{num}:");
}
}
Expand All @@ -134,30 +156,30 @@ pub fn format_assembler(ssarepr: &SSARepr) -> String {
}
}
FlatOp::Jump(label) => {
let num = name_label(*label, &mut seenlabels, &mut next_label);
let num = getlabelname(*label, &mut seenlabels, &mut labelcount);
let _ = writeln!(out, "{prefix}goto L{num}");
}
FlatOp::CatchException { target } => {
let num = name_label(*target, &mut seenlabels, &mut next_label);
let num = getlabelname(*target, &mut seenlabels, &mut labelcount);
let _ = writeln!(out, "{prefix}catch_exception L{num}");
}
FlatOp::GotoIfExceptionMismatch { llexitcase, target } => {
let num = name_label(*target, &mut seenlabels, &mut next_label);
let num = getlabelname(*target, &mut seenlabels, &mut labelcount);
let _ = writeln!(
out,
"{prefix}goto_if_exception_mismatch ${llexitcase}, L{num}"
);
}
FlatOp::GotoIfNot { cond, target } => {
let num = name_label(*target, &mut seenlabels, &mut next_label);
let num = getlabelname(*target, &mut seenlabels, &mut labelcount);
let _ = writeln!(out, "{prefix}goto_if_not {}, L{num}", cond.repr());
}
FlatOp::GotoIfNotOp {
opname,
args,
target,
} => {
let num = name_label(*target, &mut seenlabels, &mut next_label);
let num = getlabelname(*target, &mut seenlabels, &mut labelcount);
let arglist = args
.iter()
.map(|reg| reg.repr())
Expand All @@ -169,7 +191,7 @@ pub fn format_assembler(ssarepr: &SSARepr) -> String {
let cases: Vec<String> = targets
.iter()
.map(|(key, label)| {
let num = name_label(*label, &mut seenlabels, &mut next_label);
let num = getlabelname(*label, &mut seenlabels, &mut labelcount);
format!("{key}:L{num}")
})
.collect();
Expand All @@ -192,7 +214,7 @@ pub fn format_assembler(ssarepr: &SSARepr) -> String {
crate::flatten::IntOvfOp::Sub => "int_sub_jump_if_ovf",
crate::flatten::IntOvfOp::Mul => "int_mul_jump_if_ovf",
};
let num = name_label(*target, &mut seenlabels, &mut next_label);
let num = getlabelname(*target, &mut seenlabels, &mut labelcount);
let _ = writeln!(
out,
"{prefix}{opname} L{num}, {}, {} -> {}",
Expand Down Expand Up @@ -820,6 +842,39 @@ mod tests {
assert!(text.contains("L1:"));
}

#[test]
fn format_label_numbering_follows_first_printed_mention() {
// `format.py:37-44` only marks the labels that appear as jump
// targets; `format.py:45-50 getlabelname` assigns the numbers
// lazily, so a label takes its ordinal from its first *printed*
// mention — the label line counts just as much as a jump operand.
//
// In a loop the header's label line prints before the back edge
// that targets it, so the header is L1 even though the forward
// exit is the first target the instruction stream mentions.
let header = Label(10);
let exit = Label(20);
let mut ssa = empty_ssa();
ssa.insns.push(FlatOp::Label(header));
ssa.insns.push(FlatOp::GotoIfNot {
cond: crate::flatten::Register::new(RegKind::Int, 0),
target: exit,
});
ssa.insns.push(FlatOp::Jump(header));
ssa.insns.push(FlatOp::EndOfBlock);
ssa.insns.push(FlatOp::Label(exit));
assert_format(
&ssa,
"
L1:
goto_if_not %i0, L2
goto L1
---
L2:
",
);
}

#[test]
fn format_switch_uses_switchdictdescr_repr() {
let mut ssa = empty_ssa();
Expand Down
Loading
Loading