@@ -1159,7 +1159,35 @@ partial def translateCall (ctx : TranslationContext)
11591159 | .Attribute range _ attr _ => (attr.val, range)
11601160 | _ => (funcName, .none)
11611161 throwUserError range s! "Unknown method '{ methodName} '"
1162- return mkStmtExprMd .Hole
1162+ -- Havoc the receiver and Any-typed arguments since the unmodeled call
1163+ -- may mutate them and value-typed locals are not reachable via heap havoc.
1164+ -- Note: composite-typed arguments are NOT havoc'd here. If the unmodeled
1165+ -- call mutates a composite's fields, the heap should be havoc'd, but that
1166+ -- requires coordination with HeapParameterization and is out of scope.
1167+ let receiverHavoc := match f with
1168+ | .Attribute _ (.Name _ receiverName _) _ _ =>
1169+ if receiverName.val ∈ ctx.variableTypes.unzip.1 then
1170+ [mkStmtExprMd (StmtExpr.Assign
1171+ [mkStmtExprMd (StmtExpr.Identifier receiverName.val)]
1172+ (mkStmtExprMd .Hole))]
1173+ else []
1174+ | _ => []
1175+ let argHavoc := args.flatMap fun arg =>
1176+ if let .Name _ n _ := arg then
1177+ match ctx.variableTypes.find? (λ v => Prod.fst v == n.val) with
1178+ | some (varName, ty) =>
1179+ if ty == PyLauType.Any then
1180+ [mkStmtExprMd (StmtExpr.Assign
1181+ [mkStmtExprMd (StmtExpr.Identifier varName)]
1182+ (mkStmtExprMd (.Hole false none)))]
1183+ else []
1184+ | _ => []
1185+ else []
1186+ let havocStmts := receiverHavoc ++ argHavoc
1187+ if havocStmts.isEmpty then
1188+ return mkStmtExprMd .Hole
1189+ else
1190+ return mkStmtExprMd (.Block (havocStmts ++ [mkStmtExprMd .Hole]) none)
11631191 -- Step 3: translate the resolved call
11641192 let methodName := match f with
11651193 | .Attribute _ _ attr _ => attr.val
@@ -1740,7 +1768,9 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang
17401768 | _ => return (ctx, exceptionCheck ++ [expr])
17411769 -- Unmodeled call: skip exception checks (no model to check against),
17421770 -- but havoc maybe_except since the call could throw.
1771+ -- Unmodeled call: havoc is now handled in translateCall via Block.
17431772 | .Hole => return (ctx, [expr] ++ holeExceptHavoc)
1773+ | .Block _ _ => return (ctx, [expr] ++ holeExceptHavoc)
17441774 | _ => return (ctx, exceptionCheck ++ [expr])
17451775
17461776 | .Import _ _ | .ImportFrom _ _ _ _ |.Pass _ => return (ctx, [])
@@ -1841,8 +1871,20 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang
18411871 -- and Any_iter_index is only called inside the loop body where that condition is satisfied,
18421872 -- so it is sound to not put it inside AnyMaybeExceptionList
18431873 | .For _ target iter body _orelse _ => do
1844- -- The iterator expression (we abstract it away)
1845- let iterExpr ← translateExpr ctx iter
1874+ -- The iterator expression (we abstract it away).
1875+ -- When the expression contains side-effect statements (e.g. a block with
1876+ -- receiver havoc from an unmodeled method call), bind it to a temporary
1877+ -- variable so the side effects execute once and the clean variable
1878+ -- reference can be used in PIn / Any_len / the while condition.
1879+ -- This mirrors Python semantics where the iterator is evaluated once.
1880+ let iterRaw ← translateExpr ctx iter
1881+ let (iterPreamble, iterExpr) := match iterRaw.val with
1882+ | .Block (_ :: _ :: _) _ =>
1883+ let varName := s! "$for_iter_{ iter.toAst.ann.start.byteIdx} "
1884+ let varDecl := mkStmtExprMd (StmtExpr.LocalVariable varName AnyTy (some iterRaw))
1885+ let varRef := mkStmtExprMd (StmtExpr.Identifier varName)
1886+ ([varDecl], varRef)
1887+ | _ => ([], iterRaw)
18461888 if let .Call _ (.Name _ {val:= "range" ,..} _) _ _ := iter then
18471889 if let .StaticCall "range" _ := iterExpr.val then
18481890 pure ()
@@ -1900,7 +1942,7 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang
19001942 let loopStmt := mkStmtExprMdWithLoc (StmtExpr.While counterLtLen [] none innerBlock) md
19011943 let loopBlock := mkStmtExprMdWithLoc (StmtExpr.Block [loopStmt] (some breakLabel)) md
19021944 let (preamble, _) := getExceptionCheckPreamble ctx iterExpr s! "$for_iter_{ iter.toAst.ann.start.byteIdx} "
1903- return (finalCtx, preamble ++ [counterDecl] ++ [loopBlock])
1945+ return (finalCtx, iterPreamble ++ preamble ++ [counterDecl] ++ [loopBlock])
19041946
19051947 | .Break _ =>
19061948 match ctx.loopBreakLabel with
0 commit comments