Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.d/8685-fix-main-red-lint-cargo-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed four pre-existing `main` gate failures, none caused by an in-flight PR:

- `scripts/shape_descriptor_census.py` still asserted the old `select(I1, &is_stamp, I64, &id_token, "0")` fail-closed shape in the generic property-read PIC. PR #8665 legitimately replaced it with `icmp_ne(I32, &pcid, "0")` (a documented six-instruction perf win) but never updated this assertion. It also missed the two new, legitimate `object_header_size_bytes(...)` call sites `crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs` gained in PR #8680 — the checked-in baseline is refreshed to include them.
- `scripts/addr_class_inventory.py`'s `lone-valid-obj-ptr` rule reimplemented a narrower, ad hoc lookahead window (1 code line) instead of reusing the already-tested `band_predicate_near` helper (6 code lines, comment/blank-aware) used by the `handle-floor` rule. That gap flagged a legitimately paired `is_valid_obj_ptr` + `try_read_gc_header` guard PR #8680 added in `crates/perry-runtime/src/array/subclass.rs` as a false positive. Fixing the rule to call the shared helper also cleared several other long-standing false positives across the tree, so the ratchet baseline is refreshed down to the now-accurate counts.
- `commands::compile::build_cache::tests::codegen_env_vars_are_build_cache_inputs` failed because two new codegen env vars were never classified: `PERRY_CONST_ARRAY_DESCRIPTOR` (PR #8583's array-literal const-descriptor gate — changes emitted IR, added to `BUILD_CACHE_ENV_VARS`) and `PERRY_DIALECT_DUMP` (only read on an already-fatal dialect-construction failure, so it can't affect a successful build's bytes — added to `BUILD_CACHE_ENV_EXCLUSIONS`).
- `scripts/check_thread_locals.py` failed because PR #8640 added a raw `thread_local!` block in `crates/perry-runtime/src/node_submodules/test_runner.rs` instead of `crate::perry_thread_local!`. Swapped it to the hot-path macro (same syntax, same `.with()` call sites).
8 changes: 8 additions & 0 deletions crates/perry/src/commands/compile/build_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ const BUILD_CACHE_ENV_VARS: &[&str] = &[
// #8105 — number-by-construction locals (see the collector of the same
// name); `=0` empties the fact and changes every affected function's IR.
"PERRY_NUMBER_BY_CONSTRUCTION",
// #8583 follow-up gate: `=0/off/false` reverts every large constant array
// literal from the const-descriptor path to procedural construction —
// different emitted IR for the same source.
"PERRY_CONST_ARRAY_DESCRIPTOR",
];

/// #7183: codegen env vars that deliberately do NOT key the build cache.
Expand Down Expand Up @@ -147,6 +151,10 @@ const BUILD_CACHE_ENV_EXCLUSIONS: &[&str] = &[
"PERRY_CODEGEN_UNIT_TIMINGS",
// Entry outlining report output is observational only.
"PERRY_OUTLINE_ENTRY_REPORT",
// Only read on an already-fatal dialect-construction failure (a unit that
// never parses); it writes a diagnostic IR dump to `<dir>/<name>.ll` for
// triage and cannot affect the bytes of any build that actually succeeds.
"PERRY_DIALECT_DUMP",
];

#[cfg(test)]
Expand Down
148 changes: 143 additions & 5 deletions scripts/addr_class_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,76 @@ def band_predicate_near(lines: list[str], idx: int) -> bool:
return bool(BAND_PREDICATE_RE.search("\n".join(context)))


# A new SIBLING conditional at the top of a code line — the boundary
# `lone_band_predicate_near` refuses to cross. `handle-floor`'s own #6321 fix
# shape deliberately spans two separate `if` statements (a coarse range
# pre-filter, then the real band-predicate gate a few lines below), so
# `band_predicate_near`'s generic forward scan must keep allowing that. But
# `lone-valid-obj-ptr`'s real safe shape is "guard early-returns, the very
# next statement is the band-predicate call" (see array/subclass.rs) — it
# never needs to reach into an unrelated LATER conditional to find its
# predicate. Stopping there closes the false negative a CodeRabbit review
# caught: an `is_valid_obj_ptr`-only guard that already dereferences inside
# its own block must not be cleared by some other, disconnected `if
# is_handle_band(...)` a few statements later.
#
# A conditional NESTED inside the guard's own block (brace depth still > 0
# relative to the guard) is not that boundary — a second CodeRabbit pass
# caught this rule stopping on `if is_valid_obj_ptr(ptr) { if
# is_above_handle_band(...) { deref } }`, where the inner `if` is exactly the
# band predicate protecting the dereference, not a disconnected sibling.
SIBLING_CONDITIONAL_RE = re.compile(r"^\s*(?:\}\s*)?(?:else\s+)?(?:if|while|for|match)\b")


def apply_brace_deltas(depth: int, code: str) -> int:
"""Advance `depth` by `code`'s braces, IN SOURCE ORDER, floored at zero.

A net per-line count (`code.count("{") - code.count("}")`) gets an `}
else if ... {` line wrong: net zero reads as "still at the same depth",
but the leading `}` closes the ENCLOSING block first, so the following
`{` actually opens a brand-new nested one — depth should end at one
level deeper, not unchanged. Processing character by character (with
the floor so a leading close belonging to an outer block we don't track
can't push depth negative) gets this right.
"""

for char in code:
if char == "{":
depth += 1
elif char == "}":
depth = max(depth - 1, 0)
return depth


def lone_band_predicate_near(lines: list[str], idx: int) -> bool:
"""`band_predicate_near`, scoped to the current guard's own statement.

Same backward window; the forward scan stops (without including) at the
first later line that opens a new SIBLING conditional — one reached only
after the brace depth relative to `lines[idx]` has returned to zero or
below, i.e. the guard's own block (and any block nested inside it) has
already closed. A conditional still nested inside that block never
triggers the boundary, since it may be the very predicate guarding the
dereference.
"""

start = max(0, idx - BAND_PREDICATE_LOOKBACK)
context = [strip_comment(line) for line in lines[start : idx + 1]]
depth = apply_brace_deltas(0, context[-1])
taken = 0
cursor = idx + 1
while cursor < len(lines) and taken < BAND_PREDICATE_LOOKAHEAD_CODE:
code = strip_comment(lines[cursor])
if code.strip():
if depth <= 0 and SIBLING_CONDITIONAL_RE.search(code):
break
context.append(code)
taken += 1
depth = apply_brace_deltas(depth, code)
cursor += 1
return bool(BAND_PREDICATE_RE.search("\n".join(context)))


def scan_text(rel_path: str, text: str) -> list[Finding]:
findings: list[Finding] = []
if any(rel_path.startswith(prefix) for prefix in EXCLUDED_PREFIXES):
Expand All @@ -214,11 +284,7 @@ def scan_text(rel_path: str, text: str) -> list[Finding]:
findings.append(Finding(rel_path, line_no, "handle-floor", raw))
if VALID_OBJ_PTR_RE.search(code) and "fn is_valid_obj_ptr" not in code:
# A band predicate anywhere in the enclosing guard clears it.
start = max(0, idx - BAND_PREDICATE_LOOKBACK)
context = "\n".join(
strip_comment(l) for l in lines[start : idx + 2]
)
if not BAND_PREDICATE_RE.search(context):
if not lone_band_predicate_near(lines, idx):
findings.append(
Finding(rel_path, line_no, "lone-valid-obj-ptr", raw)
)
Expand Down Expand Up @@ -414,6 +480,78 @@ def expect(cond: bool, message: str) -> None:
),
"lone-valid-obj-ptr must not flag the definition",
)
# The real safe shape (array/subclass.rs): a guard that only early-returns,
# then the very next statement is the band predicate.
early_return_then_predicate = (
" if obj.is_null() || !is_valid_obj_ptr(obj.cast::<u8>()) {\n"
" return None;\n"
" }\n"
" let header = unsafe { try_read_gc_header(obj as usize)? };\n"
)
expect(
not any(
f.rule == "lone-valid-obj-ptr"
for f in scan_text(runtime, early_return_then_predicate)
),
"lone-valid-obj-ptr must accept a guard that early-returns "
"then reads the band predicate in the very next statement",
)
# CodeRabbit (PR #8685): a dereference inside an UNGUARDED is_valid_obj_ptr
# block must still be flagged even when a wholly unrelated, later sibling
# `if` happens to test a band predicate — that predicate does not guard
# the dereference above it.
separated_statement = (
" if is_valid_obj_ptr(ptr) {\n"
" (*ptr).class_id\n"
" }\n"
" if is_handle_band(other) {}\n"
)
expect(
any(
f.rule == "lone-valid-obj-ptr"
for f in scan_text(runtime, separated_statement)
),
"lone-valid-obj-ptr must still flag a dereference cleared only by "
"a disconnected LATER sibling conditional's band predicate",
)
# CodeRabbit (PR #8685, second pass): a band predicate NESTED inside the
# guard's own block — not a disconnected sibling — must still clear the
# finding, since it is exactly what protects the dereference below it.
nested_guard = (
" if is_valid_obj_ptr(ptr) {\n"
" if is_above_handle_band(ptr as usize) {\n"
" (*ptr).class_id\n"
" }\n"
" }\n"
)
expect(
not any(
f.rule == "lone-valid-obj-ptr" for f in scan_text(runtime, nested_guard)
),
"lone-valid-obj-ptr must accept a band predicate NESTED inside the "
"guard's own block",
)
# CodeRabbit (PR #8685, third pass): the guard itself following an `else
# if` chain (`} else if is_valid_obj_ptr(ptr) {`) must not miscompute its
# own starting depth as zero — a net per-line brace count treats the
# leading close and the trailing open as cancelling out, when the close
# belongs to the PRIOR branch and the open starts the guard's own block.
else_if_guard = (
" if some_other_check() {\n"
" do_other_thing();\n"
" } else if is_valid_obj_ptr(ptr) {\n"
" if is_above_handle_band(ptr as usize) {\n"
" (*ptr).class_id\n"
" }\n"
" }\n"
)
expect(
not any(
f.rule == "lone-valid-obj-ptr" for f in scan_text(runtime, else_if_guard)
),
"lone-valid-obj-ptr must accept a nested band predicate when the "
"guard itself is an `else if` branch",
)

# Band literals in code are caught; comment-only mentions are not.
hits = scan_text(runtime, "if addr < 0x100000 {\n")
Expand Down
6 changes: 1 addition & 5 deletions scripts/addr_class_ratchet_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -218,31 +218,27 @@ handle-floor | crates/perry-stdlib/src/webcrypto/jwk.rs | 1
handle-floor | crates/perry-stdlib/src/webcrypto/supports.rs | 1
handle-floor | crates/perry-stdlib/src/webcrypto/util.rs | 5
handle-floor | crates/perry-stdlib/src/zlib.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/array/subclass.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/buffer/access.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/closure/dynamic_props.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/collection_iter.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/error.rs | 3
lone-valid-obj-ptr | crates/perry-runtime/src/intl.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/intl/ctor_guard.rs | 2
lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/class_meta.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/construct.rs | 5
lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/construct.rs | 3
lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/prototype_objects.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/descriptors.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/accessors.rs | 2
lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/enumeration.rs | 3
lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/field_ops.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/has_property.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/ic_miss.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/field_set_by_name/attr_variants.rs | 2
lone-valid-obj-ptr | crates/perry-runtime/src/object/global_this/array_error.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/global_this/fetch_globals.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/global_this/typed_array.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/iterator_prototypes.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method.rs | 3
lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method/common_methods.rs | 2
lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method/primitive_methods.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/native_this_alias.rs | 1
lone-valid-obj-ptr | crates/perry-runtime/src/object/object_literal_ops.rs | 1
Expand Down
24 changes: 23 additions & 1 deletion scripts/shape_descriptor_census.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None:
)
require_code(
generic_body,
r"select\s*\(\s*I1\s*,\s*&is_stamp\s*,\s*I64\s*,\s*&id_token\s*,\s*\"0\"\s*\)",
r"icmp_ne\s*\(\s*I32\s*,\s*&pcid\s*,\s*\"0\"\s*\)",
"generic read PIC invalid-id fail-closed token",
)
for name in ("lower_put_value_static_write_ic", "lower_put_value_dyn_ic_inline"):
Expand Down Expand Up @@ -815,6 +815,28 @@ def run_sabotage_selftests(sources: dict[str, str], baseline: dict[str, object])
lambda: assert_authority_surfaces(legacy_ir),
)

# #8665: the generic read PIC's invalid-id fail-closed token (pcid != 0)
# must not go quietly missing. Plant a regression that emits an
# always-nonzero comparand instead of the real ShapeId register, and
# prove the census still catches it -- this is what stands between the
# check above and a vacuous pass, per #6942/#6946/#7024's precedent that
# an unexercised assertion is a decision nobody actually made.
dropped_fail_closed = dict(sources)
path = "crates/perry-codegen/src/expr/property_get/generic_dispatch.rs"
sabotaged_body, substitutions = re.subn(
r'icmp_ne\(I32, &pcid, "0"\)',
'icmp_ne(I32, &pcid, "-1")',
dropped_fail_closed[path],
count=1,
)
if substitutions != 1:
raise CensusError("generic read PIC fail-closed sabotage fixture missing")
dropped_fail_closed[path] = sabotaged_body
expect_rejected(
"generic read PIC invalid-id fail-closed token silently changed",
lambda: assert_authority_surfaces(dropped_fail_closed),
)

# #8113: the gep-spelled emitted guards. This arm was VACUOUS before —
# it matched only `add(..., "N")` — so plant a keys-offset gep and prove
# it is caught now.
Expand Down
4 changes: 3 additions & 1 deletion scripts/shape_descriptor_census_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"codegen_object_header_size_callsite_multiset": {
"crates/perry-codegen/src/codegen/artifacts.rs|crate::target_layout::object_header_size_bytes(target_triple),": 1,
"crates/perry-codegen/src/expr/element_shape_guard.rs|let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1,
"crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1,
"crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs|let meta_offset = (crate::target_layout::object_header_size_bytes(ctx.target_triple)": 1,
"crates/perry-codegen/src/expr/member_update.rs|let header_skip = crate::target_layout::object_header_size_bytes(": 1,
"crates/perry-codegen/src/expr/property_get.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple)": 3,
"crates/perry-codegen/src/expr/property_get/generic_dispatch.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1,
Expand Down Expand Up @@ -45,7 +47,7 @@
"crates/perry-runtime/src/object/object_ops.rs|keys_array|declaration|pub(crate) use keys_array::{": 1
},
"summary": {
"codegen_object_header_size_sites": 34,
"codegen_object_header_size_sites": 36,
"raw_member_files": 7,
"raw_member_sites": {
"keys_array": 24
Expand Down
Loading