Summary
Under perry compile, calling Array.prototype methods on an array reached through Map.get(k)!.field returns garbage (or never dispatches) for shift, splice, pop, slice, unshift. push is special-cased and works. length = writes are silently ignored.
The same array, aliased to a single-identifier local first, dispatches the same methods correctly. So the issue is specifically the chained Map.get(...).field.method() call site, not Array.prototype itself.
Found while running @perryts/redis under AOT — drivers and pools that store per-connection FIFO state in module-level Map<id, State> (the pattern recommended by your own AOT guidance for closure-capture safety) hit this on every reply.
Environment
- perry 0.5.772
- macOS 26.4 / Apple M1 Max
Minimal repro
// repro.ts
interface State { pending: number[]; }
const m = new Map<number, State>();
m.set(1, { pending: [10, 20, 30] });
const s = m.get(1)!;
console.log('typeof shift: ', typeof s.pending.shift); // undefined
console.log('typeof splice: ', typeof s.pending.splice); // undefined
console.log('typeof pop: ', typeof s.pending.pop); // undefined
console.log('typeof slice: ', typeof s.pending.slice); // undefined
const x = s.pending.shift();
console.log('shift ret:', x, 'len after:', s.pending.length);
// Actual: shift ret: [object Object] len after: 3 ← length unchanged, return is wrong
// Expected: shift ret: 10 len after: 2
s.pending.length = 0;
console.log('after length=0:', s.pending.length);
// Actual: 3 ← write ignored
// Expected: 0
// Workaround: alias to local first
const arr = s.pending;
console.log('aliased typeof shift:', typeof arr.shift); // still 'undefined'
console.log('aliased shift ret:', arr.shift()); // 10 ← actually works
console.log('back at struct:', s.pending); // [ 20, 30 ] ← mutation visible
$ perry compile repro.ts -o repro && ./repro
typeof shift: undefined
typeof splice: undefined
typeof pop: undefined
typeof slice: undefined
shift ret: [object Object] len after: 3
after length=0: 3
aliased typeof shift: undefined
aliased shift ret: 10
back at struct: [ 20, 30 ]
Behavior matrix
| Call form |
Works? |
arr.shift() where arr is a local number[] |
yes (typeof still 'undefined' but call dispatches) |
this.field.shift() (class instance field) |
yes |
m.get(k)!.field.shift() |
no — returns wrong, no mutation |
m.get(k)!.field.push(x) |
yes (special-cased) |
m.get(k)!.field.length = n |
no — silently ignored |
Impact
Per the AOT-guidance pattern in CLAUDE.md files across the perryts ecosystem ("store state in module-level Map<id, State> and use named module-level handlers"), every driver that follows this pattern has FIFO/queue logic doing s.pending.shift() or s.idle.shift(). Every reply path mutates a Map-stored array, so this isn't an edge case — it's the hot path.
Workaround in @perryts/redis: alias each array to a local before calling mutating methods. This is mechanical but easy to regress on, and length = N truncation has no workaround at all (we replace the array reference instead).
Notes
push is silently special-cased to dispatch correctly even on the broken path. So the symptom is "every other Array mutation is silently broken in a way push isn't", which is hard to spot without a test that reads back length or uses one of the affected methods.
Summary
Under
perry compile, callingArray.prototypemethods on an array reached throughMap.get(k)!.fieldreturns garbage (or never dispatches) forshift,splice,pop,slice,unshift.pushis special-cased and works.length =writes are silently ignored.The same array, aliased to a single-identifier local first, dispatches the same methods correctly. So the issue is specifically the chained
Map.get(...).field.method()call site, notArray.prototypeitself.Found while running
@perryts/redisunder AOT — drivers and pools that store per-connection FIFO state in module-levelMap<id, State>(the pattern recommended by your own AOT guidance for closure-capture safety) hit this on every reply.Environment
Minimal repro
Behavior matrix
arr.shift()wherearris a localnumber[]this.field.shift()(class instance field)m.get(k)!.field.shift()m.get(k)!.field.push(x)m.get(k)!.field.length = nImpact
Per the AOT-guidance pattern in
CLAUDE.mdfiles across the perryts ecosystem ("store state in module-levelMap<id, State>and use named module-level handlers"), every driver that follows this pattern has FIFO/queue logic doings.pending.shift()ors.idle.shift(). Every reply path mutates a Map-stored array, so this isn't an edge case — it's the hot path.Workaround in
@perryts/redis: alias each array to a local before calling mutating methods. This is mechanical but easy to regress on, andlength = Ntruncation has no workaround at all (we replace the array reference instead).Notes
pushis silently special-cased to dispatch correctly even on the broken path. So the symptom is "every other Array mutation is silently broken in a waypushisn't", which is hard to spot without a test that reads back length or uses one of the affected methods.