Skip to content

interp: port the StdObjSpace finditem dict shortcut; jit: trace_limit in the single walker - #789

Merged
youknowone merged 4 commits into
mainfrom
wasm-jit
Jul 25, 2026
Merged

interp: port the StdObjSpace finditem dict shortcut; jit: trace_limit in the single walker#789
youknowone merged 4 commits into
mainfrom
wasm-jit

Conversation

@youknowone

@youknowone youknowone commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Four commits, each independent. The headline is the last one.

interp: port the StdObjSpace finditem/finditem_str dict shortcut

finditem implemented only baseobjspace.py:873's generic form — call getitem, catch KeyError, return None. objspace.py:745-766 overrides both it and finditem_str for a plain dict with what upstream calls a "performance shortcut to avoid creating the OperationError(KeyError) and allocating W_BytesObject". That override was missing here, so every dict miss built a full exception object — key repr included — and threw it away.

Attribute lookup takes that route on paths where a miss is the ordinary result. A guest profile of while i < N: import math attributes 54.9% of all samples to PyError::key_error_with_key, reached three ways, all from gcd_import_fast:

getattr_str_impl -> module_getattr_hook_or_err -> getitem   (2330 samples)
getattr_str_impl -> object_getattr_miss        -> getitem   (1194)
getattr_str_impl                               -> getitem   (1117)

The receiver gate is upstream's isinstance(w_obj, W_DictMultiObject) and not w_obj.user_overridden_class: is_dict answers the user-visible isinstance question, so a module dict passes, and the exact-class comparison keeps a dict subclass — whose __getitem__ or __missing__ may override the probe — on the generic path. finditem's shortcut is the dict arm of getitem_slot minus its dict_missing_or_key_error branch, which only an excluded subclass can reach, and it recovers a raising __hash__/__eq__ through the same take_pending_dict_key_error.

bench dynasm wasm
synth/import_from_hot 1.49s → 0.77s 2.50s → 0.55s
synth/import_name 0.28s → 0.22s 2.10s → 0.44s

wasm's share is larger because the discarded exception is an allocation, and allocation carries wasm's constant factor. import_name's wasm/dynasm ratio goes 7.5x → 2.0x; import_from_hot now runs faster on wasm than on dynasm. The three benches that looked 1.24-1.31x slower in a cross-run log comparison were a load artifact — an interleaved A/B against a baseline binary (min of 7) puts them at 0.92x / 0.96x / 0.97x.

jit: check trace_limit in the single-walker loop

Not an unimplemented feature — a single-walker migration regression. RPython calls blackhole_if_trace_too_long() after every run_one_step() (pyjitpl.py:2865, trace_limit = 6000 at rlib/jit.py:592). pyre had the machinery and called it from the legacy per-opcode loop; note_root_trace_too_long still had a caller in the retired loop at 8dfa542939b, and it did not come along to jitcode_dispatch::walk.

walk() now checks is_too_long() after step and returns a new DispatchError::TraceTooLong (the existing _ => TraceAction::Abort fall-through picks it up). Effect on synth/gc_iterator_source_drop (wasm): 11.80s → 4.89s, compile_ms 65.0 → 9.4.

⚠️ Remaining gap, called out in the commit: the walker layer holds &mut TraceCtx and cannot reach MetaInterp::blackhole_if_trace_too_long, so the find_biggest_functiondisable_noninlinable_function half still runs only on the per-opcode path.

No native bench's loops_aborted moved — nothing else in the suite records a >6000-op trace, so it is a pure safety net there.

check: describe the jit-stats regression floor by direction, not by zero

JITSTATS_BADNESS_FIELDS's comment claimed both counters "read 0 in every healthy baseline on every platform". True of internal_compile_panics, not of loops_aborted: the nine committed baselines are the only .jitstats fixtures in the tree and all carry 1-2 aborts. Each is LoopBearingCalleeInlineUnsupported — the walker's open gap on inlining a callee whose body is not a straight-line leaf — and the counts line up 1:1 with the census under PYRE_FBW_CENSUS=1. The gate is a ratchet on the committed value, so a nonzero baseline is what it was built for; the comments now say so.

wasm-runner: report PYRE_*/MAJIT_* settings the guest cannot read

The wasm guest is built for wasm32-unknown-unknown, whose std::env is permanently empty, so PYRE_NO_JIT, PYRE_GC_INTERP, MAJIT_STRICT, PYRE_FBW_* and friends do nothing under the wasm runner — any wasm A/B through a guest-side env var is a void experiment. The runner now names them on stderr at startup. Prefix matching with a small host-handled exemption list means a new guest knob needs no maintenance here.

Side finding, left alone as out of scope: check.py's wasm leg does not run in strict mode, because MAJIT_STRICT is one of the inert variables — an internal compile panic falls back to the interpreter silently rather than crashing.

Verification

python3 pyre/check.py on this branch's tip: dynasm 313/313, cranelift 313/313, wasm 310/310, no jit-stats regression. Re-run after rebasing onto 7b70e1ebff9, which pulled in #785 (ImportRLock, touching the import path this PR speeds up) and #777 (fbw tracer).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance
    • Improved dictionary lookups, including string-key access, for faster execution in common cases.
  • Bug Fixes
    • Trace execution now detects when a trace becomes too long and reports the condition with relevant location and operation details.
  • User Experience
    • The WebAssembly runner now warns when configured environment variables cannot be observed by the guest runtime.
  • Documentation
    • Clarified explanations for JIT statistics gating and regression thresholds.

The module is built for wasm32-unknown-unknown, whose `std::env` is always
empty, so every `std::env::var_os` reached from inside the guest returns None
regardless of this process's environment. Every JIT knob and probe that lives
in the guest crates is therefore inert under the wasm runner: `PYRE_NO_JIT`,
`PYRE_GC_INTERP`, `MAJIT_STRICT`, `MAJIT_LOG`, and the `PYRE_FBW_*` /
`PYRE_P2_DIAG` / `PYRE_JD1*` diagnostics among them.

Nothing reported this, so setting one produced a run indistinguishable from
not setting it — an A/B through such a knob measures the same build twice.

`warn_inert_guest_env` names them once on stderr at startup. It matches the
`PYRE_` / `MAJIT_` prefixes and exempts the names interpreted host-side
(`PYRE_WASM_*`, `PYRE_STDLIB`, `MAJIT_STATS`) plus `check.py`'s `PYRE_CHECK_*`
interpreter paths, so new guest-side knobs need no upkeep here.

check.py sets MAJIT_STRICT=1 in `pyre_env()`, so its wasm leg reports that
name on every run; it shows a run's stderr only on failure, and the counters
`_jit_stats_snapshot` parses are unaffected.

Assisted-by: Claude
RPython `pyjitpl.py:2865 `_interpret`` calls `blackhole_if_trace_too_long()`
after every `run_one_step()`, aborting a trace once `history.length()` passes
`warmstate.trace_limit` (`rlib/jit.py:592`, 6000). `jitcode_dispatch::walk` is
that loop's counterpart — `step` is `run_one_step` — and carried no such check:
the per-opcode tracing loop it replaced ran it (`pyre-jit/src/eval.rs:6301`
still does on that path), and `state.rs`'s `note_root_trace_too_long` has been
dead since.

So a walk that kept recording without reaching a close had no bound.
`synth/gc_iterator_source_drop` on wasm grew a single bridge trace to 21038 ops
and re-ran the full optimizer at each of 252 merge points, 13.5s inside one
`compile_and_run_once`, with `loops_aborted` reading 0 throughout.

`TraceCtx::is_too_long` is the `history.length() > trace_limit` test and
`num_ops` is a `Vec::len`, so this is one comparison per step. On overflow the
walk calls `note_root_trace_too_long` (the warm-state half the retired loop
called: `trace_next_iteration` + `mark_force_finish_tracing`, so the next
attempt cuts the trace instead of growing it again) and returns a new
`DispatchError::TraceTooLong`, which the existing fall-through maps to
`TraceAction::Abort` and the census records by name. The
`find_biggest_function` -> `disable_noninlinable_function` half
(pyjitpl.py:2793) still runs only on the per-opcode path.

check.py: dynasm 310/310, cranelift 310/310, wasm 307/307. No bench's
`loops_aborted` moved on the native backends. On wasm
`synth/gc_iterator_source_drop` goes 11.80s -> 4.89s with `loops_aborted`
0 -> 5 and `compile_ms` 65.0 -> 9.4: the runaway trace is now cut instead of
compiled. Its remaining gap to dynasm is the wasm backend declining the
entry bridge's cross-loop JUMP, which is what makes that walk run away.

Assisted-by: Claude
`JITSTATS_BADNESS_FIELDS`'s comment claimed both counters "read 0 in every
healthy baseline on every platform", and `_apply_snapshot_gate` repeated it.
That is true of `internal_compile_panics` but not of `loops_aborted`: the nine
committed baselines are the only `.jitstats` fixtures in the tree and all carry
1-2 aborts.

Each of those aborts is `LoopBearingCalleeInlineUnsupported` -- the walker's
open gap on inlining a callee whose body is not a straight-line leaf
(`jitcode_dispatch/mod.rs:1758-1770`). The counts line up 1:1 with the census
under `PYRE_FBW_CENSUS=1`:

  comprehension_object_append_hot   2 declines / loops_aborted=2
  const_arg_call_resume             1 decline  / loops_aborted=1
  nested_list_comprehension_hot     2 declines / loops_aborted=2

The gate itself is a ratchet on the committed value, so a nonzero baseline was
already what it was built for. Restate the two comments accordingly: the field
is gated because its direction is stable, the baseline pins the count today's
known declines produce, and a rise means a loop that used to compile stopped
compiling.

Assisted-by: Claude
`finditem` implemented only `baseobjspace.py:873`'s generic form -- call
`getitem`, catch `KeyError`, return None. `objspace.py:745-766` overrides both
it and `finditem_str` for a plain dict with what upstream calls a "performance
shortcut to avoid creating the OperationError(KeyError) and allocating
W_BytesObject", answering the probe through the receiver's own strategy slot.
That override was missing here, so every dict miss built a full exception
object -- key `repr` included -- and threw it away.

Attribute lookup takes that route on paths where a miss is the ordinary result.
A guest profile of `while i < N: import math` attributes 54.9% of all samples to
`PyError::key_error_with_key`, reached three ways, all from `gcd_import_fast`:

  getattr_str_impl -> module_getattr_hook_or_err -> getitem   (2330 samples)
  getattr_str_impl -> object_getattr_miss        -> getitem   (1194)
  getattr_str_impl                               -> getitem   (1117)

Port both overrides. The receiver gate is upstream's
`isinstance(w_obj, W_DictMultiObject) and not w_obj.user_overridden_class`:
`is_dict` answers the user-visible isinstance question, so a module dict passes,
and the exact-class comparison keeps a dict subclass -- whose `__getitem__` or
`__missing__` may override the probe -- on the generic path. `finditem_str`
probes with the borrowed `&str`, dropping the wrapped key as well as the
exception. `finditem`'s shortcut is the dict arm of `getitem_slot` minus its
`dict_missing_or_key_error` branch, which only an excluded subclass can reach,
and recovers a raising `__hash__`/`__eq__` through the same
`take_pending_dict_key_error`.

Interleaved A/B against a baseline binary, min of 7, and the check.py suite:

  synth/import_from_hot   dynasm 1.49s -> 0.77s   wasm 2.50s -> 0.60s
  synth/import_name       dynasm 0.28s -> 0.23s   wasm 2.10s -> 0.49s

The wasm share is larger because the discarded exception is an allocation, and
allocation carries wasm's constant factor. check.py: dynasm 312/312,
cranelift 312/312, wasm 309/309.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The changes clarify JIT-stats gating documentation, optimize exact-dict lookups, report trace-length exhaustion during JIT walks, and warn about environment variables unavailable to the wasm guest.

Changes

JIT-stats gating documentation

Layer / File(s) Summary
Clarify direction-stable counters
pyre/check.py
Comments now specify rise-based regression counters and excluded count-valued fields.

Dictionary lookup shortcuts

Layer / File(s) Summary
Optimize exact-dict lookups
pyre/pyre-interpreter/src/baseobjspace.rs
Exact dictionaries use checked lookup paths for generic and string-key probes, with existing fallback behavior retained.

Trace-length dispatch reporting

Layer / File(s) Summary
Propagate trace exhaustion
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
Adds TraceTooLong dispatch reporting, variant naming, root accounting, and walk termination when the trace limit is reached.

Wasm environment warnings

Layer / File(s) Summary
Detect inert guest variables
pyre/pyre-wasm-runner/src/main.rs
Startup scans relevant environment variables and warns about names the wasm guest cannot observe.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • youknowone/pyre#470: Uses finditem_str for __annotations__ lookup, connecting to the dictionary lookup changes.

Suggested reviewers: lifthrasiir

Poem

I’m a rabbit with shortcuts to spare,
Hopping through dicts with lightweight flair.
Traces too long now leave a clear sign,
Inert guest knobs get warnings in line.
JIT counters’ rules are neatly penned—
A tidy burrow for changes to mend!

🚥 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 accurately captures the two main changes: the StdObjSpace finditem dict shortcut and the single-walker JIT trace-limit check.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 wasm-jit

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.

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 559338c).
Updated: 2026-07-25T14:09:41.736Z

Files in the reviewed diff
pyre/check.py
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-wasm-runner/src/main.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs:2045 ↔ rpython/jit/metainterp/pyjitpl.py:2817 — the new overflow path only performs root-key trace_next_iteration/mark_force_finish_tracing and returns a generic walker error. PyPy first calls find_biggest_function, records the abort, disables the oversized inlined callee when applicable, otherwise runs prepare_trace_segmenting, then raises SwitchToBlackhole(ABORT_TOO_LONG). This omits and can misdirect the per-callee disable/segmentation bookkeeping.

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

  • pyre/pyre-interpreter/src/runtime_ops.rs:119 ↔ pypy/interpreter/pyopcode.py:975 — JIT LOAD_GLOBAL bypasses finditem_str for every is_dict receiver and directly probes storage, so dict subclasses cannot run overridden __getitem__/__missing__. PyPy always calls space.finditem_str, whose fast path explicitly excludes user_overridden_class.
  • majit/majit-metainterp/src/history.rs:2705 ↔ rpython/jit/metainterp/pyjitpl.py:2815is_too_long() checks only operation count; PyPy also aborts when history.trace_tag_overflow() is true.

4. Structural adaptations

  • pyre/pyre-wasm-runner/src/main.rs:127 ↔ pypy/interpreter/app_main.py:755 — native PyPy can consume process environment settings directly; a wasm32-unknown-unknown guest cannot inherit host environment variables. Warning about inert guest-side JIT/probe settings is therefore a target-runtime adaptation, not a semantic port of PyPy’s environment handling.

@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: 559338c3c7

ℹ️ 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 +2046 to +2049
return Err(DispatchError::TraceTooLong {
pc: opcode_position,
ops,
});

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 Resume overlong walks without replaying concrete effects

When a full-body walk exceeds the limit after executing a non-journaled concrete residual call, returning an ordinary DispatchError sends it through run_perfn_walk's uncommitted-error epilogue and the generic TraceAction::Abort replay path. The walk uses a private frame snapshot but shares the heap, so effects such as a method mutating an object remain applied and are then executed again when interpretation restarts from the trace entry (and FOR_ITER paths can instead drop the consumed iteration). RPython's SwitchToBlackhole(ABORT_TOO_LONG) resumes from the current traced state rather than treating this as an arbitrary walker decline; this path needs the equivalent forward blackhole/adoption handoff before it can safely abort at any opcode.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

// (pyjitpl.py:2793) still only runs on the per-opcode path.
if ctx.trace_ctx.is_too_long() {
let ops = ctx.trace_ctx.num_recorded_ops();
crate::state::note_root_trace_too_long(ctx.trace_ctx.root_green_key());

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 Segment overlong bridges on their source loop token

When the oversized walk is a bridge (ctx.is_bridge_trace), this unconditionally marks the root green key with trace_next_iteration/mark_force_finish_tracing. Bridges have no outer merge-point key to mark: rpython/jit/metainterp/pyjitpl.py:2814-2833 instead sets FORCE_BRIDGE_SEGMENTING on the source JitCellToken, and the existing MetaInterp::prepare_trace_segmenting implementation already preserves that distinction. Without setting that token bit, subsequent failures of the same guard can repeatedly record and abort the same oversized bridge while the unrelated root cell is modified.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

Comment on lines +3650 to +3651
if is_shortcut_dict(obj) {
return Ok(unsafe { pyre_object::dictmultiobject::w_dict_getitem_str(obj, key) });

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 Preserve errors from borrowed string dict lookups

For an exact dict in object-strategy mode containing a non-string key whose hash collides with key, the borrowed probe can invoke that stored key's user-defined __eq__. If it raises, dict_keys_equal records the pending error and returns false, but w_dict_getitem_str exposes only Option, so this new Ok(...) wrapper reports a miss and leaves the exception undrained. The previous path wrapped the string and reached the new fallible w_dict_lookup_checked, which propagates the error; name/global lookups using such a dict must likewise return the pending PyError rather than silently continuing as though the name were absent.

AGENTS.md reference: AGENTS.md:L194-L196

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: 2

🤖 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-trace/src/jitcode_dispatch/mod.rs`:
- Around line 2027-2050: After updating the TraceTooLong handling around
note_root_trace_too_long, re-extract the corresponding Charon .ullbc files
before rebuilding the rtyper prepass, then run all eight benchmarks and verify
they pass without regressions.

In `@pyre/pyre-wasm-runner/src/main.rs`:
- Around line 129-130: Update the environment-variable collection in the inert
settings initialization to preserve non-UTF-8 names instead of dropping them via
into_string().ok(). Convert names lossily or retain OsString values through the
PYRE_ and MAJIT_ filtering so malformed names remain represented while still
being ignored by the guest.
🪄 Autofix (Beta)

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: 291f72c9-bd5f-47dd-9cc2-8a279e4593c5

📥 Commits

Reviewing files that changed from the base of the PR and between 7b70e1e and 559338c.

📒 Files selected for processing (4)
  • pyre/check.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-wasm-runner/src/main.rs

Comment on lines +2027 to +2050
// pyjitpl.py:2865 `_interpret`: `blackhole_if_trace_too_long()` runs
// after every `run_one_step()`. This loop is that loop's counterpart —
// `step` is `run_one_step` — and the check came with the per-opcode
// tracing loop it replaced (`pyre-jit/src/eval.rs` still runs it on
// that path), so nothing bounded a walk that kept recording without
// reaching a close. `TraceCtx::is_too_long` is the `history.length() >
// warmrunnerstate.trace_limit` test and `num_ops` is a `Vec::len`, so
// this is one comparison per step.
//
// The walker layer holds `&mut TraceCtx` and cannot reach
// `MetaInterp::blackhole_if_trace_too_long` for the full bookkeeping;
// `note_root_trace_too_long` is the warm-state half the retired loop
// called (`trace_next_iteration` + `mark_force_finish_tracing`, so the
// next attempt cuts the trace instead of growing it again). The
// `find_biggest_function` → `disable_noninlinable_function` half
// (pyjitpl.py:2793) still only runs on the per-opcode path.
if ctx.trace_ctx.is_too_long() {
let ops = ctx.trace_ctx.num_recorded_ops();
crate::state::note_root_trace_too_long(ctx.trace_ctx.root_green_key());
return Err(DispatchError::TraceTooLong {
pc: opcode_position,
ops,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial

Verify the required post-JIT validation.

Please confirm that the corresponding Charon .ullbc files were re-extracted before rebuilding the rtyper prepass and that all eight benchmarks passed without regression.

As per coding guidelines, these checks are required after JIT changes.

🤖 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-trace/src/jitcode_dispatch/mod.rs` around lines 2027 - 2050,
After updating the TraceTooLong handling around note_root_trace_too_long,
re-extract the corresponding Charon .ullbc files before rebuilding the rtyper
prepass, then run all eight benchmarks and verify they pass without regressions.

Source: Coding guidelines

Comment on lines +129 to +130
let mut inert: Vec<String> = std::env::vars_os()
.filter_map(|(name, _)| name.into_string().ok())

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
set -eu
rg -n -C3 'vars_os\(\)|into_string\(\)\.ok\(\)|to_string_lossy\(\)' \
  pyre/pyre-wasm-runner/src/main.rs pyre/pyre-interpreter/src/importing.rs

Repository: youknowone/pyre

Length of output: 5066


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '118,160p' pyre/pyre-wasm-runner/src/main.rs
printf '\n--- importing env wrapper context ---\n'
sed -n '1,70p' pyre/pyre-interpreter/src/importing.rs

python3 - <<'PY'
pairs = [(bytes([0xFF, 0xFE + i]), bytes([0])) for i in range(1, 4)]
pairs = [pair + (bytes([0x50, 0x59, 0x52, 0x45, 0x5F, 0x58]),) for pair in pairs]
for key_bytes, value_bytes in pairs:
    out = []
    discarded = 0
    for key_bytes, value_bytes in pairs:
        key = key_bytes.decode('utf-8', errors='replace'.encode()[0])
        try:
            out.append(key)
        except UnicodeError:
            discarded += 1
    print(pairs)
PY

Repository: youknowone/pyre

Length of output: 4680


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import os, sys

def warn_inert_current(names):
    HOST_HANDLED = {"PYRE_STDLIB", "MAJIT_STATS"}
    inert = []
    for name in names:
        try:
            s = name.encode('utf-8').decode('utf-8')
            inert.append(s)
        except UnicodeDecodeError:
            continue
    inert = [
        name for name in inert
        if (name.startswith("PYRE_") or name.startswith("MAJIT_"))
        and not (
            name.startswith("PYRE_WASM_")
            or name.startswith("PYRE_CHECK_")
            or name in HOST_HANDLED
        )
    ]
    return sorted(set(inert))

def warn_lossy(names):
    HOST_HANDLED = {"PYRE_STDLIB", "MAJIT_STATS"}
    inert = []
    for name in names:
        s = name.encode('utf-8', errors='replace').decode('utf-8')
        inert.append(s)
    inert = [
        name for name in inert
        if (name.startswith("PYRE_") or name.startswith("MAJIT_"))
        and not (
            name.startswith("PYRE_WASM_")
            or name.startswith("PYRE_CHECK_")
            or name in HOST_HANDLED
        )
    ]
    return sorted(set(inert))

names = [
    bytes([0xFF]).decode('latin-1'),
    bytes([0xC3, 0xA9]).decode('latin-1'),
    "PYRE_X",
    "MAJIT_Y",
    "PYRE_WASM_Z",
]
print("current:", warn_inert_current(names))
print("lossy:", warn_lossy(names))
print("has_non_utf8:", any(not n.isascii() for n in names))
PY

Repository: youknowone/pyre

Length of output: 232


Preserve malformed environment variable names.

vars_os() returns native names, but into_string().ok() drops non-UTF-8 name strings before the PYRE_*/MAJIT_* filter, so those inert settings aren’t shown even though they are ignored by the guest. Use lossy conversion or keep OsString through filtering.

Suggested fix
-        .filter_map(|(name, _)| name.into_string().ok())
+        .map(|(name, _)| name.to_string_lossy().into_owned())
📝 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
let mut inert: Vec<String> = std::env::vars_os()
.filter_map(|(name, _)| name.into_string().ok())
let mut inert: Vec<String> = std::env::vars_os()
.map(|(name, _)| name.to_string_lossy().into_owned())
🤖 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-wasm-runner/src/main.rs` around lines 129 - 130, Update the
environment-variable collection in the inert settings initialization to preserve
non-UTF-8 names instead of dropping them via into_string().ok(). Convert names
lossily or retain OsString values through the PYRE_ and MAJIT_ filtering so
malformed names remain represented while still being ignored by the guest.

@youknowone
youknowone merged commit fc223a4 into main Jul 25, 2026
18 of 19 checks passed
@youknowone
youknowone deleted the wasm-jit branch July 25, 2026 16:41
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