Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
683e806
perf: hash and compare plain instances by identity without interprete…
owenthcarey Oct 1, 2026
f73a4ca
perf: make the cycle collector and weakrefs non-owning so objects die…
owenthcarey Oct 1, 2026
a6f6301
perf: compile leaf plans to native code and inline property getters
owenthcarey Oct 1, 2026
16b876a
build: record the VM crate's direct Cranelift dependencies in the loc…
owenthcarey Oct 1, 2026
8f2bec8
perf: release returning frames' locals directly and widen native plan…
owenthcarey Oct 1, 2026
603677c
perf: fast-step generators whose prologue runs other instructions
owenthcarey Oct 1, 2026
7e9d50b
perf: run exception handlers off the full prologue and match own-clas…
owenthcarey Oct 1, 2026
cdee10d
perf: cache native plans' field positions per class and global locati…
owenthcarey Oct 1, 2026
f3e50e8
perf: read __slots__ scalar fields in tier 2's attribute fast path
owenthcarey Oct 1, 2026
eedc36a
perf: give Object a defined layout for native code
owenthcarey Oct 1, 2026
ae879b6
perf: evaluate leaf property getters in place and nurse young instances
owenthcarey Oct 1, 2026
21eb465
perf: read and write scalar instance fields in line in tier 2
owenthcarey Oct 1, 2026
48cedc1
perf: read leaf plans' cached instance fields in line
owenthcarey Oct 1, 2026
aaaf3a6
perf: read, write, append to and iterate scalar lists in line in tier 2
owenthcarey Oct 1, 2026
01ade8d
perf: memoise str lengths in the string and build string results in p…
owenthcarey Oct 1, 2026
1532324
perf: decode native pickles in one pass
owenthcarey Oct 1, 2026
b1c9d66
perf: encode lists and dicts without snapshots, and probe the memo once
owenthcarey Oct 1, 2026
f7b65a1
perf: decode without store-forwarding stalls, and reuse state tables
owenthcarey Oct 2, 2026
7b6f113
perf: give the encoder's memo an open-addressing table
owenthcarey Oct 2, 2026
199ff25
perf: peek at the next instruction without memcmp
owenthcarey Oct 2, 2026
0338165
perf: compile hot loops the tier-2 compiler leaves behind to native code
owenthcarey Oct 2, 2026
545af0f
perf: compile leaf plans only once interpreting them has cost a compile
owenthcarey Oct 2, 2026
9cf597e
perf: run field-update methods in line and reuse element pins in tier 2
owenthcarey Oct 2, 2026
fce80f7
perf: trim the native call path's guard, pin and exit work
owenthcarey Oct 2, 2026
8a64dee
perf: call compiled scalar leaves directly when defaults fill the gap
owenthcarey Oct 2, 2026
5e00d12
perf: plan scalar field updates over __slots__ members
owenthcarey Oct 2, 2026
7cf72a2
perf: lighten native calls, constructor guards and exit write-backs
owenthcarey Oct 2, 2026
f6c34ce
perf: evaluate keyword calls of pure leaves straight from tier 2
owenthcarey Oct 2, 2026
c0f8a65
perf: append constructor fields in line and return objects without th…
owenthcarey Oct 2, 2026
2781ae8
perf: compile tier-2 code without Cranelift's mid-end optimizer
owenthcarey Oct 2, 2026
002bf5e
test: count armed and slot field updates in the update coverage test
owenthcarey Oct 2, 2026
b38b6c5
perf: plan local write-backs before lowering and merge the first guar…
owenthcarey Oct 2, 2026
6ce459c
perf: let compiled loops keep pins whose objects live on elsewhere
owenthcarey Oct 2, 2026
f42ad95
perf: size a native constructor's fields up front and keep big spare …
owenthcarey Oct 2, 2026
ac52e57
perf: compile generator bodies the fast steps run without waiting on …
owenthcarey Oct 2, 2026
06f2bb7
perf: allocate the frame compiler's registers with the backtracking a…
owenthcarey Oct 2, 2026
eab0132
perf: step nested generators without reference churn or a thread-loca…
owenthcarey Oct 2, 2026
8faf477
perf: evaluate pure-leaf functions in place when native code calls them
owenthcarey Oct 2, 2026
70dd3d2
perf: evaluate subscripts in frameless leaf bodies
owenthcarey Oct 2, 2026
fcfecbf
perf: build and step map and filter natively over native iterables
owenthcarey Oct 2, 2026
c8ef39f
perf: build sets of simple keys from lists and tuples in one sized pass
owenthcarey Oct 2, 2026
78e40b2
perf: run big-int arithmetic in the core loop
owenthcarey Oct 2, 2026
cdb1101
perf: share functions' name objects and presize their slot table
owenthcarey Oct 2, 2026
8a5f405
perf: call native deque operations from compiled code
owenthcarey Oct 2, 2026
22c6ee2
perf: recompile from a later loop after a cold exit
owenthcarey Oct 2, 2026
66fbc3c
perf: build deque iterators natively
owenthcarey Oct 2, 2026
3dd9f16
perf: load cached code's column spans on first use, and Fx-hash the i…
owenthcarey Oct 2, 2026
c6122b3
perf: check isinstance against tuples of classes in the core loop
owenthcarey Oct 2, 2026
9421987
perf: store natively built datetime values packed, with a per-class pool
owenthcarey Oct 2, 2026
964383b
perf: call a bound datetime.fromisoformat in the core loop
owenthcarey Oct 2, 2026
73d7d23
perf: keep datetime operators and fields in the frame compiler's code
owenthcarey Oct 2, 2026
09ae555
perf: read method, class-constant and global caches in line in compil…
owenthcarey Oct 2, 2026
8b04438
perf: compile leaf plans without the mid-end optimizer, and long fram…
owenthcarey Oct 2, 2026
5d3a905
perf: read module-global lists on their list lanes in tier 2
owenthcarey Oct 2, 2026
3531174
perf: compile int compares with object operands and identity tests in…
owenthcarey Oct 2, 2026
73f8efc
perf: land a compiled effect leaf's last store at once
owenthcarey Oct 2, 2026
b7413c9
perf: step dict.items() loops in tier 2 without building pairs
owenthcarey Oct 2, 2026
7ebc3fb
perf: check isinstance against a tuple of plain classes in place
owenthcarey Oct 2, 2026
e8475ba
perf: make functions in the leaf burst and key call shapes by code
owenthcarey Oct 2, 2026
67006d7
perf: build Rust-side exceptions without name lookups or eager messages
owenthcarey Oct 2, 2026
d571fae
perf: catch exceptions in the quiet loop and record tracebacks lazily
owenthcarey Oct 2, 2026
32ff811
perf: trim the remaining per-exception bookkeeping
owenthcarey Oct 3, 2026
62f9ba7
perf: run re.Pattern and re.Match methods natively
owenthcarey Oct 2, 2026
c46c23b
perf: skip regex tails that cannot start, and count classes in one loop
owenthcarey Oct 3, 2026
cea4bfd
perf: test regex sets by table, and find short finditer matches up front
owenthcarey Oct 3, 2026
a21df4d
perf: skip regex start positions for case-folded literals and branches
owenthcarey Oct 3, 2026
b33a0a4
perf: switch to plain Python callees and back with less bookkeeping
owenthcarey Oct 3, 2026
0ce6ba9
perf: store a compiled effect leaf's scalar field in line
owenthcarey Oct 3, 2026
7136380
perf: make functions in the leaf burst and keep young ones off the GC…
owenthcarey Oct 3, 2026
569aba4
perf: run cell-variable, `*args` and `with` calls inline in the core …
owenthcarey Oct 3, 2026
574aadf
perf: keep several classes' leaf methods per call site and enter warm…
owenthcarey Oct 3, 2026
ed6f038
perf: store `__slots__` members in line and trim the inline call path
owenthcarey Oct 3, 2026
720ce88
perf: share a new function's name slots and run closures over free va…
owenthcarey Oct 3, 2026
dde35a2
perf: keep the frame compiler to tier 2's start-up and import budget
owenthcarey Oct 3, 2026
f7448fe
perf: build the opcode shim's stack-effect tables on first use
owenthcarey Oct 3, 2026
3f3f032
perf: run class-body machinery and metaclass attribute reads directly
owenthcarey Oct 3, 2026
dc40bc7
style: format, and satisfy Rust 1.99's deprecations and Clippy lints
owenthcarey Oct 3, 2026
94fdb3a
perf: keep Cranelift's mid-end on for tier 2
owenthcarey Oct 3, 2026
7dd3c53
perf: count attribute reads that leave native code against frame comp…
owenthcarey Oct 3, 2026
e7d5aa3
fix: park a frame that caught an exception through the full path
owenthcarey Oct 3, 2026
78c5a0e
fix: avoid an unsigned underflow in the regex prefix search
owenthcarey Oct 3, 2026
7c93771
fix: render OSError as `[Errno N] strerror` in unraisable reports
owenthcarey Oct 3, 2026
550fe92
fix: let threads and signals in after the quiet loop catches an excep…
owenthcarey Oct 3, 2026
46a97fc
docs: report the branch's final numbers against CPython and main
owenthcarey Oct 3, 2026
d0b76ef
ci: cap lints when building the bench gate's merge-base reference
owenthcarey Oct 3, 2026
e478a16
fix: drop an invalid extension from the plan JIT's in-line method cache
owenthcarey Oct 3, 2026
904f4a9
ci: diagnose call_overhead when the Windows bench gate fails
owenthcarey Oct 3, 2026
bbd274b
ci: time call_overhead directly when the Windows bench gate fails
owenthcarey Oct 3, 2026
aec04c0
ci: time call_overhead through the bench harness when the Windows gat…
owenthcarey Oct 4, 2026
5d33998
ci: compare absolute and relative fixture paths when the Windows gate…
owenthcarey Oct 4, 2026
8998768
ci: isolate the JIT tier behind the Windows absolute-path slowdown
owenthcarey Oct 4, 2026
3025dc1
ci: record VM stats for the Windows absolute-path slowdown
owenthcarey Oct 4, 2026
a0d23e0
ci: profile the Windows absolute-path slowdown with samply
owenthcarey Oct 4, 2026
e00f662
ci: keep the Windows binaries and PDBs with the call_overhead profiles
owenthcarey Oct 4, 2026
e753adf
perf: reuse a keyword leaf call's `**kwargs` dictionary
owenthcarey Oct 4, 2026
b8adb6b
ci: drop the temporary call_overhead diagnostic step
owenthcarey Oct 5, 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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,13 @@ jobs:
# rust-cache still primes dependency compilation, and the
# merge-base is usually a recent main commit CI has built
# before, so this step is mostly cache hits.
#
# The reference binary is only timed, so a lint that a newer
# toolchain added (one the merge-base predates) mustn't fail
# the build: lints are capped at warnings here.
shell: bash
env:
RUSTFLAGS: "--cap-lints=warn"
run: |
BASE_SHA=$(git merge-base "${{ github.event.pull_request.base.sha }}" HEAD)
echo "merge-base: $BASE_SHA"
Expand Down Expand Up @@ -263,6 +269,7 @@ jobs:
python tools/bench_cold_jit.py \
--base "$BASE_WEAVEPY" --new target/release/weavepy.exe \
--samples 7 \
--fixtures sumvm:2000000,nested_loops:120,jitloop:1000,call_overhead:150000 \
--out "target/performance/cold-jit-ci/${{ github.run_id }}-${{ github.run_attempt }}/report.json"
- uses: actions/upload-artifact@v4
if: always() && steps.cold_jit_diagnostic.outcome != 'skipped' && steps.cold_jit_diagnostic.outcome != ''
Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ manual_map = "allow"
comparison_chain = "allow"
unnecessary_cast = "allow"
len_without_is_empty = "allow"
# New in Clippy 1.99; tests across the workspace assert emptiness this way.
assert_is_empty = "allow"
# Interpreter compares IEEE-754 values exactly by design (Python `==`,
# rounding, struct packing); an epsilon would be incorrect.
float_cmp = "allow"
Expand Down
6 changes: 3 additions & 3 deletions crates/weavepy-capi/src/gc_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ fn instance_type_ptr(obj: &Object) -> Option<*mut PyTypeObject> {
// An instance that never crossed into C (no box, no inline body)
// has no C-held edges for `tp_traverse`/`tp_clear` to report or
// drop: the VM traverses its own fields. This keeps the bridge —
// a mint, a C call and a free per node — off the prompt-reap
// cascade of every native exception instance.
// a mint, a C call and a free per node — off every collection
// that visits a native exception instance.
Object::Instance(i) if i.c_body.get() != 0 => crate::types::type_ptr_for_class(&i.cls()),
_ => None,
}
Expand Down Expand Up @@ -212,7 +212,7 @@ pub unsafe extern "C" fn PyObject_GC_Track(op: *mut c_void) {
}
let obj = unsafe { crate::object::clone_object(op as *mut PyObject) };
if matches!(obj, Object::Instance(_)) {
weavepy_vm::gc_trace::track(obj);
weavepy_vm::gc_trace::track(&obj);
}
}

Expand Down
30 changes: 5 additions & 25 deletions crates/weavepy-capi/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,30 +329,10 @@ pub unsafe fn release_c_ownership(p: *mut PyObject) {
.lock()
.ok()
.and_then(|mut g| g.as_mut().and_then(|m| m.remove(&(p as usize))));
if let Some(inst) = &pinned {
if pinned.is_some() {
// C just dropped what may be this instance's last program-visible
// reference (CPython would run `tp_dealloc` right here). If the VM
// side still pins it through GC-handle / weakref-registry clones, a
// plain `Rc` drop below cannot reap it — a *tracked* extension
// temporary that dies inside a C call (a Cython generator abandoned
// by `BlockManager.iget`, still holding `self` in its closure) would
// otherwise stay pinned by its own handle until the next full
// collection, keeping everything it references alive with it
// (pandas' `_is_view_after_cow_rules` then reads a stale live
// `Block` weakref). Park it for the eval loop's between-bytecodes
// reap; anything still genuinely alive fails the drain's
// refcount-dead test untouched.
//
// Only a *GC-tracked* instance needs the park: an untracked one has
// no handle pinning it, so the plain `Rc` drop below reclaims it
// immediately (running `tp_dealloc` through the free hook) — and a
// queued clone would instead keep it alive until the next eval-loop
// safe point, which never comes for a drop performed outside
// bytecode execution (an embedding host dropping its last handle).
let obj = weavepy_vm::object::Object::Instance(inst.clone());
if weavepy_vm::gc_trace::is_tracked(weavepy_vm::weakref_registry::id_of(&obj)) {
weavepy_vm::vm_singletons::queue_cext_dropped(&obj);
}
// reference; the plain `Rc` drop below reclaims it then (running
// `tp_dealloc` through the free hook), as CPython's would.
} else if unsafe { crate::mirror::is_orphaned_instance_body(p) }
&& !body_free_in_flight(p as usize)
{
Expand Down Expand Up @@ -618,8 +598,8 @@ unsafe fn dealloc_and_free_body(p: *mut PyObject) {
(*ty).tp_basicsize = orig_basicsize.wrapping_add(8);
// A `tp_dealloc` is extension code: its decref chains can
// re-enter the VM, and any bytecode that runs beneath it
// must see a live C frame (RFC 0047 — the prompt reaper's
// borrowed-pointer window).
// must see a live C frame (RFC 0047 — the borrowed-pointer
// window that defers parked C drops).
let _cext_guard = weavepy_vm::vm_singletons::enter_cext_call();
// Consent protocol: the dealloc must reach a `tp_free`
// (absorbed by `PyObject_Free`/`PyObject_GC_Del`, which
Expand Down
9 changes: 4 additions & 5 deletions crates/weavepy-capi/src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,10 @@ pub fn ensure_active<R>(body: impl FnOnce() -> R) -> R {
// RFC 0047 (wave 5): mark a C-extension call live on this thread for
// the duration of `body`. Every bridged C invocation (dunder shims,
// `PyCFunction`s, descriptors, tp_call, foreign hooks) funnels through
// here, so the VM's prompt reaper can tell "plain bytecode, no
// extension frame below" (`depth == 0` — reclaiming a dead escaped
// subgraph is as safe as CPython's own `tp_dealloc`) from "inside a C
// call that may hold borrowed body pointers" (`depth > 0` — defer
// memory reclamation to the tracing collector).
// here, so the VM can tell "plain bytecode, no extension frame below"
// (`depth == 0` — releasing objects C dropped is as safe as CPython's
// own `tp_dealloc`) from "inside a C call that may hold borrowed body
// pointers" (`depth > 0` — keep them parked).
let _cext_guard = weavepy_vm::vm_singletons::enter_cext_call();
if current_interpreter_mut().is_some() {
if ea_trace_enabled() {
Expand Down
15 changes: 7 additions & 8 deletions crates/weavepy-capi/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1659,14 +1659,13 @@ pub(crate) unsafe fn free_box(p: *mut PyObject) {
evict_property_box(weavepy_vm::sync::Rc::as_ptr(rc) as usize, p);
}
// RFC 0047 (wave 5): C is dropping what may be the payload's last
// program-visible reference, from inside an extension call the VM's
// prompt reaper cannot see. Park instance/container payloads for a
// refcount-guarded reap at the next eval-loop safe point so anything
// that died here gets its weakrefs cleared with CPython's timing
// (pandas' `BlockValuesRefs.has_reference` prunes dead block
// weakrefs the very next Python-level call after a Cython-internal
// `self.blocks = ...` rebind). A payload that is still alive
// elsewhere fails the reap's deadness test untouched.
// program-visible reference, from inside an extension call that may
// still hold borrowed body pointers. Park instance/container payloads
// and release them at the next eval-loop safe point outside any C
// call, so anything that dies there gets its weakrefs cleared before
// the next Python-level read (pandas' `BlockValuesRefs.has_reference`
// prunes dead block weakrefs right after a Cython-internal
// `self.blocks = ...` rebind).
weavepy_vm::vm_singletons::queue_cext_dropped(&bx.payload.obj);
if let Some(d) = bx.payload.destructor {
let raw = Box::into_raw(bx);
Expand Down
2 changes: 1 addition & 1 deletion crates/weavepy-capi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2585,7 +2585,7 @@ fn inherit_inline_base_layout(
};

let bases = t.bases.borrow();
let first_ptr = bases.first().and_then(&resolve);
let first_ptr = bases.first().and_then(resolve);

// CPython's `best_base()`: the "solid base" is the base with the widest
// instance layout (`tp_basicsize`), scanned across *all* bases — not
Expand Down
2 changes: 1 addition & 1 deletion crates/weavepy-compiler/src/cpython_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1480,7 +1480,7 @@ fn encode_exception_table(code: &CodeObject, starts: &[usize]) -> Vec<u8> {
/// depths for inlined comprehensions; entries the walk never reaches
/// stay -1. Handlers whose depth is still the sentinel do not seed
/// the walk (their depth is exactly what's being computed).
pub(crate) fn compute_startdepths(code: &CodeObject) -> Vec<i64> {
pub fn compute_startdepths(code: &CodeObject) -> Vec<i64> {
use OpCode as O;
let n = code.instructions.len();
let mut startdepth: Vec<i64> = vec![-1; n];
Expand Down
2 changes: 1 addition & 1 deletion crates/weavepy-compiler/src/flowgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3092,7 +3092,7 @@ impl Cfg {
}
co.instructions = instructions;
co.linetable = linetable;
co.coltable = coltable;
co.coltable = coltable.into();
co.exception_table = table;
co.constants = self.consts;
no_interrupt.sort_unstable();
Expand Down
84 changes: 81 additions & 3 deletions crates/weavepy-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ impl JitHint {
true
}

/// Whether the tier-2 state has no further interest in this code's
/// back edges (see [`Self::set_backedge_quiet`]).
#[inline]
pub fn is_backedge_quiet(&self) -> bool {
self.backedge_quiet
.load(std::sync::atomic::Ordering::Relaxed)
}

pub fn set_backedge_quiet(&self) {
self.backedge_quiet
.store(true, std::sync::atomic::Ordering::Relaxed);
Expand Down Expand Up @@ -504,8 +512,9 @@ pub struct CodeObject {
/// PEP-657 fine-grained column spans, one per instruction (same length
/// as `instructions` once emission finishes). Drives the column fields
/// of `co_positions()`. Empty when never populated (e.g. code objects
/// reconstructed from marshal, which doesn't carry columns).
pub coltable: Vec<ColSpan>,
/// reconstructed from marshal, which doesn't carry columns). Decoded
/// from the code cache on first use (see [`ColTable`]).
pub coltable: ColTable,
/// Number of positional + keyword arguments (excluding `*args`/`**kwargs`).
pub arg_count: u32,
/// Number of positional-only arguments.
Expand Down Expand Up @@ -625,6 +634,74 @@ pub struct WireOverrides {

/// A per-instruction source-column span (PEP-657). `col`/`end_col` are
/// 0-based UTF-8 byte offsets within their respective source lines, and
/// A code object's column spans (see [`CodeObject::coltable`]): a
/// vector, or the code cache's encoding of one, decoded on first use.
/// Most code never reports a column (only tracebacks and
/// `co_positions()` read them), so a module loaded from the cache skips
/// the work until something does.
#[derive(Clone, Default)]
pub struct ColTable {
spans: std::sync::OnceLock<Vec<ColSpan>>,
encoded: Option<std::sync::Arc<[u8]>>,
}

impl ColTable {
/// The spans `encoded` holds in the code cache's form (see
/// `native_code`), decoded when first read.
pub(crate) fn encoded(encoded: std::sync::Arc<[u8]>) -> Self {
ColTable {
spans: std::sync::OnceLock::new(),
encoded: Some(encoded),
}
}

fn spans(&self) -> &Vec<ColSpan> {
self.spans.get_or_init(|| {
self.encoded
.as_deref()
.and_then(native_code::decode_coltable)
.unwrap_or_default()
})
}
}

impl From<Vec<ColSpan>> for ColTable {
fn from(spans: Vec<ColSpan>) -> Self {
ColTable {
spans: std::sync::OnceLock::from(spans),
encoded: None,
}
}
}

impl std::ops::Deref for ColTable {
type Target = Vec<ColSpan>;

fn deref(&self) -> &Vec<ColSpan> {
self.spans()
}
}

impl std::ops::DerefMut for ColTable {
fn deref_mut(&mut self) -> &mut Vec<ColSpan> {
self.spans();
self.encoded = None;
self.spans.get_mut().expect("decoded above")
}
}

impl PartialEq for ColTable {
fn eq(&self, other: &Self) -> bool {
**self == **other
}
}

impl std::fmt::Debug for ColTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
(**self).fmt(f)
}
}

/// are `-1` when the column was not tracked. `end_lineno` is `0` when
/// unknown (callers fall back to the instruction's start line).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -2623,7 +2700,8 @@ impl Compiler {
}
};
self.co.linetable.push(line);
self.co.coltable.push(self.resolve_colspan());
let span = self.resolve_colspan();
self.co.coltable.push(span);
offset
}

Expand Down
41 changes: 26 additions & 15 deletions crates/weavepy-compiler/src/native_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::bytecode::{CacheTable, Instruction, OpCode};
use crate::{CodeObject, ColSpan, Constant, ExcHandler};

/// The layout revision; bump it whenever the encoding changes.
pub const VERSION: u8 = 2;
pub const VERSION: u8 = 3;

/// Encode `code` (and its nested code objects). `None` for a code object
/// the format doesn't carry: one with raw CPython wire overrides, or with
Expand All @@ -28,6 +28,21 @@ pub fn encode(code: &CodeObject) -> Option<Vec<u8>> {
Some(w.0)
}

/// Decode a column-span block [`encode`] wrote (see `ColTable`).
pub(crate) fn decode_coltable(bytes: &[u8]) -> Option<Vec<ColSpan>> {
let mut r = Reader { bytes, pos: 0 };
let end_lines = r.deltas()?;
let mut spans = Vec::with_capacity(end_lines.len());
for end_lineno in end_lines {
spans.push(ColSpan {
end_lineno,
col: r.i32()?,
end_col: r.i32()?,
});
}
(r.pos == bytes.len()).then_some(spans)
}

/// Decode what [`encode`] wrote, stamping `filename` on every code
/// object. `None` for input it didn't write (or a different version's).
pub fn decode(bytes: &[u8], filename: &str) -> Option<CodeObject> {
Expand Down Expand Up @@ -116,11 +131,15 @@ impl Writer {
// Line numbers as deltas from the previous entry: mostly zero, so
// one byte each whatever the line.
self.deltas(c.linetable.iter().copied());
self.deltas(c.coltable.iter().map(|s| s.end_lineno));
for s in &c.coltable {
self.int(i64::from(s.col));
self.int(i64::from(s.end_col));
// The column spans as one length-prefixed block, which a reader
// keeps encoded until something reads a column (see `ColTable`).
let mut cols = Writer(Vec::new());
cols.deltas(c.coltable.iter().map(|s| s.end_lineno));
for s in c.coltable.iter() {
cols.int(i64::from(s.col));
cols.int(i64::from(s.end_col));
}
self.bytes(&cols.0);
self.uint(u64::from(c.arg_count));
self.uint(u64::from(c.posonly_count));
self.uint(u64::from(c.kwonly_count));
Expand Down Expand Up @@ -360,15 +379,7 @@ impl Reader<'_> {
});
}
let linetable = self.deltas()?;
let end_lines = self.deltas()?;
let mut coltable = Vec::with_capacity(end_lines.len());
for end_lineno in end_lines {
coltable.push(ColSpan {
end_lineno,
col: self.i32()?,
end_col: self.i32()?,
});
}
let coltable = crate::ColTable::encoded(Arc::from(self.raw()?));
let arg_count = self.u32()?;
let posonly_count = self.u32()?;
let kwonly_count = self.u32()?;
Expand Down Expand Up @@ -472,7 +483,7 @@ mod tests {
constants: vec![Constant::Int(-5)],
varnames: vec!["x".to_owned()],
linetable: vec![1, 2, 2],
coltable: vec![ColSpan::default(); 3],
coltable: vec![ColSpan::default(); 3].into(),
arg_count: 1,
is_generator: true,
is_nested: true,
Expand Down
Loading
Loading