Skip to content

Commit 5fb8577

Browse files
Merge branch 'main' into disallow-transparent-statement-bodies
2 parents ee0e39b + 641d967 commit 5fb8577

6 files changed

Lines changed: 103 additions & 6 deletions

File tree

Strata/Languages/Laurel/LaurelToCoreTranslator.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,9 @@ def translateStmt (stmt : StmtExprMd)
432432
| .InstanceCall .. =>
433433
-- Instance method call: havoc the target variable
434434
return [Core.Statement.havoc ident md]
435+
| .Hole _ _ =>
436+
-- Hole RHS: havoc the target (unmodeled call side-effect).
437+
return [Core.Statement.havoc ident md]
435438
| _ =>
436439
let coreExpr ← translateExpr value
437440
return [Core.Statement.set ident coreExpr md]
@@ -506,6 +509,10 @@ def translateStmt (stmt : StmtExprMd)
506509
return [Imperative.Stmt.loop (.det condExpr) decreasingExprCore invExprs bodyStmts md]
507510
| .Exit target =>
508511
return [Imperative.Stmt.exit (some target) md]
512+
| .Hole _ _ =>
513+
-- Hole in statement position: treat as havoc (no-op).
514+
-- This can occur when an unmodeled call's Block is flattened.
515+
return []
509516
| _ =>
510517
-- Expression in statement position: preserve as an unused variable init
511518
exprAsUnusedInit stmt md

Strata/Languages/Python/PythonToLaurel.lean

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
\[ERROR\] procedure '__main__': expected 1 arguments, got 0
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
test_havoc_callee_after_hole_call.py(8, 0): ❓ unknown - expected unknown because xs should be havocked
2+
test_havoc_callee_after_hole_call.py(12, 0): ❓ unknown - expected unknown because xs should be havocked
3+
test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - chained call: receiver not havocked (chained attribute is not a Name)
4+
test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - unrelated variable: nothing should be havocked
5+
test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - composite arg: heap not havocked (out of scope)
6+
test_havoc_callee_after_hole_call.py(30, 0): ❓ unknown - expected unknown because argument locals should be havocked
7+
test_havoc_callee_after_hole_call.py(36, 0): ❓ unknown - assume_assume(1193)_calls_PIn_0
8+
test_havoc_callee_after_hole_call.py(37, 4): ✔️ always true if reached - for-loop over unmodeled iterator should not crash
9+
DETAIL: 4 passed, 0 failed, 4 inconclusive
10+
RESULT: Inconclusive

StrataTest/Languages/Python/expected_laurel/test_missing_models.expected

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
test_missing_models.py(12, 4): ❓ unknown (pass on 2 paths, unknown on 2 paths) - (Origin_test_helper_procedure_Requires)req_name_is_foo
1+
test_missing_models.py(12, 4): ❓ unknown (pass on 2 paths, unknown on 1 path) - (Origin_test_helper_procedure_Requires)req_name_is_foo
22
test_missing_models.py(12, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_str
33
test_missing_models.py(12, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_bar
4-
test_missing_models.py(15, 4): ❓ unknown (pass on 2 paths, unknown on 2 paths) - (Origin_test_helper_procedure_Requires)req_name_is_foo
4+
test_missing_models.py(15, 4): ❓ unknown (pass on 2 paths, unknown on 1 path) - (Origin_test_helper_procedure_Requires)req_name_is_foo
55
test_missing_models.py(15, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_str
66
test_missing_models.py(15, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_bar
77
test_missing_models.py(8, 4): ❓ unknown - init_calls_Any_get_0
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# strata-args: --check-mode bugFinding --check-level full
2+
class MyClass:
3+
def __init__(self, n: int):
4+
self.val : int = n
5+
6+
xs: list[int] = [1, 2]
7+
xs.some_unmodeled_call_1(3)
8+
assert xs == [1, 2], "expected unknown because xs should be havocked"
9+
10+
xs = [1,2]
11+
ys: list[int] = xs.some_unmodeled_call_2()
12+
assert xs == [1, 2], "expected unknown because xs should be havocked"
13+
14+
xs = [1,2]
15+
xs.some_unmodeled_call_3.some_unmodeled_call_4()
16+
assert xs == [1, 2], "chained call: receiver not havocked (chained attribute is not a Name)"
17+
18+
xs = [1,2]
19+
some_function().some_unmodeled_call_5()
20+
assert xs == [1, 2], "unrelated variable: nothing should be havocked"
21+
22+
a : MyClass = MyClass(2)
23+
a.val = 1
24+
some_unmodeled_call_6(a)
25+
assert a.val == 1, "composite arg: heap not havocked (out of scope)"
26+
27+
xs2: list[int] = [1, 2]
28+
ys2: list[int] = []
29+
xs2.unknown_method_that_may_modify_arguments(ys2)
30+
assert ys2 == [], "expected unknown because argument locals should be havocked"
31+
32+
# Regression: unmodeled method call as for-loop iterator.
33+
# The receiver havoc must be lifted out of the iterator expression so
34+
# the block does not end up in expression position inside the assume.
35+
response: dict = {"messages": []}
36+
for msg in response.unmodeled_iter_method():
37+
assert True, "for-loop over unmodeled iterator should not crash"

0 commit comments

Comments
 (0)