Skip to content

codegen: splice()/unshift() drop spread arguments — insert the spread array as one nested element #6870

Description

@proggeramlug

Summary

arr.splice(start, deleteCount, ...items) and arr.unshift(...items) drop the spread flag and insert the spread source as a single nested array element instead of spreading it.

push and concat are correct — only splice and unshift are affected.

Repro

const src = ["X", "Y"];

const a: string[] = ["a", "b"]; a.splice(1, 0, ...src);
const b: string[] = ["a", "b"]; b.unshift(...src);
const c: string[] = ["a", "b"]; c.push(...src);

console.log("splice :", JSON.stringify(a));
console.log("unshift:", JSON.stringify(b));
console.log("push   :", JSON.stringify(c));
node --experimental-strip-types      perry 0.5.1239
splice : ["a","X","Y","b"]           splice : ["a",["X","Y"],"b"]     <-- wrong
unshift: ["X","Y","a","b"]           unshift: [["X","Y"],"a","b"]     <-- wrong
push   : ["a","b","X","Y"]           push   : ["a","b","X","Y"]       ok

Passing the items as explicit arguments (a.splice(1, 0, "X", "Y")) is correct, so the defect is specific to spread.

Cause

crates/perry-hir/src/lower/expr_call/local_array_methods.rs.

The push arm checks the AST spread flag (line ~328):

let any_spread = call.args.iter().any(|a| a.spread.is_some());
if any_spread { /* ArrayPushSpread ... */ }

The splice arm (line ~456) never consults call.args[i].spread — it just collects the lowered args:

let items: Vec<Expr> = args_iter.collect();
return Ok(Ok(Expr::ArraySplice { array_id, start, delete_count, items }));

so ...src arrives as one Expr and is stored as one element. unshift (line ~374) has the same shape.

Impact

Found while compiling the Milo compiler (https://github.com/milo-language/milo), ~32k lines of TS, with Perry. Its LLVM emitter hoists entry-block allocas with:

lines.splice(insertAt, 0, ...hoisted);   // codegen.ts hoistAllocas

Under Perry the nested array stringified through lines.join("\n") using the default , separator, so the compiled Milo compiler emitted malformed LLVM IR and every Milo program failed to build:

error: expected value token
  %notFound.addr = alloca i64,  %base.addr = alloca i64,  %nptr.addr = alloca ptr, ...

This is a silent wrong-answer bug: no diagnostic, and the corruption only surfaces far downstream.

Test coverage

Nothing in test-files/ exercises spread-into-splice (grep -rn "splice([^)]*\.\.\." test-files/ finds only comments). Worth adding alongside the fix, plus an unshift case.

Environment

  • perry 0.5.1239, macOS arm64

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions