Skip to content

Drive zlib, binascii, and struct builtins off the __pyre_kw__ marker ABI - #1123

Merged
youknowone merged 7 commits into
mainfrom
rtyper-legacy
Aug 11, 2026
Merged

Drive zlib, binascii, and struct builtins off the __pyre_kw__ marker ABI#1123
youknowone merged 7 commits into
mainfrom
rtyper-legacy

Conversation

@youknowone

@youknowone youknowone commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Ports three modules' keyword-accepting builtins from the __pyre_kw__ marker-dict ABI to the PyPy Signature gateway. The caller binds keywords into PY_NULL-padded positional slots before the wrapper runs, and each body reads slots positionally instead of peeling split_builtin_kwargs / bind_builtin_kwargs. The marker producer and both helpers stay for the remaining null-sig callers.

Commits

  • macros: add #[posonly] marker to #[pyre_function] — a #[posonly] attribute on the first non-positional-only parameter ends the positional-only run before it (the twin of #[kwonly]), emitting marker_posonly(). Mirrors PyPy visit_posonly / __posonly__=None.
  • zlib: compress/decompress/compressobj/decompressobj and _ZlibDecompressor off the marker ABI — module functions moved to the inline_functions: arm (macro derives the Signature); _ZlibDecompressor.__new__/.decompress registered with a hand-built SignatureBuilder. compressobj now declares all six parameters (was reading wbits/zdict at hardcoded positions, skipping method/memLevel/strategy).
  • binascii: the nine keyword-accepting functions off the marker ABIb2a_hex/hexlify, a2b_qp/b2a_qp, and a2b_base64/b2a_base64/b2a_uu via inline_functions:; the all-positional-only crc32/crc_hqx via a hand-built SignatureBuilder in extra_init: (both HOPELESS arity so the positional path routes through the binder). crc32/crc_hqx now report keyword/arity errors under the bare function name, matching how funcrun_obj names an interp2app function.
  • struct: pack/pack_into/unpack_from and the Struct methods off the marker ABIunpack_from (module + method) moved to signature-driven params (#[posonly] on buffer, offset optional); the three varargs functions pack/pack_into/Struct.pack_into registered with a hand-built posonly-then-varargs SignatureBuilder, reading the packed *args tuple tail from the last bound slot. pack_into/Struct.pack_into now reject stray keywords (takes no keyword arguments) instead of silently discarding them.

Verification

  • cargo test --all --no-default-features --features dynasm — green (7531/0)
  • python3 pyre/check.py — bit-exact 3-backend, 0 mismatch except one base-owned red on synth/pickle_terminal_raise_resume (a pre-existing JIT resume layout-lottery, reproduces on the parent base with PYRE_JIT=1 and is clean under PYRE_JIT=0; not introduced by this set)
  • cargo fmt --all -- --check — clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Enhancements

    • Improved argument handling across binascii, marshal, struct, and zlib.
    • Added clearer support for positional-only, keyword-only, optional, and variadic arguments.
    • Improved validation and error messages for missing, unexpected, or invalid arguments.
    • Standardized offset and numeric parameter conversion while preserving existing serialization, compression, and file I/O behavior.
  • Tests

    • Added coverage for valid argument usage, defaults, positional restrictions, keyword handling, and rejected arguments.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds positional-only signature support to the function macro. It migrates binascii, struct, zlib, and marshal APIs from manual argument parsing to declarative, signature-bound gateways. Builtin interpreter and JIT dispatch now uses signature-aware positional binding.

Changes

Builtin signature binding migration

Layer / File(s) Summary
Positional-only macro support
pyre/pyre-macros/src/lib.rs, pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
The macro emits positional-only signature boundaries. Smoke tests verify valid binding, computed results, and keyword rejection.
Builtin positional dispatch
pyre/pyre-interpreter/src/call.rs, pyre/pyre-jit/src/call_jit.rs
Builtin fast paths and JIT residual calls use the public signature-aware positional binding helper.
binascii gateway signatures
pyre/pyre-interpreter/src/module/binascii/mod.rs
binascii functions use declarative gateways, shared argument conversion helpers, and positional-only CRC signatures.
struct argument binding
pyre/pyre-interpreter/src/module/struct/mod.rs, pyre/extra_tests/snippets/stdlib_struct.py
struct APIs use explicit bound parameters, optional offset conversion, positional-only boundaries, and variadic packing arguments. Tests cover accepted and rejected keyword forms.
zlib argument binding
pyre/pyre-interpreter/src/module/zlib/mod.rs
zlib functions and _ZlibDecompressor use explicit signatures, shared default coercion helpers, and bound argument handlers.
marshal argument binding
pyre/pyre-interpreter/src/module/marshal/mod.rs, pyre/extra_tests/snippets/stdlib_marshal.py
marshal APIs use explicit positional-only and keyword-only signatures. Shared helpers handle options, serialization, deserialization, file I/O, and code-object checks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • youknowone/pyre#809: Covers the signature-based builtin keyword-binding migration used by these modules.

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

A rabbit marks each argument slot,
Positional rules now bind the lot.
struct, zlib, marshal agree,
binascii rejects keys with glee.
Gateways hop in tidy time.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: migrating zlib, binascii, and struct builtins from the pyre_kw marker ABI.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rtyper-legacy

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

Here are some automated review suggestions for this pull request.

Reviewed commit: 1248478ee3

ℹ️ 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 +45 to +46
if w.is_null() || unsafe { is_none(w) } {
return Ok(default);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject explicit None for crc32's initial value

When a caller passes binascii.crc32(data, None), this branch treats the explicit None like an omitted slot and computes with CRC 0. Before this change, the second positional value was passed to int_w, and PyPy's oldcrc='truncatedint_w' converter likewise raises TypeError for None; only PY_NULL should select the default. Keep the null check for omission but pass an explicit None through integer conversion.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit fe5d0d8).
Updated: 2026-08-10T23:30:35.726Z

Files in the reviewed diff
pyre/extra_tests/snippets/stdlib_marshal.py
pyre/extra_tests/snippets/stdlib_struct.py
pyre/pyre-interpreter/src/call.rs
pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
pyre/pyre-interpreter/src/module/binascii/mod.rs
pyre/pyre-interpreter/src/module/marshal/mod.rs
pyre/pyre-interpreter/src/module/struct/mod.rs
pyre/pyre-interpreter/src/module/zlib/mod.rs
pyre/pyre-jit/src/call_jit.rs
pyre/pyre-macros/src/lib.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-interpreter/src/module/zlib/mod.rs:83 ↔ pypy/module/zlib/interp_zlib.py:65: int_or_default treats an explicit None as an omitted default; PyPy’s @unwrap_spec(...=int) rejects None for compress, decompress, compressobj, and decompressobj. This behavior was already present as arg_int(... None => default) on upstream/main.

  • pyre/pyre-interpreter/src/module/zlib/mod.rs:545 ↔ pypy/module/zlib/interp_zlib.py:244: compressobj discards method, memLevel, and strategy (let _ = ...), while PyPy passes all three to rzlib.deflateInit; invalid values therefore are neither converted nor validated. This was already present before the patch.

  • pyre/pyre-interpreter/src/module/zlib/mod.rs:23 ↔ pypy/module/zlib/interp_zlib.py:113: streaming compressor/decompressor state is held in process-global ID-keyed BTreeMaps rather than on each Compress/Decompress object. The Rust code explicitly retains entries after object collection, unlike PyPy’s object-owned stream/finalizer lifecycle. This registry design predates the patch.

  • pyre/pyre-interpreter/src/module/marshal/mod.rs:583 ↔ pypy/module/marshal/interp_marshal.py:93: marshal.load calls file.read() for the remaining stream and then repairs position via seek; PyPy repeatedly calls read(n) for exactly the required bytes and rejects over-returning readers. This behavior was moved, not introduced, by the patch.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/marshal/mod.rs:530 ↔ pypy/module/marshal/interp_marshal.py:25: Pyre adds Python-3.13+-style allow_code keyword-only parameters and positional-only markers; the local PyPy source is an older interface without them. This is a Python-version API adaptation.

  • pyre/pyre-interpreter/src/module/binascii/mod.rs:224 ↔ pypy/module/binascii/interp_base64.py:38: Pyre exposes the CPython-compatible a2b_base64(data, /, *, strict_mode=False) signature, while this PyPy source accepts strict_mode positionally. This is a Python-version/API compatibility adaptation.

@youknowone youknowone changed the title Drive zlib and binascii builtins off the __pyre_kw__ marker ABI Drive zlib, binascii, and struct builtins off the __pyre_kw__ marker ABI Aug 9, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with 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.

Inline comments:
In `@pyre/extra_tests/snippets/stdlib_struct.py`:
- Around line 114-120: Extend the struct.pack and struct.pack_into tests to call
each hand-registered gateway with required positional arguments omitted,
including the module and bound-method variants represented by struct.pack,
struct.pack_into, and _s.pack_into. Assert that every incomplete call raises
TypeError, while preserving the existing keyword-rejection cases.

In `@pyre/pyre-interpreter/src/module/binascii/mod.rs`:
- Around line 35-40: Update slot_bool to return Result<bool, PyError> and
propagate errors from crate::baseobjspace::is_true instead of replacing them
with the default. Adjust all migrated callers— including a2b_qp, b2a_qp,
a2b_base64, b2a_base64, and b2a_uu—to return/propagate the result with ?, while
preserving default handling for null or None values.
- Around line 42-49: Update slot_u32 and crc_hqx to use
crate::baseobjspace::truncatedint_w when reading CRC initialization integers, so
values are truncated to the low 32 bits instead of raising OverflowError.
Preserve the existing default handling for null or None arguments.

In `@pyre/pyre-interpreter/src/module/zlib/mod.rs`:
- Around line 381-386: Update the max_length handling in
_ZlibDecompressor.decompress so an omitted or None argument remains the default,
zero maps to unlimited decompression, and explicitly negative values raise
ValueError instead of being discarded by usize::try_from. Preserve the existing
conversion and downstream behavior for non-negative values.

In `@pyre/pyre-macros/src/lib.rs`:
- Around line 287-290: Add compile-time validation in the pyre_function
signature-generation flow around marker_posonly so #[posonly] appears before any
#[kwonly] parameter and before **kwargs; reject invalid ordering instead of
allowing marker_posonly() to panic. Also require an explicit trailing
positional-only boundary when the signature is entirely positional-only or
transitions from positional-only to kw-only parameters, preserving the existing
marker emission for valid non-trailing cases.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 383bac88-3e6a-435f-9fc9-752fcaf9dbd3

📥 Commits

Reviewing files that changed from the base of the PR and between 241a97f and 783710e.

📒 Files selected for processing (6)
  • pyre/extra_tests/snippets/stdlib_struct.py
  • pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
  • pyre/pyre-interpreter/src/module/binascii/mod.rs
  • pyre/pyre-interpreter/src/module/struct/mod.rs
  • pyre/pyre-interpreter/src/module/zlib/mod.rs
  • pyre/pyre-macros/src/lib.rs

Comment on lines +114 to +120
# pack / pack_into (module and method) reject keyword arguments.
with assert_raises(TypeError):
struct.pack(format="ii")
with assert_raises(TypeError):
struct.pack_into("ii", bytearray(8), 0, 1, 2, extra=3)
with assert_raises(TypeError):
_s.pack_into(bytearray(8), 0, 1, 2, extra=3)

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

Add cases for omitted required arguments.

The new tests cover keyword rejection only. The binder pads omitted positionals with PY_NULL, so the missing-argument path is the risky one for the hand-registered vararg gateways. Add calls that omit required slots.

💚 Proposed additional cases
 with assert_raises(TypeError):
     _s.pack_into(bytearray(8), 0, 1, 2, extra=3)
+
+# pack / pack_into reject calls that omit required positional slots.
+with assert_raises(TypeError):
+    struct.pack()
+with assert_raises(TypeError):
+    struct.pack_into("ii", bytearray(8))
+with assert_raises(TypeError):
+    _s.pack_into()
+with assert_raises(TypeError):
+    _s.pack_into(bytearray(8))
📝 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
# pack / pack_into (module and method) reject keyword arguments.
with assert_raises(TypeError):
struct.pack(format="ii")
with assert_raises(TypeError):
struct.pack_into("ii", bytearray(8), 0, 1, 2, extra=3)
with assert_raises(TypeError):
_s.pack_into(bytearray(8), 0, 1, 2, extra=3)
# pack / pack_into (module and method) reject keyword arguments.
with assert_raises(TypeError):
struct.pack(format="ii")
with assert_raises(TypeError):
struct.pack_into("ii", bytearray(8), 0, 1, 2, extra=3)
with assert_raises(TypeError):
_s.pack_into(bytearray(8), 0, 1, 2, extra=3)
# pack / pack_into reject calls that omit required positional slots.
with assert_raises(TypeError):
struct.pack()
with assert_raises(TypeError):
struct.pack_into("ii", bytearray(8))
with assert_raises(TypeError):
_s.pack_into()
with assert_raises(TypeError):
_s.pack_into(bytearray(8))
🤖 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 `@pyre/extra_tests/snippets/stdlib_struct.py` around lines 114 - 120, Extend
the struct.pack and struct.pack_into tests to call each hand-registered gateway
with required positional arguments omitted, including the module and
bound-method variants represented by struct.pack, struct.pack_into, and
_s.pack_into. Assert that every incomplete call raises TypeError, while
preserving the existing keyword-rejection cases.

Comment thread pyre/pyre-interpreter/src/module/binascii/mod.rs Outdated
Comment thread pyre/pyre-interpreter/src/module/binascii/mod.rs Outdated
Comment on lines +381 to +386
let max_length = match args.get(2).copied() {
Some(o) if !o.is_null() && !unsafe { is_none(o) } => {
usize::try_from(crate::baseobjspace::int_w(o)?).ok()
}
_ => None,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

CPython _ZlibDecompressor.decompress max_length negative zero behavior

💡 Result:

In CPython’s zlib.Decompress (implemented as _ZlibDecompressor.decompress in Modules/zlibmodule.c), max_length must be non-negative; a negative value (including “negative zero”) is rejected with ValueError. The relevant checks treat max_length < 0 as an error [1][2]. So there is no special “-0” behavior: if a value is truly negative, it fails the max_length < 0 check. Separately, when max_length == 0, CPython sets the effective limit to PY_SSIZE_T_MAX (i.e., “no practical limit” / whole input is decompressed), consistent with the documentation that max_length=0 decompresses the whole input and leaves unconsumed_tail empty [1][3][4]. How this maps to “negative zero”: - If you pass a real Python float -0.0, Python will convert it to an integer 0 when calling a signature expecting a Py_ssize_t, so CPython sees max_length == 0 (not negative) and proceeds with the max_length==0 path (unlimited/whole input decompressed) [1][5]. - If you somehow pass a negative integer-like value (e.g., -1), CPython detects max_length < 0 and raises ValueError [1][2]. Key evidence: - CPython C implementation: “if (max_length < 0) { PyErr_SetString(PyExc_ValueError, "max_length must be non-negative"); return NULL; } … else if (max_length == 0) hard_limit = PY_SSIZE_T_MAX; else hard_limit = max_length;” [1][2]. - Docs: “If max_length is zero then the whole input is decompressed, and unconsumed_tail is empty.” [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant implementation without running repository code.
fd -a 'mod.rs|zlibmodule.c' . | sed 's#^\./##' | sort | head -50

echo
echo "--- pyre mod.rs around max_length ---"
sed -n '350,410p' pyre/pyre-interpreter/src/module/zlib/mod.rs

echo
echo "--- CPython Module/zlib.c max_length handling ---"
python3 - <<'PY'
# Read the relevant CPython source from upstream tags/sources without installing anything.
import urllib.request
urls = [
    "https://raw.githubusercontent.com/python/cpython/main/Modules/zlibmodule.c",
    "https://raw.githubusercontent.com/python/cpython/3.11/Modules/zlibmodule.c",
    "https://raw.githubusercontent.com/python/cpython/v3.9.4/Modules/zlibmodule.c",
]
for url in urls:
    try:
        data = urllib.request.urlopen(url, timeout=20).read().decode("utf-8", "replace")
    except Exception as e:
        print(f"{url}: unreachable: {e}")
        continue
    idx = data.find("static PyObject *")
    if idx != -1:
        data = data[idx:]
    for term in ["_zlib_decomp_decompress", "max_length"]:
        i = data.find(term)
        if i != -1:
            start = max(0, i-1800)
            end = min(len(data), i+2600)
            print(f"URL: {url} found around {term}")
            print(data[start:end])
            print("---")
            break
PY

Repository: youknowone/pyre

Length of output: 6597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- relevant int conversion ---"
rg -n "fn int_w|pub fn int_w|int_w\\(" pyre/pyre-interpreter/src -g '*.rs' -C 4

echo
echo "--- Python int conversion behavior probe ---"
python3 - <<'PY'
values = [-2**64, -2**32, -1, -0, 0, 1]
for x in values:
    print(f"{x!r:20} as usize: {x.to_bytes((x.bit_length()//8)+1, 'big', signed=True).__len__()} via int(x): {x}; converted via object? {type(x)}")
try:
    -1
except Exception as e:
    print(repr(e))
PY

Repository: youknowone/pyre

Length of output: 50374


🌐 Web query:

CPython Modules zlibmodule.c zlibmodule.c max_length decompress Decompress read

💡 Result:

In CPython, the zlib.Decompress.decompress method (defined in Modules/zlibmodule.c) uses the max_length parameter to limit the amount of uncompressed data returned in a single call [1][2]. When max_length is provided as a non-zero value, the decompress method ensures the returned bytes object does not exceed that length [2][3]. If the decompression process requires more output than max_length to consume the provided input, the operation stops, and any remaining input data is stored in the object's unconsumed_tail attribute [2][3]. This unconsumed_tail must be passed to subsequent calls to decompress to continue the process [2][3]. If max_length is zero (the default), the method attempts to decompress the entire input, and unconsumed_tail will be empty [2][3]. From an implementation perspective in Modules/zlibmodule.c, max_length dictates the allocation strategy for the output buffer [1][4]. Specifically: - Hard Limit Enforcement: The max_length is used to establish a hard_limit for the output buffer growth [4][5]. - Buffer Management: The implementation uses internal functions (such as OutputBuffer_Grow or arrange_output_buffer_with_maximum in various versions) to manage memory [4][5]. If the limit is reached, the decompressor halts output production, effectively allowing for controlled, chunked decompression of large streams [2][4]. - Input Handling: Any input data that could not be processed due to the max_length constraint is saved into self->unconsumed_tail via internal helper functions like save_unconsumed_input [4][5]. This functionality is primarily useful for memory-efficient processing of compressed data streams where the total output size may exceed available memory or when only specific-sized chunks are required [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -u
python3 - <<'PY'
import ssl
ctx = ssl.SSLContext()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

urls = [
    "https://raw.githubusercontent.com/python/cpython/v3.9.5/Modules/zlibmodule.c",
    "https://raw.githubusercontent.com/python/cpython/3.11/Modules/zlibmodule.c",
]
for url in urls:
    try:
        data = urllib.request.urlopen(url, context=ctx, timeout=30).read().decode("utf-8", "replace")
    except Exception as e:
        print(f"Fetch failed: {url}: {e}")
        continue
    for needle in ["_zlib_decomp_decompress", "z_decompress_impl"]:
        i = data.find(needle)
        if i != -1:
            slice = data[max(0, i-2500): min(len(data), i+5500)]
            print(f"=== {url} around {needle} ===")
            print(slice)
            break
PY

Repository: youknowone/pyre

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
from urllib import request
import ssl
ctx = ssl.SSLContext()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

urls = [
    "https://raw.githubusercontent.com/python/cpython/v3.9.5/Modules/zlibmodule.c",
    "https://raw.githubusercontent.com/python/cpython/3.11/Modules/zlibmodule.c",
]
for url in urls:
    try:
        data = request.urlopen(url, context=ctx, timeout=30).read().decode("utf-8", "replace")
    except Exception as e:
        print(f"Fetch failed: {url}: {e}")
        continue
    needles = ["_zlib_decomp_decompress", "z_decompress_impl"]
    found = False
    for needle in needles:
        i = data.find(needle)
        if i != -1:
            found = True
            slice = data[max(0, i-3200): min(len(data), i+7200)]
            print(f"=== {url} around {needle ===")
            for line in slice.splitlines():
                if "max_length" in line or "hard_limit" in line or "Py_ssize_t" in line or "ValueError" in line:
                    print(line)
            break
    if not found:
        print(f"Naedle not found in {url}")
PY

echo
echo "--- inspect int_w declaration/implementation ---"
rg -n "pub fn int_w|fn int_w|Py_ssize_t|overflow_error" pyre/pyre-interpreter/src/base* pyre/pyre-interpreter/src/module -g '*.rs' | head -80

Repository: youknowone/pyre

Length of output: 10614


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
from urllib import request
import ssl
ctx = ssl.SSLContext()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

urls = [
    "https://raw.githubusercontent.com/python/cpython/v3.9.5/Modules/zlibmodule.c",
    "https://raw.githubusercontent.com/python/cpython/3.11/Modules/zlibmodule.c",
]
for url in urls:
    try:
        data = request.urlopen(url, context=ctx, timeout=30).read().decode("utf-8", "replace")
    except Exception as e:
        print(f"Fetch failed: {url}: {e}")
        continue
    needles = ["_zlib_decomp_decompress", "z_decompress_impl"]
    found = False
    for needle in needles:
        i = data.find(needle)
        if i != -1:
            found = True
            slice = data[max(0, i-3200): min(len(data), i+7200)]
            print(f"=== {url} around {needle} ===")
            for line in slice.splitlines():
                if "max_length" in line or "hard_limit" in line or "Py_ssize_t" in line or "ValueError" in line:
                    print(line)
            break
    if not found:
        print(f"Needle not found in {url}")
PY

echo
echo "--- int_w implementation ---"
sed -n '8265,8295p' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 1973


🌐 Web query:

github raw python/cpython Modules zlibmodule.c _zlib_decomp_decompress max_length

💡 Result:

In the Python CPython source code, _zlib_decomp_decompress (historically referenced as zlib_Decompress_decompress_impl) is the implementation function for the decompress() method of the zlib.Decompress object [1][2][3]. The max_length parameter, which is of type Py_ssize_t, allows a user to limit the size of the decompressed output data returned by a single call to the method [1][4][5]. Key technical details regarding max_length include: Functionality: If max_length is set to a non-zero value, the decompress() method will return no more than max_length bytes of decompressed data [4][5]. This mechanism is primarily used to prevent memory exhaustion when processing untrusted or potentially malicious compressed data (often referred to as zip bombs) [3]. Unconsumed Input: When max_length limits the output, any input data that could not be processed due to this limit is stored in the unconsumed_tail attribute of the Decompress object [4][5]. The user must pass this unconsumed_tail data to subsequent calls to decompress() to continue the decompression process [4][5]. If max_length is zero, the decompressor will attempt to decompress the entire input, and unconsumed_tail will remain empty [4][5]. Implementation: Within Modules/zlibmodule.c, the implementation manages memory for the output buffer [1][6]. When max_length is used, the code ensures the output buffer does not grow beyond this specified limit [6][3][7]. If the internal state machine (typically calling the underlying zlib library's inflate function) encounters a scenario where it would exceed this limit, it pauses, saves the remaining input, and returns the data processed up to that point [1][2][3]. Historical Context: The max_length parameter has been a standard feature for controlling decompression size in the zlib module for many years [8][7]. Since Python 3.6, max_length has been explicitly supported as a keyword argument [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
from urllib import request
import ssl

urls = ["https://raw.githubusercontent.com/python/cpython/main/Modules/zlibmodule.c"]
ctx = ssl.SSLContext()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

for url in urls:
    data = request.urlopen(url, context=ctx, timeout=30).read().decode("utf-8", "replace")
    for needle in ["_zlib_decomp_decompress", "z_decompress_impl"]:
        idx = data.find(needle)
        if idx != -1:
            lines = data.splitlines()
            for j in range(max(0, idx.count('\n') - 200), idx.count('\n') + 300):
                line = lines[j]
                if "max_length" in line or "hard_limit" in line or "Py_ssize_t" in line or "ValueError" in line:
                    print(f"{j+1}: {line}")
PY

echo
echo "--- relevant Rust conversions ---"
sed -n '4408,4426p' pyre/pyre-interpreter/src/baseobjspace.rs
sed -n '8302,8307p' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 1364


Reject negative max_length values in _ZlibDecompressor.decompress.

max_length=-1 should remain the default only when omitted/None; an explicitly passed negative value must raise ValueError. max_length=0 should be treated as an unlimited decompression.

🤖 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 `@pyre/pyre-interpreter/src/module/zlib/mod.rs` around lines 381 - 386, Update
the max_length handling in _ZlibDecompressor.decompress so an omitted or None
argument remains the default, zero maps to unlimited decompression, and
explicitly negative values raise ValueError instead of being discarded by
usize::try_from. Preserve the existing conversion and downstream behavior for
non-negative values.

Comment on lines +287 to +290
if !posonly_marked && pt.attrs.iter().any(|a| a.path().is_ident("posonly")) {
sig_stmts.push(quote! { __b.marker_posonly(); });
posonly_marked = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

while IFS= read -r file; do
  if rg -q '#\[(posonly|kwonly|kwargs)\]' "$file"; then
    ast-grep outline "$file" --items all --type function
    rg -n -C 12 '#\[(posonly|kwonly|kwargs)\]' "$file"
  fi
done < <(fd --type f --extension rs . pyre)

Repository: youknowone/pyre

Length of output: 205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked Rust files under pyre matching macro/keyword markers:\n'
git ls-files 'pyre/**/*.rs' | rg 'lib\.rs$|pyre-macros|gateway|signature|marker' || true

printf '\nMacro candidate locations:\n'
fd -e rs --hidden . pyre pyre-macros | sort

printf '\nFind marker attributes:\n'
rg -n -C 8 '#\[(posonly|kwonly|kwargs)\]' pyre -g '*.rs' || true

printf '\nFind SignatureBuilder / marker methods:\n'
rg -n -C 6 'struct SignatureBuilder|impl SignatureBuilder|marker_posonly|marker_kwonly|marker_posonly\(|marker_kwonly\(' pyre -g '*.rs' || true

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Marker-only use sites, grouped by file, with line range and attribute order:\n'
rg -n -W 10 '#\[(posonly|kwonly|kwargs)\]' pyre -g '*.rs' | \
  awk '
    /^pyre\// { f=$0 }
    /#\[(posonly|kwonly|kwargs)\]/ {
      line=$0; sub(/^[0-9]+:/, "", line)
      if (attr != "") print attr
      attr=line
      files[++n]=f; lines[n]=$0; attrs[n]=line
    }
    END {
      if (n==0) { print "NO_MARKER_USAGE" }
      for (i=1;i<=n;i++) {
        if (i>1 && files[i] != files[i-1]) print "--- " files[i-1] "---"
        print lines[i] "\n" attrs[i]
      }
    }
  ' || true

printf '\nSignatureBuilder definitions and marker implementation snippets:\n'
rg -n -C 8 'struct SignatureBuilder|impl SignatureBuilder|marker_posonly|stringify\(posonlyargcount\)|marker_kwonly' pyre pyre-macros -g '*.rs' || true

printf '\npyre_function macro relevant sections:\n'
sed -n '220,330p' pyre/pyre-macros/src/lib.rs
sed -n '1896,1970p' pyre/pyre-macros/src/lib.rs

Repository: youknowone/pyre

Length of output: 29243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Markdown-safe marker-only use sites, grouped by file:\n'
python3 - <<'PY'
from pathlib import Path
import re

files = [Path(p) for p in __import__('subprocess').check_output(
    ['git','ls-files','pyre','**-/*.rs'], text=True).splitlines()
]
for path in files:
    text = path.read_text(errors='replace')
    if re.search(r'(?m)^#\[(posonly|kwonly|kwargs)\]', text):
        print(f'--- {path} ---')
        lines = text.splitlines()
        in_block = False
        for i, line in enumerate(lines, 1):
            if re.search(r'(?m)^#\[(posonly|kwonly|kwargs)\]', line):
                start=max(1,i-6); end=min(len(lines),i+8)
                print(f'[{start}-{end}]')
                for j in range(start,end+1):
                    print(f'{j}: {lines[j-1]}')
                in_block=True
        if not in_block:
            print('(no matches?')
PY

printf '\nCurrent SignatureBuilder marker contract:\n'
sed -n '68,79p' pyre/pyre-interpreter/src/gateway.rs

printf '\nCurrent pyre_functions signature emission around posonly/kwonly:\n'
sed-
n '262,303p' pyre/pyre-macros/src/lib.rs

Repository: youknowone/pyre

Length of output: 825


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Current pyre_functions signature emission around posonly/kwonly:\n'
sed -n '262,303p' pyre/pyre-macros/src/lib.rs

printf '\nPython behavioral probe of SignatureBuilder marker ordering and terminal posonly:\n'
python3 - <<'PY'
class SignatureBuilder:
    def __init__(self):
        self.name = "__test__"
        self.argnames = []
        self.varargname = None
        self.kwargname = None
        self.posonlyargcount = 0
        self.kwonlystartindex = -1

    def append(self, argname):
        self.argnames.append(argname)

    def marker_posonly(self):
        assert self.posonlyargcount == 0 and self.kwonlystartindex == -1
        self.posonlyargcount = len(self.argnames)

    def marker_kwonly(self):
        assert self.kwonlystartindex == -1
        self.kwonlystartindex = len(self.argnames)

    def signature(self):
        kwonlyargcount = len(self.argnames) - self.kwonlystartindex if self.kwonlystartindex != -1 else 0
        return self.argnames, self.varargname, self.kwargname, kwonlyargcount, self.posonlyargcount

for attrs, name in [
    (['data', 'kwonly', 'default', 'strict_mode'], 'posonly before kwonly'),
    (['kwonly', 'default', 'fix_imports', 'posonly', 'default', 'encoding'], 'posonly after kwonly'),
    (['posonly', 'data'], 'posonly before end'),
    (['data'], 'no marker'),
]:
    print(f'--- {name} ---')
    b=SignatureBuilder()
    calls=[]
    for item in attrs:
        if item in ('kwonly','#[kwonly]'): calls.append('marker_kwonly'); b.marker_kwonly()
        elif item in ('posonly','#[posonly]'): calls.append('marker_posonly'); b.marker_posonly()
        else: b.append(item)
    try:
        print(calls, 'ok signature=', b.signature())
    except AssertionError as e:
        print(calls, 'panic asserted order violation:', e)
PY

Repository: youknowone/pyre

Length of output: 2357


Validate #[posonly] order and end-of-signature placement.

pyre_function emits marker_posonly() at the first parameter that has #[posonly], then appends the remaining parameters. SignatureBuilder::marker_posonly() asserts that marker_kwonly() has not run, so a later #[kwonly] parameter panics. A trailing positional-only boundary with no following parameter is not represented. Add compile-time validation for #[posonly] to appear before any #[kwonly] and before **kwargs, and require an explicit boundary form for all-positional-only or positional-only-with-kwonly signatures.

🤖 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 `@pyre/pyre-macros/src/lib.rs` around lines 287 - 290, Add compile-time
validation in the pyre_function signature-generation flow around marker_posonly
so #[posonly] appears before any #[kwonly] parameter and before **kwargs; reject
invalid ordering instead of allowing marker_posonly() to panic. Also require an
explicit trailing positional-only boundary when the signature is entirely
positional-only or transitions from positional-only to kw-only parameters,
preserving the existing marker emission for valid non-trailing cases.

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 783710ec9b

ℹ️ 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 +1194 to +1196
let buf = unsafe { readbuf(buffer)? };
let offset = match offset {
Some(o) => unsafe { crate::builtins::space_index_w(o)? },

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 Coerce offset before borrowing the input buffer

When offset is an object with a Python __index__, space_index_w can execute arbitrary code after readbuf has returned a raw slice; that code can resize an underlying bytearray/array/mmap or otherwise invalidate the borrowed storage, after which do_unpack_from reads the stale slice. PyPy's @unwrap_spec(offset=int) performs this coercion before entering the body, and the previous resolve_buffer_offset did likewise. Convert offset before readbuf in both this method and the module-level unpack_from implementation.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

this.ensure_ready()?;
let format = majit_metainterp::jit::promote_string(this.format);
let fmt = unsafe { w_str_get_value(format) };
let offset = unsafe { crate::builtins::space_index_w(args[2])? };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject missing Struct.pack_into arguments before conversion

When Struct.pack_into is called without both buffer and offset, the signature binder pads the missing slots with PY_NULL, but this method passes the null offset directly to space_index_w. Thus calls such as Struct('i').pack_into(bytearray(4)) now report that an object cannot be interpreted as an integer instead of the required-argument error that the previous implementation explicitly produced. Check args[1] and args[2] before converting either slot, as the new module-level pack_into already does.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e86c18976

ℹ️ 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".

// interp_zlib.py:66 `compress(data, __posonly__=None, level, wbits)` —
// `data` positional-only, `level`/`wbits` positional-or-keyword.
fn compress(
data: PyBufferStr,

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 Copy the zlib input before running integer coercions

When data is a bytearray and level or wbits has a custom __int__/__index__, the wrapper first converts PyBufferStr into a raw slice through charbuf_w, then int_or_default executes arbitrary Python code without holding a buffer export. If that coercion resizes the bytearray, the saved slice is invalidated before backend::compress reads it, causing stale-memory reads or corrupted output; decompress has the same ordering at line 517. The previous implementation copied with as_bytes before coercing these arguments, so retain an owned copy or an active export across coercion.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

@youknowone
youknowone force-pushed the rtyper-legacy branch 2 times, most recently from bb9f920 to d20e34d Compare August 10, 2026 13:22
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🤖 Prompt for all review comments with 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.

Inline comments:
In `@pyre/extra_tests/snippets/stdlib_marshal.py`:
- Around line 114-116: Extend the positional-only API assertions near the
existing marshal.loads keyword test to also cover marshal.load with a BytesIO
file object passed by keyword. Import and reuse BytesIO as done in
test_file_api, and assert that marshal.load(file=...) raises TypeError.

In `@pyre/extra_tests/snippets/stdlib_struct.py`:
- Around line 105-112: Add a negative assertion to the unpack_from tests showing
that passing format by keyword raises the expected positional-only TypeError,
while retaining the existing valid positional and keyword cases for buffer and
offset in the struct.unpack_from block.

In `@pyre/pyre-interpreter/src/module/marshal/mod.rs`:
- Around line 586-598: Refactor load to call unmarshal_bytes instead of
duplicating wire deserialization and reject_code handling. Update
unmarshal_bytes to return both the deserialized value and consumed byte count,
then use that count with before and seek the file accordingly while preserving
the allow_code behavior in unmarshal_bytes.
- Around line 530-541: The marshal wrappers must root live PyObjectRef arguments
before option resolution can invoke Python. In
pyre/pyre-interpreter/src/module/marshal/mod.rs:530-541 (marshal.dumps),
544-553, 557-571, and 575-593, add pin_root and shadow_stack_get handling for
every argument used after resolve_version or resolve_allow_code, then pass the
rooted values into marshal_to_bytes or the corresponding load/dump operation
while preserving existing behavior.

In `@pyre/pyre-interpreter/src/module/struct/mod.rs`:
- Around line 1253-1275: Update the missing-argument errors in pack and
pack_into to use the gateway’s standard phrasing for omitted required positional
arguments, including the function name and explicit missing-argument count where
applicable. Keep the existing null checks and error type unchanged, and preserve
strict structural parity with the neighboring gateway diagnostics.

In `@pyre/pyre-interpreter/src/module/zlib/mod.rs`:
- Around line 510-511: Replace the unchecked i64-to-i32 cast for level with a
checked conversion that raises OverflowError when the value is outside the C int
range. Apply the same range validation at both level call sites in the compress
and compressobj paths, while preserving the existing default handling.
- Around line 84-104: Update int_or_default and zdict_or_none to treat only
PY_NULL/absent slots as omitted; remove the is_none checks so explicit None is
passed through to the existing integer and byte-conversion handling. Preserve
PY_NULL defaults for omitted optional arguments, allowing compress, decompress,
and compressobj to retain their upstream None behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ca61ff47-f2e4-4e3b-9940-8dd43066cc74

📥 Commits

Reviewing files that changed from the base of the PR and between ab70bc2 and d20e34d.

📒 Files selected for processing (8)
  • pyre/extra_tests/snippets/stdlib_marshal.py
  • pyre/extra_tests/snippets/stdlib_struct.py
  • pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
  • pyre/pyre-interpreter/src/module/binascii/mod.rs
  • pyre/pyre-interpreter/src/module/marshal/mod.rs
  • pyre/pyre-interpreter/src/module/struct/mod.rs
  • pyre/pyre-interpreter/src/module/zlib/mod.rs
  • pyre/pyre-macros/src/lib.rs

Comment on lines +114 to +116
# `bytes` / `file` are positional-only too.
with self.assertRaises(TypeError):
marshal.loads(bytes=data)

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

Add the load(file=...) assertion that the comment claims.

The comment states that bytes and file are positional-only, but only marshal.loads(bytes=data) is tested. Add the load case so the file boundary is covered.

💚 Proposed test addition
         # `bytes` / `file` are positional-only too.
         with self.assertRaises(TypeError):
             marshal.loads(bytes=data)
+        with self.assertRaises(TypeError):
+            marshal.load(file=BytesIO(data))

Import BytesIO in this test, as test_file_api does.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 115-115: dill and marshal deserialize arbitrary objects/bytecode and execute code on untrusted input. Use a safe serialization format and never load untrusted data.
Context: marshal.loads(bytes=data)
Note: [CWE-502] Deserialization of Untrusted Data.

(dill-marshal-deserialization-python)

🪛 OpenGrep (1.26.0)

[WARNING] 116-116: marshal.loads() can execute arbitrary code during deserialization. Use a safe format like JSON instead.

(coderabbit.deserialization.python-marshal)

🪛 Ruff (0.16.1)

[warning] 115-115: Use pytest.raises instead of unittest-style assertRaises

Replace assertRaises with pytest.raises

(PT027)


[error] 116-116: Deserialization with the marshal module is possibly dangerous

(S302)

🤖 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 `@pyre/extra_tests/snippets/stdlib_marshal.py` around lines 114 - 116, Extend
the positional-only API assertions near the existing marshal.loads keyword test
to also cover marshal.load with a BytesIO file object passed by keyword. Import
and reuse BytesIO as done in test_file_api, and assert that
marshal.load(file=...) raises TypeError.

Comment on lines +105 to +112

# unpack_from accepts buffer / offset positionally or by keyword.
_buf = struct.pack("ii", 111, 222)
assert struct.unpack_from("ii", _buf, offset=0) == (111, 222)
assert struct.unpack_from("ii", buffer=_buf, offset=0) == (111, 222)
_s = struct.Struct("ii")
assert _s.unpack_from(_buf, offset=0) == (111, 222)
assert _s.unpack_from(buffer=_buf) == (111, 222)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the positional-only negative case for format.

The block proves that buffer and offset accept keywords. It does not prove that format rejects one. That assertion pins the posonly boundary direction for struct.unpack_from.

💚 Proposed additional case
 assert _s.unpack_from(buffer=_buf) == (111, 222)
+with assert_raises(TypeError):
+    struct.unpack_from(format="ii", buffer=_buf)
📝 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
# unpack_from accepts buffer / offset positionally or by keyword.
_buf = struct.pack("ii", 111, 222)
assert struct.unpack_from("ii", _buf, offset=0) == (111, 222)
assert struct.unpack_from("ii", buffer=_buf, offset=0) == (111, 222)
_s = struct.Struct("ii")
assert _s.unpack_from(_buf, offset=0) == (111, 222)
assert _s.unpack_from(buffer=_buf) == (111, 222)
# unpack_from accepts buffer / offset positionally or by keyword.
_buf = struct.pack("ii", 111, 222)
assert struct.unpack_from("ii", _buf, offset=0) == (111, 222)
assert struct.unpack_from("ii", buffer=_buf, offset=0) == (111, 222)
_s = struct.Struct("ii")
assert _s.unpack_from(_buf, offset=0) == (111, 222)
assert _s.unpack_from(buffer=_buf) == (111, 222)
with assert_raises(TypeError):
struct.unpack_from(format="ii", buffer=_buf)
🤖 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 `@pyre/extra_tests/snippets/stdlib_struct.py` around lines 105 - 112, Add a
negative assertion to the unpack_from tests showing that passing format by
keyword raises the expected positional-only TypeError, while retaining the
existing valid positional and keyword cases for buffer and offset in the
struct.unpack_from block.

Comment on lines +530 to +541
fn dumps(
value: PyObjectRef,
version: Option<PyObjectRef>,
#[posonly]
#[kwonly]
allow_code: Option<PyObjectRef>,
) -> Result<PyObjectRef, crate::PyError> {
let version = resolve_version(version)?;
let allow_code = resolve_allow_code(allow_code)?;
let out = marshal_to_bytes(value, version, allow_code)?;
Ok(bytesobject::w_bytes_from_bytes(&out))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the inline-function gateway roots incoming arguments for the callee,
# and compare with the rooting pattern used by other Python-capable builtins.
fd -t f 'gateway.rs' pyre/pyre-interpreter/src | xargs rg -n -C 8 'pin_root|shadow_stack_get|inline_function'
rg -n -C 4 'pin_root' pyre/pyre-interpreter/src/module/zlib/mod.rs pyre/pyre-interpreter/src/module/struct/mod.rs

Repository: youknowone/pyre

Length of output: 7028


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== marshal file sections =="
wc -l pyre/pyre-interpreter/src/module/marshal/mod.rs
sed -n '480,610p' pyre/pyre-interpreter/src/module/marshal/mod.rs

echo "== helper definitions =="
rg -n -C 6 'fn resolve_version|fn resolve_allow_code|fn bytes_like|marshal_to_bytes|unmarshal_bytes' pyre/pyre-interpreter/src/module/marshal/mod.rs

echo "== pyre_function call sites around marshal =="
rg -n -C 8 'pyre_function|make_module_builtin_function_with_arity_and_maybe_sig|inline_function' pyre/pyre-interpreter/src/module/marshal/mod.rs pyre/pyre-interpreter/src/gateway.rs

echo "== compare builtin rooting patterns =="
rg -n -C 8 'fn (loads|dump|dumps|load)' pyre/pyre-interpreter/src/module -g '*.rs' | sed -n '1,220p'

Repository: youknowone/pyre

Length of output: 37720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gateway macro implementation =="
rg -n -C 12 'macro_rules! py_module|inline_functions|BuiltinCodeFn\(' pyre/pyre-interpreter/src/gateway.rs

echo "== gc_roots pin/push/get definitions =="
rg -n -C 6 'fn push_roots|fn pin_root|fn shadow_stack_get|fn shadow_stack_len|push_root' pyre/pyre-interpreter/src -g '*.rs'

echo "== marshal helper implementations =="
sed -n '36,70p' pyre/pyre-interpreter/src/module/marshal/mod.rs
sed -n '390,460p' pyre/pyre-interpreter/src/module/marshal/mod.rs

echo "== focused comparison builtin pickle dump/loads code =="
sed -n '730,785p' pyre/pyre-interpreter/src/module/_pickle/mod.rs

Repository: youknowone/pyre

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== marshal macro exact section =="
rg -n -C 20 'inline_functions:' pyre/pyre-interpreter/src/gateway.rs pyre/pyre-interpreter/src -g '*.rs' | sed -n '1,260p'

echo "== gc_roots small implementation =="
fd -t f 'gc_roots.rs' pyre/pyre-interpreter/src -x sh -c 'echo "--- $1"; wc -l "$1"; rg -n -C 8 "pub fn (push_roots|pin_root|shadow_stack_get|shadow_stack_len)|struct .*RootScope|impl .*RootScope" "$1"; sed -n "1,220p"' sh {}

echo "== marshal public helper bodies =="
sed -n '1,50p' pyre/pyre-interpreter/src/module/marshal/mod.rs
sed -n '424,442p' pyre/pyre-interpreter/src/module/marshal/mod.rs

Repository: youknowone/pyre

Length of output: 24832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pyre_function definition and generated arity/sig symbols =="
rg -n -C 12 'pub fn create_builtins|pyre_function|struct BuiltinCode|BuiltinCodeFn|fn .*_pyre_arity|fn .*_pyre_sig|push_roots|pin.*root' pyre/pyre-interpreter/src/lib.rs pyre/pyre-interpreter/src/gateway.rs | sed -n '1,260p'

echo "== gc_roots implementation via precise symbol search =="
rg -n 'pub (struct|fn) (RootScope|push_roots|pin_root|shadow_stack_get|shadow_stack_len|pin_roots)|pin_roots|shadow_stack_len' pyre/pyre-interpreter/src -g '*.rs' | sed -n '1,220p'

echo "== pyre_function symbol occurrences =="
rg -n -C 8 '#\[pyre_function\]|macro_rules! pyre_function|fn pyre_function|create_builtins|pyre_function' pyre/pyre-interpreter/src/lib.rs pyre/pyre-interpreter/src/gateway.rs | sed -n '1,300p'

Repository: youknowone/pyre

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pyre_function macro from pyre_macros =="
rg -n -C 10 'pub fn pyre|create.*builtins.*pyre|let name|shadow_stack_len|pin_root|push_roots|PyreFunction|BuiltinCodeFn|kwonly|posonly' pyre/pyre-macros/src pyre/pyre-object/src -g '*.rs' | sed -n '1,360p'

echo "== gc_roots exact implementation snippets =="
rg -n -A 18 'pub fn push_roots|pub fn pin_root|pub fn shadow_stack_get|pub fn shadow_stack_len|pub fn pin_roots|shadow_stack' pyre/pyre-object/src -g '*.rs' | sed -n '1,300p'

echo "== pyre_macros macro source files =="
find pyre/pyre-macros/src -maxdepth 2 -type f -print | sort
for f in $(find pyre/pyre-macros/src -maxdepth 2 -type f -print | sort); do echo "--- $f"; wc -l "$f"; done
rg -n -C 6 '#\[proc_macro_attribute\]|proc_macro|quote!|BuiltinCodeFn|args:|pin_root|shadow_stack_get' pyre/pyre-macros/src -g '*.rs' | sed -n '1,340p'

Repository: youknowone/pyre

Length of output: 50371


Root marshal argument values before option resolution. #[pyre_function] generated wrappers pass PyObjectRef arguments into the inline wrappers, but those values are raw locals until the wrappers themselves pin them. resolve_version and resolve_allow_code can call into Python and move GC objects, so marshal.dumps, marshal.loads, marshal.dump, and marshal.load each need pin_root plus shadow_stack_get for live arguments used after resolving options.

📍 Affects 1 file
  • pyre/pyre-interpreter/src/module/marshal/mod.rs#L530-L541 (this comment)
  • pyre/pyre-interpreter/src/module/marshal/mod.rs#L544-L553
  • pyre/pyre-interpreter/src/module/marshal/mod.rs#L557-L571
  • pyre/pyre-interpreter/src/module/marshal/mod.rs#L575-L593
🤖 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 `@pyre/pyre-interpreter/src/module/marshal/mod.rs` around lines 530 - 541, The
marshal wrappers must root live PyObjectRef arguments before option resolution
can invoke Python. In pyre/pyre-interpreter/src/module/marshal/mod.rs:530-541
(marshal.dumps), 544-553, 557-571, and 575-593, add pin_root and
shadow_stack_get handling for every argument used after resolve_version or
resolve_allow_code, then pass the rooted values into marshal_to_bytes or the
corresponding load/dump operation while preserving existing behavior.

Comment on lines +586 to +598
let mut reader = wire::Cursor {
data: data.as_slice(),
position: 0,
};
let result =
wire::deserialize_value(&mut reader, PyreMarshalBag).map_err(marshal_error)?;
let new_position = w_int_new(before.saturating_add(reader.position as i64));
call_method(file, "seek", &[new_position])?;
let result = result.get();
if !allow_code {
reject_code(result)?;
}
Ok(result)

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

Reuse unmarshal_bytes in load.

The deserialize and reject_code body here repeats unmarshal_bytes. The only extra requirement is the consumed byte count. Return the consumed length from unmarshal_bytes and call it from load, so the allow_code rejection rule stays defined in one place.

🤖 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 `@pyre/pyre-interpreter/src/module/marshal/mod.rs` around lines 586 - 598,
Refactor load to call unmarshal_bytes instead of duplicating wire
deserialization and reject_code handling. Update unmarshal_bytes to return both
the deserialized value and consumed byte count, then use that count with before
and seek the file accordingly while preserving the allow_code behavior in
unmarshal_bytes.

Comment on lines +1253 to +1275
fn pack(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
if args[0].is_null() {
return Err(crate::PyError::type_error("missing format argument"));
}
let fmt = format_to_string(args[0])?;
let values = unsafe { w_tuple_items_copy_as_vec(args[1]) };
do_pack(&fmt, &values)
}

/// `interp_struct.py:76 pack_into(space, w_format, w_buffer, offset, args_w)`.
/// Bound slice `[format, buffer, offset, (*args tuple)]`. As with [`pack`],
/// the three required positional slots are `PY_NULL`-checked here.
fn pack_into(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
if args[0].is_null() || args[1].is_null() || args[2].is_null() {
return Err(crate::PyError::type_error(
"pack_into() missing format, buffer or offset argument",
));
}
let fmt = format_to_string(args[0])?;
let offset = unsafe { crate::builtins::space_index_w(args[2])? };
let values = unsafe { w_tuple_items_copy_as_vec(args[3]) };
do_pack_into(&fmt, args[1], offset, &values)
}

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

Align the missing-argument messages with the gateway arity diagnostics.

Both bodies raise hand-written text. pack omits the function name that the neighboring messages include, and neither message matches the arity error PyPy's gateway produces for an omitted positional slot. Emit the same phrasing the binder uses for a missing required argument, or name the count explicitly.

♻️ Proposed message alignment
 fn pack(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
     if args[0].is_null() {
-        return Err(crate::PyError::type_error("missing format argument"));
+        return Err(crate::PyError::type_error(
+            "pack() missing required argument 'format' (pos 1)",
+        ));
     }

As per coding guidelines: "Port RPython/PyPy code with strict line-by-line structural parity; do not take shortcuts".

🤖 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 `@pyre/pyre-interpreter/src/module/struct/mod.rs` around lines 1253 - 1275,
Update the missing-argument errors in pack and pack_into to use the gateway’s
standard phrasing for omitted required positional arguments, including the
function name and explicit missing-argument count where applicable. Keep the
existing null checks and error type unchanged, and preserve strict structural
parity with the neighboring gateway diagnostics.

Source: Coding guidelines

Comment on lines +84 to 104
/// Coerce an argument slot to an integer, `None`/absent (`PY_NULL`) → default.
///
/// The Signature-bound call path fills every declared slot: a supplied
/// keyword / positional carries the value, an omitted optional carries
/// `PY_NULL`.
fn int_or_default(o: PyObjectRef, default: i64) -> Result<i64, crate::PyError> {
if o.is_null() || unsafe { is_none(o) } {
Ok(default)
} else {
crate::baseobjspace::int_w(o)
}
}

/// Fetch an optional `zdict` bytes argument by keyword or position.
fn arg_zdict(
pos: &[PyObjectRef],
kwargs: Option<PyObjectRef>,
index: usize,
) -> Result<Option<Vec<u8>>, crate::PyError> {
match crate::builtins::kwarg_get(kwargs, "zdict").or_else(|| pos.get(index).copied()) {
Some(o) if !unsafe { is_none(o) } => Ok(Some(as_bytes(o)?)),
_ => Ok(None),
/// Coerce an optional `zdict` slot to bytes, `None`/absent (`PY_NULL`) → None.
fn zdict_or_none(o: PyObjectRef) -> Result<Option<Vec<u8>>, crate::PyError> {
if o.is_null() || unsafe { is_none(o) } {
Ok(None)
} else {
Ok(Some(as_bytes(o)?))
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the macro's default-slot handling to confirm an omitted optional can stay PY_NULL.
rg -nP -C6 'posonly|default' pyre/pyre-macros/src/lib.rs | head -120

# Compare with the PyPy declarations these handlers port.
fd -a 'interp_zlib.py' | xargs -r rg -nP -C4 'unwrap_spec|def (compress|decompress|Compress___new__|Decompress___new__)'

Repository: youknowone/pyre

Length of output: 9081


🏁 Script executed:

#!/bin/bash
# Inspect the Ruby/zlib implementation and CPython source/docs for zdict handling.
fd -a 'rzlib.*' -t f | xargs -r rg -nP -C8 'w_zdict|unpack|zdict|unpack'
python3 - <<'PY'
try:
    import zlib
    for name, args in [
        ("compress level=None", [b"x"], {"level": None}),
        ("decompress wbits=None", [b"x"], {"wbits": None}),
        ("compressobj zdict=None", [], {"zdict": None}),
        ("compressobj zdict=empty", [], {"zdict": b""}),
    ]:
        try:
            res = zlib.compress(*(args[:1]), **args[1:]) if name.startswith("compress") else zlib.decompress(*(args[:1]), **args[1:]) if name.startswith("decompress") else zlib.compressobj(**args[1:])
            print(name, "OK", type(res).__name__ if callable(res) else "<bytes>")
        except Exception as e:
            print(name, type(e).__name__, e)
except Exception as e:
    print("zlib unavailable:", e)
PY

Repository: youknowone/pyre

Length of output: 13941


🏁 Script executed:

#!/bin/bash
# Inspect PyPy's w_zdict type/implementation and the exact Python zlib C signature for context.
python3 - <<'PY'
try:
    import zlib, inspect
    for name, func in [
        ("compress", zlib.compress),
        ("decompress", zlib.decompress),
        ("compressobj", zlib.compressobj),
        ("Decompress.__init__", zlib.Decompress.__init__),
    ]:
        try:
            print(name, inspect.signature(func))
        except Exception as e:
            print(name, "SIG_ERROR", e)
except Exception as e:
    print("zlib unavailable:", type(e).__name__, e)
PY

# Search for PyPy w_zdict implementation.
rg -nP -C8 'w_zdict|zdict' pypy/ pypy-interpreter/ --glob '*.py' --glob '*.rs' || true

Repository: youknowone/pyre

Length of output: 14442


🏁 Script executed:

#!/bin/bash
# Inspect the target zlib Rust code and the macro's generated defaults/unwrap behavior.
wc -l pyre/pyre-interpreter/src/module/zlib/mod.rs
sed -n '1,130p' pyre/pyre-interpreter/src/module/zlib/mod.rs
sed -n '530,555p' pyre/pyre-interpreter/src/module/zlib/mod.rs
rg -n -C4 'int_or_default|zdict_or_none|compressobj.*python_func|deflateInit' pyre/pyre-interpreter/src/module/zlib/mod.rs
sed -n '360,395p' pyre/pyre-macros/src/lib.rs

Repository: youknowone/pyre

Length of output: 11093


Distinguish None from omitted optional arguments in this path.

unpack_kwargs_from passes supplied signature slots unchanged, so level=None, wbits=None, and zdict=None are not PY_NULL. Current helpers turn explicit None into the function default, while PyPy ports w_zdict=None and the documented integer default slots through unwrap_spec(...) converters that accept None. Drop the is_none branch in int_or_default and zdict_or_none, and keep declared omitted optionals as PY_NULL; otherwise compress(level=None), decompress(wbits=None), and compressobj(zdict=None) bypass upstream None handling.

🤖 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 `@pyre/pyre-interpreter/src/module/zlib/mod.rs` around lines 84 - 104, Update
int_or_default and zdict_or_none to treat only PY_NULL/absent slots as omitted;
remove the is_none checks so explicit None is passed through to the existing
integer and byte-conversion handling. Preserve PY_NULL defaults for omitted
optional arguments, allowing compress, decompress, and compressobj to retain
their upstream None behavior.

Source: Coding guidelines

Comment on lines +510 to +511
let level = int_or_default(level, -1)? as i32;
let wbits = to_wbits(int_or_default(wbits, backend::MAX_WBITS as i64)?);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

as i32 truncates an out-of-range level instead of raising.

int_or_default returns i64. The as i32 cast wraps silently, so zlib.compress(b"x", level=1 << 32) yields level 0 and succeeds, and zlib.compressobj(level=1 << 32) behaves the same way. CPython and PyPy raise OverflowError for a value that does not fit a C int. Range-check the value before the cast.

🐛 Proposed fix for the level range check
+/// Coerce an argument slot to a C `int` level, rejecting out-of-range values.
+fn level_or_default(o: PyObjectRef, default: i64) -> Result<i32, crate::PyError> {
+    let v = int_or_default(o, default)?;
+    i32::try_from(v).map_err(|_| {
+        crate::PyError::overflow_error("Python int too large to convert to C int")
+    })
+}

Then use it at both call sites:

-            let level = int_or_default(level, -1)? as i32;
+            let level = level_or_default(level, -1)?;

Also applies to: 546-547

🤖 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 `@pyre/pyre-interpreter/src/module/zlib/mod.rs` around lines 510 - 511, Replace
the unchecked i64-to-i32 cast for level with a checked conversion that raises
OverflowError when the value is outside the C int range. Apply the same range
validation at both level call sites in the compress and compressobj paths, while
preserving the existing default handling.

expand_pyre_function recognized #[kwonly] and #[kwargs] but had no way to
mark a positional-only boundary, so a signature-driven module function could
not express positional-only leading parameters. A #[posonly] attribute on the
first non-positional-only parameter now emits SignatureBuilder::marker_posonly
before that parameter's append, symmetric to the #[kwonly] handling and
emitted before it (the builder asserts posonly precedes kwonly). The marker
attribute is stripped from the generated inner fn.

marker_posonly mirrors gateway.py visit_posonly (the __posonly__=None
sentinel).

macro_smoke.rs gains _posonly_bound_probe and a test asserting the derived
Signature carries posonlyargcount == 1, that a keyword still binds the
non-positional-only tail, and that passing the positional-only name as a
keyword raises TypeError.

Assisted-by: Claude
…compressor off the marker ABI

The four module functions moved from the functions: arm (registered as
varargs `/ *` closures with a null Signature) to inline_functions:, so the
#[pyre_function] expansion derives a Signature and the call path binds
keywords into positional PY_NULL-padded slots before the wrapper runs. data /
string are positional-only via #[posonly] (interp_zlib.py:66,92). compressobj
now declares all six parameters level/method/wbits/memLevel/strategy/zdict
(interp_zlib.py:228); method/memLevel/strategy stay accepted-and-ignored
(Compressor::new threads only level/wbits/zdict) but are now keyword-bindable,
replacing the hardcoded positional indices that skipped them.

_ZlibDecompressor.__new__ and .decompress become named functions registered
through make_new_descr_maybe_sig / make_builtin_function_maybe_sig with a
hand-built Signature (cls / self positional-only via marker_posonly).

arg_int / arg_zdict, which peeled the marker dict and resolved by
keyword-or-position, are replaced by int_or_default / zdict_or_none that read
a filled Signature slot (value, or PY_NULL for an omitted optional). crc32 /
adler32 keep their varargs closures (no keyword API). This removes all six
split_builtin_kwargs peels in the module.

Pre-existing divergences left untouched: int_or_default accepts level=None as
the default, and method/memLevel/strategy remain ignored.

Assisted-by: Claude
Register b2a_hex/hexlify, a2b_qp/b2a_qp, and a2b_base64/b2a_base64/b2a_uu
through the inline_functions: arm so #[pyre_function] derives their
Signature and the caller binds keywords into positional slots. Register
the all-positional-only crc32/crc_hqx through a hand-built
SignatureBuilder (marker_posonly after every append) in extra_init:,
since #[posonly] on a parameter cannot mark a run with no trailing
parameter. Both take HOPELESS arity so the positional path routes
through the binder rather than the fixed-arity fast entry.

The bodies read PY_NULL-padded positional slots instead of peeling
split_builtin_kwargs / bind_builtin_kwargs; the two helpers and the
marker producer stay for the remaining null-sig callers.

crc32/crc_hqx report keyword and arity errors under the bare function
name (crc32() ...) the way funcrun_obj names an interp2app function,
dropping the manual binascii. prefix.

a2b_base64's strict_mode stays keyword-only, unchanged; interp_base64.py
marks no __kwonly__ there, so upstream keeps it positional-or-keyword.

Assisted-by: Claude
…he marker ABI

Convert all five `__pyre_kw__` marker peels in `_struct` to the Signature
gateway. `unpack_from` moves to the `inline_functions:` arm (`format`
positional-only via `#[posonly]` on `buffer`, `offset` an `Option` so an
omitted slot is 0 while an explicit `offset=None` raises). `pack`, `pack_into`,
and the `Struct.pack_into` method are `*args` builtins: their `&[PyObjectRef]`
bodies are registered by hand with a `varargname`-bearing `Signature`
(`sig_posonly_then_varargs`), so the binder packs the excess positionals into a
tuple the body reads via `w_tuple_items_copy_as_vec`. `Struct.unpack_from`
converts through the `#[pyre_methods]` arm with typed `buffer`/`offset` params;
`Struct.pack_into` is registered into the type dict from `extra_init:` because
the method arm emits no Signature for a varargs method. `resolve_buffer_offset`
is deleted (its two callers now bind by Signature).

`pack_into` / `Struct.pack_into` previously discarded stray keywords silently;
the all-positional-only Signature now rejects them ("takes no keyword
arguments"), matching the reference. The keyword-rejection error name is the
bare function name from the Signature path rather than the module-qualified
form the hand-written peel spelled.

The stdlib_struct snippet gains positive keyword `unpack_from` calls and
keyword-rejection assertions for pack/pack_into.

Assisted-by: Claude
Move the four keyword-accepting marshal functions to the `inline_functions:`
arm, so the macro derives each `Signature` and the gateway binds keywords
into PY_NULL-padded slots before the body runs, replacing the four
`split_builtin_kwargs` peels.

`value`/`version` are positional-only and `allow_code` keyword-only
(`dumps(value, version, /, *, allow_code=True)`); the `#[posonly] #[kwonly]`
markers stack on `allow_code` so `marker_posonly()` fires after every
positional param is appended. `version`/`allow_code` are `Option` so an
omitted slot takes the default while an explicit `None` still reaches
`int_w`/`is_true`. `dump`/`dumps` now reject the `version=` keyword the
marker path accepted through `parse_version`.

`marshal_to_bytes`/`unmarshal_bytes` hold the shared serialize/deserialize
bodies; `dumps_bytes`/`loads_bytes` delegate to them.

Assisted-by: Claude
struct `unpack_from` (module and method): coerce `offset` through
`space_index_w` before `readbuf` borrows the buffer's backing store as a raw
slice, so an `__index__` that resizes or frees that store cannot leave the
body reading freed memory. `Struct.pack_into` guards a PY_NULL `buffer` /
`offset` before conversion, reporting the missing argument instead of a
"cannot be interpreted as an integer" error.

binascii `slot_bool` returns `Result` and propagates `is_true` errors rather
than swallowing a raising `__bool__` / `__len__` with the default; a2b_qp,
b2a_qp, a2b_base64, b2a_base64, and b2a_uu take the result with `?`. `slot_u32`
and `crc_hqx` read their CRC seed through `truncatedint_w` (matching
`oldcrc='truncatedint_w'`), truncating a wide integer to 32 bits instead of
raising OverflowError; an omitted slot still defaults, but an explicit `None`
now raises.

zlib decompress (`Decompress` and `_ZlibDecompressor`): an explicit `None`
`max_length` goes through `int_w` and raises instead of being treated as the
omitted-slot default.

Assisted-by: Claude
builtin_code_call assumes an already-bound flat slice; its direct callers
pass the output of bind_kwargs_to_signature. The JIT residual-call path
(bh_call_fn_impl) instead handed it raw positionals, so a varargname-bearing
Signature builtin — _struct pack/pack_into and Struct.pack_into, whose bodies
read a packed *args tuple slot via w_tuple_items_copy_as_vec — received the
unbound arguments and read a non-tuple as a tuple, overrunning
w_tuple_getitem_known (SIGSEGV under JIT; test_pickle/test_pickletools
depend on struct.pack).

Rename finish_builtin_code_positional to builtin_code_call_positional, make it
pub, and route pyre-jit's residual builtin dispatch through it: it binds a
HOPELESS-arity Signature through _match_signature before invoking the body,
the same as the frame dispatch already did. builtin_code_call still never
binds, so the already-bound direct call sites are unchanged.

Assisted-by: Claude

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: fe5d0d8df8

ℹ️ 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 +5100 to +5102
return match pyre_interpreter::call::builtin_code_call_positional(
code as pyre_object::PyObjectRef,
&call_args,

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 Root the builtin code across signature binding

When the blackhole residual path calls a signature-bearing variadic builtin such as the newly registered _struct.pack, builtin_code_call_positional invokes bind_kwargs_to_signature, whose w_tuple_new can trigger the moving collector. The code passed here was copied from the rooted callable before that allocation but is not itself pinned or reloaded, so the subsequent builtin_code_call(current_code, ...) may dereference its evacuated address. The interpreter path explicitly pins code before the same helper; preserve that rooting/reload behavior here (or inside the helper) as well.

AGENTS.md reference: AGENTS.md:L14-L20

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@pyre/pyre-jit/src/call_jit.rs`:
- Around line 5096-5103: Update the dispatch branch around
builtin_code_call_positional to root or pin code before signature binding can
allocate GC objects, then reload the rooted code immediately before invoking the
positional builtin. Pass this reloaded pointer to the dispatch call instead of
the original pre-binding code pointer.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ccde6fee-11c8-45ca-b0e4-9f8fe7427633

📥 Commits

Reviewing files that changed from the base of the PR and between d20e34d and fe5d0d8.

📒 Files selected for processing (2)
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-jit/src/call_jit.rs

Comment on lines +5096 to +5103
// `call_args` are raw positionals; a HOPELESS-arity Signature
// (`*args`, optional positional) needs `_match_signature` binding
// before the body reads its slots. `builtin_code_call` never binds,
// so route through the positional entry that does.
return match pyre_interpreter::call::builtin_code_call_positional(
code as pyre_object::PyObjectRef,
&call_args,
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 \
  'fn bind_kwargs_to_signature|bind_kwargs_to_signature\(|builtin_code_call_positional\(|pin_root\(code|shadow_stack_get\(code_slot' \
  pyre

Repository: youknowone/pyre

Length of output: 46749


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- JIT caller context ---'
sed -n '5000,5135p' pyre/pyre-jit/src/call_jit.rs

printf '%s\n' '--- positional gateway and binder ---'
sed -n '900,980p' pyre/pyre-interpreter/src/call.rs
sed -n '2227,2520p' pyre/pyre-interpreter/src/call.rs

printf '%s\n' '--- code/function representation and accessors ---'
rg -n -C 10 \
  'fn getcode|pub .*getcode|struct BuiltinCode|BuiltinCode.*malloc|builtin_code_get_signature|builtin_code_call_positional' \
  pyre/pyre-interpreter pyre/pyre-object pyre/pyre-jit

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- BuiltinCode allocation and function code field ---'
sed -n '270,330p' pyre/pyre-interpreter/src/gateway.rs
sed -n '590,635p' pyre/pyre-interpreter/src/gateway.rs
sed -n '1098,1145p' pyre/pyre-interpreter/src/function.rs

printf '%s\n' '--- root setup and reload_args definition ---'
rg -n -C 18 \
  'fn reload_args|let root_base|push_roots\(\)|pin_root\(.*callable|pin_root\(.*arg' \
  pyre/pyre-jit/src/call_jit.rs

printf '%s\n' '--- binder allocation calls ---'
python3 - <<'PY'
from pathlib import Path
p = Path("pyre/pyre-interpreter/src/call.rs")
text = p.read_text()
start = text.index("pub(crate) fn bind_kwargs_to_signature")
end = text.index("\n/// [`call_with_kwargs_in_ctx`]", start)
body = text[start:end]
for i, line in enumerate(body.splitlines(), 1):
    if any(token in line for token in (
        "vec![", "Vec::", "w_tuple_new", "w_dict_new_kwargs",
        "w_str_from_wtf8_managed", "w_dict_store", "push_roots",
        "pin_root", "builtin_code_call",
    )):
        print(f"{start + body[:sum(len(x)+1 for x in body.splitlines()[:i-1])].count(chr(10)) + 1}: {line}")
PY

Repository: youknowone/pyre

Length of output: 16474


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 16 \
  'pub fn builtin_code_new|fn builtin_code_new|builtin_code_new_with_signature|malloc_typed|BUILTIN_CODE_TYPE|BuiltinCode.*GC' \
  pyre/pyre-interpreter/src/gateway.rs pyre/pyre-interpreter/src/function.rs pyre

printf '%s\n' '--- direct JIT builtin dispatch and root slot arithmetic ---'
sed -n '4946,4975p' pyre/pyre-jit/src/call_jit.rs
sed -n '5052,5112p' pyre/pyre-jit/src/call_jit.rs

Repository: youknowone/pyre

Length of output: 50372


Verify the code root before signature binding.

bind_kwargs_to_signature can allocate GC objects when it packs *args or **kwargs. Pin code before calling builtin_code_call_positional, then reload it from the root immediately before dispatch. The current branch passes the pre-binding pointer to builtin_code_call.

🤖 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 `@pyre/pyre-jit/src/call_jit.rs` around lines 5096 - 5103, Update the dispatch
branch around builtin_code_call_positional to root or pin code before signature
binding can allocate GC objects, then reload the rooted code immediately before
invoking the positional builtin. Pass this reloaded pointer to the dispatch call
instead of the original pre-binding code pointer.

@youknowone
youknowone merged commit a009db9 into main Aug 11, 2026
28 of 29 checks passed
@youknowone
youknowone deleted the rtyper-legacy branch August 11, 2026 01:54
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