jit: deoptimize instead of trapping or wrapping, and add an aot feature by youknowone · Pull Request #8624 · RustPython/RustPython · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0aa370d
jit: compile into a shared module owned by JitEngine
youknowone Aug 24, 2026
807f050
jit: add a Strict safety level that rejects diverging operations
youknowone Aug 24, 2026
eb9b0bd
jit: add supports_code as a pre-filter for speculative callers
youknowone Aug 24, 2026
a7388af
vm: add an aot feature that compiles functions on their first call
youknowone Aug 24, 2026
b9cd4ea
jit: reject the compiled self-call under Strict
youknowone Aug 24, 2026
09f2698
ci: build with aot and run the snippet in both modes
youknowone Aug 24, 2026
f4b8c2f
vm: let an interrupt out of a speculative annotation read
youknowone Aug 24, 2026
45059c9
extra_tests: gate the aot snippet on a RustPython-only symbol
youknowone Aug 24, 2026
2899448
jit: build the libffi cif once per compiled function
youknowone Aug 24, 2026
84501c7
jit: fold the two LoadGlobal rejections into one condition
youknowone Aug 24, 2026
39ba2b1
jit: call compiled code through a generated entry point
youknowone Aug 24, 2026
d8dfa4a
vm: trim the jit call path and its build warnings
youknowone Aug 24, 2026
dec60b0
vm: compute f_lineno from the instruction pointer
youknowone Aug 25, 2026
b648f30
jit: pin down what a conditionally-defined local compiles to
youknowone Aug 30, 2026
e6480cd
jit: pass a deopt buffer through the call boundary
youknowone Aug 30, 2026
9ce4ec2
jit: spill locals and the value stack at a deopt site
youknowone Aug 30, 2026
8bf155b
jit: deoptimize instead of trapping or wrapping on int overflow
youknowone Aug 30, 2026
c17a00f
jit: correct the bound-mask comment in the deopt buffer layout
youknowone Aug 30, 2026
27fed67
jit: floor int division and take the divisor's sign for the remainder
youknowone Aug 30, 2026
d33ec18
jit: derive the floor-division remainder from the quotient
youknowone Aug 30, 2026
262e656
jit: deoptimize where a power or a float division leaves the reals
youknowone Aug 30, 2026
88857f9
jit: close the float power trap and its remaining wrong answers
youknowone Aug 30, 2026
4647c24
jit: revive the 1e100 ** 1e50 crash as a deopt test
youknowone Aug 30, 2026
88ce77e
jit: remove compile_fpow's edge cases now unreachable under the expon…
youknowone Aug 30, 2026
d7a8665
jit: let Strict compile arithmetic now that it deoptimizes
youknowone Aug 30, 2026
06934c2
jit: replace compile_fpow's double-double pow with a call to f64::powf
youknowone Aug 30, 2026
f9ae2f1
jit: give aot.py's third widening case its own compiled function, rai…
youknowone Aug 30, 2026
1750218
vm: resume the interpreter from a deopt record
youknowone Aug 30, 2026
96168d0
jit: name the whole observer class for a non-resumable site
youknowone Aug 30, 2026
2eb5fa8
jit: document slot 0's third status and Permissive's resolution point
youknowone Aug 30, 2026
ee0886a
extra_tests: assert compiled arithmetic answers what the interpreter …
youknowone Aug 30, 2026
aba23c3
extra_tests: cover binary add and subtract in the equivalence table
youknowone Aug 30, 2026
c7569ef
jit: refuse a control-flow merge reached with a non-empty stack
youknowone Aug 30, 2026
4be3574
vm: check a deopt record against the frame before resuming from it
youknowone Aug 30, 2026
4834c3f
vm: interpret a call while a tracer or a monitoring tool is installed
youknowone Aug 30, 2026
3fb4f23
jit: document the safety of the checked invoke path
youknowone Aug 30, 2026
be01d51
ci: build the aot step against the locked dependency versions
youknowone Aug 30, 2026
91788b0
jit: make the pre-filter's merge rejection actually fire
youknowone Aug 31, 2026
09a1d8d
jit: pin the pre-filter's rejection of a mid-expression merge
youknowone Aug 31, 2026
6e95e36
vm: report the automatic compiler from sys._jit
youknowone Aug 31, 2026
2c0661d
jit: move a doc comment onto the function it describes
youknowone Aug 31, 2026
dfc84fa
jit: keep a true-division operand of 1 << 53 compiled
youknowone Aug 31, 2026
5b5f9ac
extra_tests: assert scale's value instead of a division it does not do
youknowone Aug 31, 2026
60ee4c9
ci: build the benchmarks with automatic compilation on
youknowone Aug 31, 2026
295b93d
vm: record the traced line even where a frame carries no trace function
youknowone Aug 31, 2026
f640964
vm: attach the frame object to the caller when an escaped frame returns
youknowone Sep 2, 2026
1dbd9f6
vm: resolve f_back from the thread still running the frame
youknowone Sep 2, 2026
c65cad7
vm: read a running frame's position from the thread running it
youknowone Sep 2, 2026
8074517
ci: run part of the CPython suite with automatic compilation on
youknowone Sep 2, 2026
640aab5
vm: set a resumed frame's offset through a method on the frame
youknowone Sep 2, 2026
7ca3cae
jit: poll the eval breaker at every backward jump
youknowone Sep 2, 2026
07d937c
vm: decide a float power by its operands' values, not their sign bits
youknowone Sep 2, 2026
6c62636
common: keep the sign of a zero quotient
youknowone Sep 2, 2026
82aa257
docs: describe what the automatic compiler takes and turns down
youknowone Sep 2, 2026
65822ed
vm: end the finalizing span with the shutdown that opened it
youknowone Sep 2, 2026
ba229e1
ci: build the benchmarks without automatic compilation again
youknowone Sep 2, 2026
ffb2740
vm: compile a function once it is warm, on the types its calls pass
youknowone Sep 3, 2026
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
4 changes: 4 additions & 0 deletions .cspell.dict/rust-more.txt
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"deduped",
"deoptimized",
"deoptimize",
"deoptimizes",
"emscripten",
"excs",
"fdigits",
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ jobs:
run: cargo build --locked --no-default-features --features ssl-openssl-vendor
if: runner.os == 'Linux'

# Automatic compilation must not change what a program does, so the
# snippet has to pass identically with it on and off.
- name: Test aot build
run: |
cargo build --locked --features aot
target/debug/rustpython -X aot=1 extra_tests/snippets/aot.py
target/debug/rustpython -X aot=0 extra_tests/snippets/aot.py
target/debug/rustpython -X aot=1 extra_tests/snippets/jit.py
if: runner.os == 'Linux'

# The snippets cover arithmetic. These cover the rest of what the
# automatic call path moves through: frames, tracebacks, tracing, and
# the threads that have to be able to leave a compiled loop.
- name: Test aot against the CPython suite
run: |
target/debug/rustpython -X aot=1 -m test \
test_sys test_traceback test_sys_settrace test_monitoring \
test_bdb test_trace test_exceptions test_generators test_threading
if: runner.os == 'Linux'

# - name: Install tk-dev for tkinter build
# run: sudo apt-get update && sudo apt-get install -y tk-dev
# if: runner.os == 'Linux'
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ stdlib = ["rustpython-stdlib", "rustpython-pylib", "encodings"]
flame-it = ["rustpython-vm/flame-it", "rustpython-stdlib/flame-it", "flame", "flamescope"]
freeze-stdlib = ["stdlib", "rustpython-vm/freeze-stdlib", "rustpython-pylib?/freeze-stdlib"]
jit = ["rustpython-vm/jit"]
aot = ["rustpython-vm/aot"]
threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"]
sqlite = ["rustpython-stdlib/sqlite"]
ssl = ["host_env"]
Expand Down
59 changes: 54 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,70 @@ cargo build --release --target wasm32-wasip1 --features="freeze-stdlib"

### JIT (Just in time) compiler

RustPython has a **very** experimental JIT compiler that compile python functions into native code.
RustPython has a **very** experimental JIT compiler that compiles python functions into native code.
It comes in two forms: an automatic one that compiles a function once it has been called
often enough to be worth it, and an explicit `__jit__()` that compiles the one function
it is called on.

#### Building

By default the JIT compiler isn't enabled, it's enabled with the `jit` cargo feature.
Neither is built by default.

```bash
cargo run --features jit
cargo run --features aot # automatic, and the explicit one with it
cargo run --features jit # explicit `__jit__()` alone
```

This requires autoconf, automake, libtool, and clang to be installed.

#### Using
#### Using the automatic compiler

To compile a function, call `__jit__()` on it.
A build that has it still has to be switched on, with `-X aot=1`, `RUSTPYTHON_AOT=1`
or `PYTHON_JIT=1`; `-X aot=0` and `=0` switch it back off. A function is then counted
as it is called and offered to the compiler once it has been called enough times to
repay one. The attempt happens once, so a function it turns down costs that one
attempt and is interpreted from then on; a function called only a handful of times
is never offered at all.

`sys._jit.is_available()` reports whether the compiler was built in, `is_enabled()`
whether it is switched on, and `_stats()` returns `(compiled, rejected, deoptimized)`
for the functions it has looked at so far — a RustPython extension.

#### What it compiles

Scalar functions, and nothing else. The two forms differ in where the types come
from: `__jit__()` reads them off the annotations, and turns down a function without
them; the automatic path takes them from the arguments of the call that made the
function warm, so an unannotated function compiles as readily as an annotated one.
Nothing about that reads a `__annotations__` or runs an `__annotate__`.

A guess about types is a guess: a later call whose arguments do not fit the compiled
signature is run by the interpreter instead.

Taken: `int`, `float` and `bool` arguments, locals and return values; arithmetic,
comparison and boolean operators; `if`, `while`, and the assignments between them.

Turned down: arguments of any other type, `*args`/`**kwargs`, closures, generators
and coroutines, `try`/`except`, attributes and methods, containers, `for`, calls to
anything but the function itself, and expressions that merge with an operand still
on the stack, such as a conditional expression. A call a function makes to itself
is compiled only by `__jit__()`; the automatic path turns those down too, because
the global it goes through can be rebound between one call and the next.

Where a machine word runs out — an overflow, a division by zero, a shift past the
width, a power with no real answer — the compiled code hands the frame back at the
instruction it could not do, with the values it had, and the interpreter carries on
from there. The native code is dropped at that point, and the function is
interpreted afterwards.

Compiled code runs with no python frame. That is why `sys._jit.is_active()` is
always `False`, why such a call reports no line and no return to `sys.settrace` or
`sys.monitoring`, and why a call is interpreted, and left uncompiled, while either
of those is installed.

#### Using `__jit__()`

To compile a single function, call `__jit__()` on it. This needs only the `jit` feature.

```python
def foo():
Expand Down
4 changes: 2 additions & 2 deletions crates/capi/src/pyframe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ pub unsafe extern "C" fn PyFrame_GetCode(frame: *mut PyFrameObject) -> *mut PyCo

#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyFrame_GetLineNumber(frame: *mut PyFrameObject) -> c_int {
with_vm(|_vm| {
let lineno = unsafe { &*frame }.f_lineno();
with_vm(|vm| {
let lineno = unsafe { &*frame }.f_lineno(vm);
Ok(lineno.try_into().unwrap_or(c_int::MAX))
})
}
7 changes: 7 additions & 0 deletions crates/common/src/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ use num_traits::{One, ToPrimitive, Zero};

#[must_use]
pub fn true_div(numerator: &BigInt, denominator: &BigInt) -> f64 {
// A rational carries no signed zero, so `0 / -1` would round to `0.0`. A
// quotient of two differently signed operands is negative down to and
// including its zero, and only an exactly zero numerator loses that here:
// a quotient too small to represent still rounds to `-0.0` on its own.
if numerator.is_zero() && denominator.sign() == Sign::Minus {
return -0.0;
}
let rational = Rational::from_integers_ref(numerator.into(), denominator.into());
match rational.rounding_into(RoundingMode::Nearest) {
// returned value is $t::MAX but still less than the original
Expand Down
1 change: 0 additions & 1 deletion crates/jit/Cargo.toml
Loading
Loading