Skip to content

Commit 90a4c82

Browse files
committed
Fix frame-based memo detecting stale entries from recycled frames
CPython recycles frame objects, so id(frame) can match a stale frame_stack entry from a previous function call. Include frame.f_code identity in the matching key so that recycled frames with different code objects are never confused for an ongoing call. Fixes spurious test_fixed_dim[0-0-True] and test_dimension_wrapped_int_pass failures seen only in CI.
1 parent 7f5e79e commit 90a4c82

File tree

1 file changed

+15
-11
lines changed

1 file changed

+15
-11
lines changed

src/shapix/_memo.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,32 +99,36 @@ def get_memo(_depth: int = 2) -> ShapeMemo:
9999
return ShapeMemo()
100100

101101
frame_id = id(frame)
102+
code = frame.f_code
102103

103104
if not hasattr(_local, "frame_stack"):
104-
_local.frame_stack = [] # list[tuple[int, int, ShapeMemo]]
105+
_local.frame_stack = [] # list[tuple[int, object, int, ShapeMemo]]
105106

106-
# Stack entries: (frame_id, max_lasti, memo)
107-
stack: list[tuple[int, int, ShapeMemo]] = _local.frame_stack
107+
# Stack entries: (frame_id, code_obj, max_lasti, memo)
108+
# We store the code object alongside id(frame) to prevent false matches
109+
# when CPython recycles a frame object at the same address for a different
110+
# function (common in parametrised test loops and tight call sequences).
111+
stack: list[tuple[int, object, int, ShapeMemo]] = _local.frame_stack
108112
lasti: int = frame.f_lasti
109113

110114
# Fast path: same frame as last check (next param in same call)
111-
if stack and stack[-1][0] == frame_id:
112-
_, prev_lasti, prev_memo = stack[-1]
115+
if stack and stack[-1][0] == frame_id and stack[-1][1] is code:
116+
_, _, prev_lasti, prev_memo = stack[-1]
113117
if lasti >= prev_lasti:
114118
# Same call, advancing through params — update max_lasti
115-
stack[-1] = (frame_id, lasti, prev_memo)
119+
stack[-1] = (frame_id, code, lasti, prev_memo)
116120
return prev_memo
117121
# f_lasti went backwards → frame-id was reused (new call to same fn).
118122
# Discard the stale entry and fall through to create a fresh memo.
119123
stack.pop()
120124

121125
# Check deeper in stack (returning to outer call after inner completed)
122126
for i in range(len(stack) - 2, -1, -1):
123-
if stack[i][0] == frame_id:
124-
_, prev_lasti, prev_memo = stack[i]
127+
if stack[i][0] == frame_id and stack[i][1] is code:
128+
_, _, prev_lasti, prev_memo = stack[i]
125129
if lasti >= prev_lasti:
126130
del stack[i + 1 :]
127-
stack[i] = (frame_id, lasti, prev_memo)
131+
stack[i] = (frame_id, code, lasti, prev_memo)
128132
return prev_memo
129133
# Frame-id reuse at a deeper level — discard everything from i onward
130134
del stack[i:]
@@ -137,10 +141,10 @@ def get_memo(_depth: int = 2) -> ShapeMemo:
137141
while f is not None:
138142
active.add(id(f))
139143
f = f.f_back
140-
stack[:] = [(fid, li, m) for fid, li, m in stack if fid in active]
144+
stack[:] = [(fid, c, li, m) for fid, c, li, m in stack if fid in active]
141145

142146
memo = ShapeMemo()
143-
stack.append((frame_id, lasti, memo))
147+
stack.append((frame_id, code, lasti, memo))
144148
return memo
145149

146150

0 commit comments

Comments
 (0)