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
3 changes: 3 additions & 0 deletions changelog.d/7855-array-isarray-reassignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- `Array.isArray` now checks the current runtime value of reassigned locals instead of folding from the binding's initializer. This fixes both false negatives after assigning an array and false positives after assigning a non-array (#7844).
15 changes: 14 additions & 1 deletion crates/perry-codegen/src/expr/array_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,20 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// `Tuple(_)` types short-circuit; anything Union-shaped
// falls through to the runtime.
let v = lower_expr(ctx, o)?;
if let Some(ty) = crate::type_analysis::static_type_of(ctx, o) {
// #7844: a local's type may have been refined from its initializer,
// but reassignment invalidates that proof in both directions. A
// number-initialized local can now hold an array, and an
// array-initialized local can now hold a number. Mirror
// `is_array_expr`/`receiver_class_name`: only fold a local whose
// binding has not been written after initialization.
let static_type_is_still_valid = !matches!(
o.as_ref(),
Expr::LocalGet(id) if ctx.reassigned_locals.contains(id)
);
if let Some(ty) = static_type_is_still_valid
.then_some(crate::type_analysis::static_type_of(ctx, o))
.flatten()
{
if matches!(
ty,
perry_hir::types::Type::Array(_) | perry_hir::types::Type::Tuple(_)
Expand Down
48 changes: 48 additions & 0 deletions crates/perry-codegen/tests/native_proof_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,54 @@ fn for_loop(counter_id: u32, bound: Expr, body: Vec<Stmt>) -> Stmt {
// i8` in the module satisfied.
use native_proof_support::assert_buffer_store_uses_dynamic_fallback;

#[test]
fn array_isarray_reassigned_local_uses_runtime_predicate() {
let body = vec![
Stmt::Let {
id: 1,
name: "number_to_array".to_string(),
ty: Type::Any,
mutable: true,
init: Some(int(0)),
},
Stmt::Expr(Expr::LocalSet(1, Box::new(Expr::Array(vec![int(1)])))),
Stmt::Expr(Expr::ArrayIsArray(Box::new(local(1)))),
Stmt::Let {
id: 2,
name: "array_to_number".to_string(),
ty: Type::Any,
mutable: true,
init: Some(Expr::Array(vec![int(1)])),
},
Stmt::Expr(Expr::LocalSet(2, Box::new(int(0)))),
Stmt::Expr(Expr::ArrayIsArray(Box::new(local(2)))),
Stmt::Let {
id: 3,
name: "unchanged_array".to_string(),
ty: Type::Any,
mutable: false,
init: Some(Expr::Array(vec![int(1)])),
},
Stmt::Expr(Expr::ArrayIsArray(Box::new(local(3)))),
Stmt::Return(Some(int(0))),
];
let ir = String::from_utf8(
compile_module(
&module("array_isarray_reassignment_7844.ts", body),
empty_opts(),
)
.unwrap(),
)
.unwrap();

assert_eq!(
ir.matches("call double @js_array_is_array(").count(),
2,
"both reassigned locals must use the runtime predicate, while the unchanged array may \
retain its compile-time true fold:\n{ir}"
);
}

#[test]
fn artifact_schema_v6_records_consumed_native_facts_for_buffer_region() {
let body = vec![
Expand Down
52 changes: 52 additions & 0 deletions test-files/test_gap_7844_array_isarray_reassigned_local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
let numberToArray: any = 0;
numberToArray = [numberToArray];
console.log(
"number-to-array",
Array.isArray(numberToArray),
numberToArray instanceof Array,
typeof numberToArray,
);

let stringToArray: any = "s";
stringToArray = [stringToArray];
console.log(
"string-to-array",
Array.isArray(stringToArray),
stringToArray instanceof Array,
typeof stringToArray,
);

let nullToArray: any = null;
nullToArray = [nullToArray];
console.log(
"null-to-array",
Array.isArray(nullToArray),
nullToArray instanceof Array,
typeof nullToArray,
);

let arrayToNumber: any = [9];
arrayToNumber = 42;
console.log(
"array-to-number",
Array.isArray(arrayToNumber),
arrayToNumber instanceof Array,
typeof arrayToNumber,
);

let arrayToString: any = [9];
arrayToString = "s";
console.log(
"array-to-string",
Array.isArray(arrayToString),
arrayToString instanceof Array,
typeof arrayToString,
);

const unchanged: any = [1];
console.log(
"unchanged-array",
Array.isArray(unchanged),
unchanged instanceof Array,
typeof unchanged,
);
Loading