From b84cc76c4a8feb34c2a8ab9a9dfc5fd3a86c38af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 22:09:13 +0200 Subject: [PATCH] fix: restore three lint gates after the merge batch The census update tracks #9317's funnel refactor without weakening the invariant; the mysql2 test change removes real cross-allocation exposure rather than raising the ceiling. --- changelog.d/9322-main-gates-after-batch.md | 23 ++++++++ crates/perry-ext-mysql2/src/lib.rs | 62 +++++++++++++--------- crates/perry/tests/strided_tagged_fill.rs | 2 +- scripts/shape_descriptor_census.py | 9 +++- 4 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 changelog.d/9322-main-gates-after-batch.md diff --git a/changelog.d/9322-main-gates-after-batch.md b/changelog.d/9322-main-gates-after-batch.md new file mode 100644 index 0000000000..8cd4651b89 --- /dev/null +++ b/changelog.d/9322-main-gates-after-batch.md @@ -0,0 +1,23 @@ +### Fixed — three `lint` gates that went red on `main` + +- **`shape_descriptor_census.py` matches the #9317 stamp funnel.** The census + asserts the shape descriptor is published before the `ObjectHeader` ShapeId, + and found that write by its literal spelling. #9317 correctly routed every + post-birth publication through + `stamp_object_shape_id_with_carrier_note`, so the inline write is gone and + the assertion could no longer locate it. The ordering itself is unchanged — + `shape_descriptor_ensure_with_holes` still precedes the stamp in + `publish_object_shape_from`. The census and its own inversion self-test now + name the funnel. + +- **An unused `Path` import** in `strided_tagged_fill.rs` (#9316), which + `cargo check --workspace --all-targets -D warnings` treats as an error. It + is invisible to a plain `cargo build`, which does not compile test targets. + +- **`unrooted_local_shape.py` per-file ceiling** for `perry-ext-mysql2` + (#9319). Its two new tests allocated eight heap values up front and then + pushed them one at a time, leaving every earlier value live across a + `js_array_push` that can move it — the #8217 shape. Each value is now built + inside the iteration that pushes it, and the array is consumed in the + expression that builds it. The file returns to its ceiling of 9 with no + baseline change, and the repository total drops 576 → 567. diff --git a/crates/perry-ext-mysql2/src/lib.rs b/crates/perry-ext-mysql2/src/lib.rs index ce6c5d020a..92d2776622 100644 --- a/crates/perry-ext-mysql2/src/lib.rs +++ b/crates/perry-ext-mysql2/src/lib.rs @@ -1634,32 +1634,42 @@ mod tests { #[test] fn parameter_extraction_preserves_every_supported_value() { unsafe { - let short = perry_runtime::JSValue::try_short_string(b"hi") - .expect("two-byte string uses the SSO representation"); - let long = alloc_string("long-string"); - let date = perry_runtime::date::js_date_new_from_timestamp(1_706_933_106_789.0); - let buffer = perry_ffi::alloc_buffer(&[0, 1, 127, 128, 255]); - - let mut array = js_array_alloc(8); - for value in [ - JsValue::from_bits(short.bits()), - JsValue::from_string_ptr(long.as_raw()), - JsValue::from_int32(42), - JsValue::from_number(3.25), - JsValue::TRUE, - JsValue::NULL, - JsValue::from_bits(date.to_bits()), - JsValue::from_object_ptr(buffer), - ] { - array = js_array_push(array, value); - } - + // Each heap value is built inside the iteration that pushes it. + // The eager form -- allocate all eight, then push them one at a + // time -- leaves every earlier value live across a `js_array_push` + // that can move it, which is the #8217 shape. let expected_date = chrono::NaiveDate::from_ymd_opt(2024, 2, 3) .unwrap() .and_hms_milli_opt(4, 5, 6, 789) .unwrap(); - let actual = extract_params_from_jsvalue(JsValue::from_object_ptr(array)) - .expect("all supported parameter values must marshal"); + // Built and consumed in one expression: no named local holds the + // array across the pushes that can move it. + let actual = extract_params_from_jsvalue(JsValue::from_object_ptr((0..8u32).fold( + js_array_alloc(8), + |acc, slot| { + let value = match slot { + 0 => JsValue::from_bits( + perry_runtime::JSValue::try_short_string(b"hi") + .expect("two-byte string uses the SSO representation") + .bits(), + ), + 1 => JsValue::from_string_ptr(alloc_string("long-string").as_raw()), + 2 => JsValue::from_int32(42), + 3 => JsValue::from_number(3.25), + 4 => JsValue::TRUE, + 5 => JsValue::NULL, + 6 => JsValue::from_bits( + perry_runtime::date::js_date_new_from_timestamp(1_706_933_106_789.0) + .to_bits(), + ), + _ => JsValue::from_object_ptr(perry_ffi::alloc_buffer(&[ + 0, 1, 127, 128, 255, + ])), + }; + js_array_push(acc, value) + }, + ))) + .expect("all supported parameter values must marshal"); assert_eq!( actual, vec![ @@ -1679,8 +1689,9 @@ mod tests { #[test] fn parameter_extraction_rejects_values_instead_of_substituting_null() { unsafe { - let mut undefined_array = js_array_alloc(1); - undefined_array = js_array_push(undefined_array, JsValue::UNDEFINED); + // Nested so the fresh array is never a named local held across the + // push that can move it. + let undefined_array = js_array_push(js_array_alloc(1), JsValue::UNDEFINED); let error = extract_params_from_jsvalue(JsValue::from_object_ptr(undefined_array)) .expect_err("undefined must never become SQL NULL"); assert_eq!(error, "Bind parameter at index 0 is undefined"); @@ -1690,8 +1701,7 @@ mod tests { .expect_err("a non-array params container must be rejected"); assert_eq!(error, "Bind parameters must be an array"); - let mut object_array = js_array_alloc(1); - object_array = js_array_push(object_array, object); + let object_array = js_array_push(js_array_alloc(1), object); let error = extract_params_from_jsvalue(JsValue::from_object_ptr(object_array)) .expect_err("an unsupported parameter must never become SQL NULL"); assert_eq!(error, "Unsupported bind parameter at index 0"); diff --git a/crates/perry/tests/strided_tagged_fill.rs b/crates/perry/tests/strided_tagged_fill.rs index 646bef0213..d4f1ba499f 100644 --- a/crates/perry/tests/strided_tagged_fill.rs +++ b/crates/perry/tests/strided_tagged_fill.rs @@ -12,7 +12,7 @@ //! instead), an out-of-range window (node grows the array), and the plain //! strided-gap behavior, under normal and forced-evacuation runs. -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::{Command, Output}; fn perry_bin() -> PathBuf { diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py index d27fa0d005..a3bcaa6fe2 100644 --- a/scripts/shape_descriptor_census.py +++ b/scripts/shape_descriptor_census.py @@ -398,10 +398,15 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None: "by-id descriptor before reverse accelerator", ) sync = function_body(shapes, "publish_object_shape_from") + # #9317 routed every post-birth ShapeId publication through + # `stamp_object_shape_id_with_carrier_note`, which performs the header + # write and then arms `old_carrier` for a promoted receiver. The header + # write is no longer spelled inline here, so name the funnel: the ordering + # this guards (descriptor authoritative BEFORE the stamp) is unchanged. assert_before( sync, "shape_descriptor_ensure", - "(*obj).parent_class_id = id", + "stamp_object_shape_id_with_carrier_note", "descriptor before ObjectHeader ShapeId", ) # #8113 MINT-THEN-STAMP. With `field_count` deleted, the descriptor is the @@ -786,7 +791,7 @@ def run_sabotage_selftests(sources: dict[str, str], baseline: dict[str, object]) # #9029 tombstones: the lineage publish carries hole_count, so the # mint call in publish_object_shape_from is the _with_holes form. "shape_descriptor_ensure_with_holes(", - "(*obj).parent_class_id = id", + "stamp_object_shape_id_with_carrier_note", ) inverted_publication[path] = inverted_publication[path].replace( publication_body, inverted_body, 1