From d0d3e117ba1cb1be4b0d3e2066fd996e413a2f54 Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Wed, 22 Apr 2026 13:44:20 -0400 Subject: [PATCH 01/20] test: Add reproduction for list.append() hole not mutating receiver list.append() is translated to a deterministic hole whose return value is discarded. Since ListAny is a value type (not heap-allocated), the hole has no effect on the list variable. The list remains empty, causing downstream truthiness checks to evaluate to false and for-loop bodies to be reported as unreachable. --- .../Python/expected_laurel/test_list_append_hole.expected | 6 ++++++ StrataTest/Languages/Python/tests/test_list_append_hole.py | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected create mode 100644 StrataTest/Languages/Python/tests/test_list_append_hole.py diff --git a/StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected b/StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected new file mode 100644 index 0000000000..37e251a752 --- /dev/null +++ b/StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected @@ -0,0 +1,6 @@ +test_list_append_hole.py(6, 8): ❌ fail (❗path unreachable) - set_x_calls_Any_get_0 +test_list_append_hole.py(6, 8): ❌ fail (❗path unreachable) - assert(137) +test_list_append_hole.py(5, 4): ✔️ always true if reached - ite_cond_calls_Any_to_bool_0 +test_list_append_hole.py(2, 14): ✔️ always true if reached - (main ensures) Return type constraint +DETAIL: 2 passed, 2 failed, 0 inconclusive, 2 unreachable +RESULT: Failures found diff --git a/StrataTest/Languages/Python/tests/test_list_append_hole.py b/StrataTest/Languages/Python/tests/test_list_append_hole.py new file mode 100644 index 0000000000..f598f9adf8 --- /dev/null +++ b/StrataTest/Languages/Python/tests/test_list_append_hole.py @@ -0,0 +1,6 @@ +# strata-args: --check-mode bugFinding --check-level full +def main() -> None: + xs: list[int] = [] + xs.append(1) + if xs: + x: int = xs[0] From ddea3df18cd39fdba457b8418f00b0f2280a9965 Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Wed, 22 Apr 2026 13:59:23 -0400 Subject: [PATCH 02/20] fix: Havoc value-typed locals referenced in unmodeled calls When a call translates to a hole, havoc any value-typed local variables that appear as the receiver or arguments, since the unmodeled call may mutate them and they are not reachable via heap havoc. For method calls (receiver.method(args)), havoc both the receiver and any local variable arguments. For plain function calls (f(args)), havoc any local variable arguments. Without this fix, value-typed locals (ListAny, DictStrAny) retain their last assigned value through holes, causing false unreachability reports when downstream code checks truthiness of the receiver. --- Strata/Languages/Python/PythonToLaurel.lean | 21 ++++++++++++++++++- .../test_list_append_hole.expected | 13 +++++++----- .../Python/tests/test_list_append_hole.py | 4 ++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index a18c013c44..c54d54138b 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -1505,7 +1505,26 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang | _ => return (ctx, exceptionCheck ++ [expr]) -- Unmodeled call: skip exception checks (no model to check against), -- but havoc maybe_except since the call could throw. - | .Hole => return (ctx, [expr] ++ holeExceptHavoc) + -- Also havoc any value-typed locals referenced in the call (receiver + -- and arguments), since the unmodeled call may mutate them and they + -- are not reachable via heap havoc. + | .Hole => + let knownLocals := ctx.variableTypes.unzip.1 + let havocLocal (e : Python.expr SourceRange) : List StmtExprMd := + match e with + | .Name _ n _ => + if n.val ∈ knownLocals then + let target := mkStmtExprMd (StmtExpr.Identifier n.val) + [mkStmtExprMdWithLoc (StmtExpr.Assign [target] (mkStmtExprMd .Hole)) md] + else [] + | _ => [] + let localHavocs := match value with + | .Call _ (.Attribute _ receiver _ _) args _ => + havocLocal receiver ++ args.val.toList.flatMap havocLocal + | .Call _ _ args _ => + args.val.toList.flatMap havocLocal + | _ => [] + return (ctx, [expr] ++ localHavocs ++ holeExceptHavoc) | _ => return (ctx, exceptionCheck ++ [expr]) | .Import _ _ | .ImportFrom _ _ _ _ |.Pass _ => return (ctx, []) diff --git a/StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected b/StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected index 37e251a752..bf0998a838 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_list_append_hole.expected @@ -1,6 +1,9 @@ -test_list_append_hole.py(6, 8): ❌ fail (❗path unreachable) - set_x_calls_Any_get_0 -test_list_append_hole.py(6, 8): ❌ fail (❗path unreachable) - assert(137) -test_list_append_hole.py(5, 4): ✔️ always true if reached - ite_cond_calls_Any_to_bool_0 +test_list_append_hole.py(5, 4): ❓ unknown - ite_cond_calls_Any_to_bool_0 +test_list_append_hole.py(6, 8): ❓ unknown - set_x_calls_Any_get_0 +test_list_append_hole.py(6, 8): ❓ unknown - assert(137) +test_list_append_hole.py(9, 4): ❓ unknown - ite_cond_calls_Any_to_bool_0 +test_list_append_hole.py(10, 8): ❓ unknown - set_y_calls_Any_get_0 +test_list_append_hole.py(10, 8): ❓ unknown - assert(212) test_list_append_hole.py(2, 14): ✔️ always true if reached - (main ensures) Return type constraint -DETAIL: 2 passed, 2 failed, 0 inconclusive, 2 unreachable -RESULT: Failures found +DETAIL: 1 passed, 0 failed, 6 inconclusive +RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_list_append_hole.py b/StrataTest/Languages/Python/tests/test_list_append_hole.py index f598f9adf8..34681c3c72 100644 --- a/StrataTest/Languages/Python/tests/test_list_append_hole.py +++ b/StrataTest/Languages/Python/tests/test_list_append_hole.py @@ -4,3 +4,7 @@ def main() -> None: xs.append(1) if xs: x: int = xs[0] + ys: list[int] = [] + xs.extend(ys) + if ys: + y: int = ys[0] From ee15f2d5381c3616e54879680900cf915b8d520e Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Wed, 22 Apr 2026 14:52:35 -0400 Subject: [PATCH 03/20] fix: Havoc local arguments in unmodeled calls Extend mkHavocStmtsForUnmodeledCall to also havoc non-composite local variables passed as arguments, since the unmodeled call may have side effects on them (e.g., iterating over an iterable argument). Add test case for d.update(other) to test_havoc_callee_after_hole_call. --- .../test_havoc_callee_after_hole_call.expected | 17 +++++++++-------- .../test_unmodeled_method_havoc.expected | 9 --------- .../tests/test_havoc_callee_after_hole_call.py | 5 +++++ .../Python/tests/test_unmodeled_method_havoc.py | 14 -------------- 4 files changed, 14 insertions(+), 31 deletions(-) delete mode 100644 StrataTest/Languages/Python/expected_laurel/test_unmodeled_method_havoc.expected delete mode 100644 StrataTest/Languages/Python/tests/test_unmodeled_method_havoc.py diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 02f9ab0efb..7ab308fb4e 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -1,13 +1,14 @@ -test_havoc_callee_after_hole_call.py(7, 0): ✅ pass - assert_assert(115)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(7, 0): ✔️ always true if reached - assert_assert(115)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(7, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(12, 0): ✅ pass - assert_assert(230)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(12, 0): ✔️ always true if reached - assert_assert(230)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(12, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(17, 0): ✅ pass - assert_assert(363)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(17, 0): ✔️ always true if reached - assert_assert(363)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(17, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(21, 0): ✅ pass - assert_assert(486)_calls_Any_to_bool_0 -test_havoc_callee_after_hole_call.py(21, 0): ✅ pass - expected pass nothing should be havocked -test_havoc_callee_after_hole_call.py(23, 0): ✅ pass - callElimAssert_requires_5 -test_havoc_callee_after_hole_call.py(26, 0): ✅ pass - assert_assert(612)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(21, 0): ✔️ always true if reached - assert_assert(486)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(21, 0): ✔️ always true if reached - expected pass nothing should be havocked +test_havoc_callee_after_hole_call.py(26, 0): ✔️ always true if reached - assert_assert(612)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(26, 0): ❓ unknown - expected unknown because heap should be havocked -DETAIL: 7 passed, 0 failed, 4 inconclusive +test_havoc_callee_after_hole_call.py(31, 0): ✔️ always true if reached - assert_assert(756)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(31, 0): ❓ unknown - expected unknown because argument locals should be havocked +DETAIL: 7 passed, 0 failed, 5 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/expected_laurel/test_unmodeled_method_havoc.expected b/StrataTest/Languages/Python/expected_laurel/test_unmodeled_method_havoc.expected deleted file mode 100644 index d749655832..0000000000 --- a/StrataTest/Languages/Python/expected_laurel/test_unmodeled_method_havoc.expected +++ /dev/null @@ -1,9 +0,0 @@ -test_unmodeled_method_havoc.py(6, 4): ❓ unknown - ite_cond_calls_Any_to_bool_0 -test_unmodeled_method_havoc.py(7, 8): ❓ unknown - set_x_calls_Any_get_0 -test_unmodeled_method_havoc.py(7, 8): ❓ unknown - assert(211) -test_unmodeled_method_havoc.py(13, 4): ❓ unknown - ite_cond_calls_Any_to_bool_0 -test_unmodeled_method_havoc.py(14, 8): ❓ unknown - set_v_calls_Any_get_0 -test_unmodeled_method_havoc.py(14, 8): ❓ unknown - assert(478) -test_unmodeled_method_havoc.py(2, 14): ✔️ always true if reached - (main ensures) Return type constraint -DETAIL: 1 passed, 0 failed, 6 inconclusive -RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index 0dd809c501..a9cb074f3a 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -24,3 +24,8 @@ def __init__(self, n: int): a.val = 1 some_unmodeled_call_6(a) assert a.val == 1, "expected unknown because heap should be havocked" + +d: dict[str, int] = {"a": 1} +other: dict[str, int] = {} +d.update(other) +assert other == {}, "expected unknown because argument locals should be havocked" diff --git a/StrataTest/Languages/Python/tests/test_unmodeled_method_havoc.py b/StrataTest/Languages/Python/tests/test_unmodeled_method_havoc.py deleted file mode 100644 index db67460c2c..0000000000 --- a/StrataTest/Languages/Python/tests/test_unmodeled_method_havoc.py +++ /dev/null @@ -1,14 +0,0 @@ -# strata-args: --check-mode bugFinding --check-level full -def main() -> None: - # Receiver havoc: xs.append() is unmodeled, so xs must not stay empty - xs: list[int] = [] - xs.append(1) - if xs: - x: int = xs[0] - # Argument havoc: d.update(other) is unmodeled and iterates over other, - # which is an observable side effect abstracted away by the hole. - other: dict[str, int] = {} - d: dict[str, int] = {"a": 1} - d.update(other) - if other: - v: int = other["b"] From d0b1a1719de4ee0c43d8251f2f82474ed34b70ad Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Wed, 22 Apr 2026 14:55:28 -0400 Subject: [PATCH 04/20] fix: Use unknown method in argument havoc test Replace d.update(other) with xs2.unknown_method_that_may_modify_arguments(ys) since update doesn't actually mutate its argument. The unknown method makes the motivation clear: the call is a black box that could modify any argument. --- .../test_havoc_callee_after_hole_call.expected | 2 +- .../Python/tests/test_havoc_callee_after_hole_call.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 7ab308fb4e..5f12576043 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -8,7 +8,7 @@ test_havoc_callee_after_hole_call.py(21, 0): ✔️ always true if reached - ass test_havoc_callee_after_hole_call.py(21, 0): ✔️ always true if reached - expected pass nothing should be havocked test_havoc_callee_after_hole_call.py(26, 0): ✔️ always true if reached - assert_assert(612)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(26, 0): ❓ unknown - expected unknown because heap should be havocked -test_havoc_callee_after_hole_call.py(31, 0): ✔️ always true if reached - assert_assert(756)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(31, 0): ✔️ always true if reached - assert_assert(776)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(31, 0): ❓ unknown - expected unknown because argument locals should be havocked DETAIL: 7 passed, 0 failed, 5 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index a9cb074f3a..c5588cfbb9 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -25,7 +25,7 @@ def __init__(self, n: int): some_unmodeled_call_6(a) assert a.val == 1, "expected unknown because heap should be havocked" -d: dict[str, int] = {"a": 1} -other: dict[str, int] = {} -d.update(other) -assert other == {}, "expected unknown because argument locals should be havocked" +xs2: list[int] = [1, 2] +ys: list[int] = [] +xs2.unknown_method_that_may_modify_arguments(ys) +assert ys == [], "expected unknown because argument locals should be havocked" From b4853a352adeb2ece204a5c7b5d73e736aee66a2 Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Wed, 22 Apr 2026 14:58:52 -0400 Subject: [PATCH 05/20] fix: Regenerate expected output in default (deductive) mode The test has no strata-args, so it runs in deductive mode, not bugFinding. The expected file was incorrectly generated with bugFinding mode. --- .../test_havoc_callee_after_hole_call.expected | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 5f12576043..c1ee505783 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -1,14 +1,15 @@ -test_havoc_callee_after_hole_call.py(7, 0): ✔️ always true if reached - assert_assert(115)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(7, 0): ✅ pass - assert_assert(115)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(7, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(12, 0): ✔️ always true if reached - assert_assert(230)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(12, 0): ✅ pass - assert_assert(230)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(12, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(17, 0): ✔️ always true if reached - assert_assert(363)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(17, 0): ✅ pass - assert_assert(363)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(17, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(21, 0): ✔️ always true if reached - assert_assert(486)_calls_Any_to_bool_0 -test_havoc_callee_after_hole_call.py(21, 0): ✔️ always true if reached - expected pass nothing should be havocked -test_havoc_callee_after_hole_call.py(26, 0): ✔️ always true if reached - assert_assert(612)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(21, 0): ✅ pass - assert_assert(486)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(21, 0): ✅ pass - expected pass nothing should be havocked +test_havoc_callee_after_hole_call.py(23, 0): ✅ pass - callElimAssert_requires_5 +test_havoc_callee_after_hole_call.py(26, 0): ✅ pass - assert_assert(612)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(26, 0): ❓ unknown - expected unknown because heap should be havocked -test_havoc_callee_after_hole_call.py(31, 0): ✔️ always true if reached - assert_assert(776)_calls_Any_to_bool_0 +test_havoc_callee_after_hole_call.py(31, 0): ✅ pass - assert_assert(776)_calls_Any_to_bool_0 test_havoc_callee_after_hole_call.py(31, 0): ❓ unknown - expected unknown because argument locals should be havocked -DETAIL: 7 passed, 0 failed, 5 inconclusive +DETAIL: 8 passed, 0 failed, 5 inconclusive RESULT: Inconclusive From 13b307c1208feca7e21506ee14dd81a93bf0e368 Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Mon, 27 Apr 2026 17:09:38 -0400 Subject: [PATCH 06/20] fix: Only havoc Any-typed arguments in unmodeled calls Primitive types (int, bool, str) are pass-by-value in Python and cannot be mutated by a callee. Only havoc arguments whose tracked type is Any, since those could be mutable containers (lists, dicts) in the value-type model. Composite types are already covered by the heap havoc. --- Strata/Languages/Python/PythonToLaurel.lean | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index e989f6c9af..0db0011e7a 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -1131,13 +1131,15 @@ private def mkHavocStmtsForUnmodeledCall (ctx : TranslationContext) let heapHavoc := if !involveHeap then [] else [mkStmtExprMdWithLoc (StmtExpr.Assign [freeVar "$heap"] (mkStmtExprMd (.Hole false none))) md] -- Havoc local arguments: the unmodeled call may have side effects on - -- its arguments (e.g., iterating over an iterable argument). + -- arguments that are Any-typed (e.g., mutable containers like lists or dicts). + -- Primitive types (int, bool, str) are pass-by-value and cannot be mutated. let argHavoc := inputExprs.flatMap fun inputExpr => if let .Name _ n _ := inputExpr then match ctx.variableTypes.find? (λ v => Prod.fst v == n.val) with | some (varName, ty) => - if isCompositeType ctx ty then [] - else [mkStmtExprMdWithLoc (StmtExpr.Assign [freeVar varName] (mkStmtExprMd (.Hole false none))) md] + if ty == PyLauType.Any then + [mkStmtExprMdWithLoc (StmtExpr.Assign [freeVar varName] (mkStmtExprMd (.Hole false none))) md] + else [] | _ => [] else [] [mkStmtExprMd $ .Block (holeExceptHavoc ++ calleeHavoc ++ argHavoc ++ heapHavoc) none] From c037d3c9e7a902e868feaacae196d049d4106b91 Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Tue, 28 Apr 2026 15:43:34 -0400 Subject: [PATCH 07/20] docs: Document that composite heap havoc is out of scope Add comment noting that composite-typed arguments are not havoc'd by this change. Havocing the heap for unmodeled calls that receive composites requires coordination with HeapParameterization. --- Strata/Languages/Python/PythonToLaurel.lean | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index 710c9158d4..f71d90c856 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -1676,6 +1676,9 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang -- but havoc maybe_except since the call could throw. -- Also havoc the receiver and Any-typed arguments since the unmodeled -- call may mutate them and value-typed locals are not reachable via heap havoc. + -- Note: composite-typed arguments are NOT havoc'd here. If the unmodeled + -- call mutates a composite's fields, the heap should be havoc'd, but that + -- requires coordination with HeapParameterization and is out of scope. | .Hole => let receiverHavoc := match value with | .Call _ (.Attribute _ (.Name _ receiverName _) _ _) _ _ => From 538f7ef79eadbde6c3b72aa71d45d9ab737e0e7e Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Tue, 28 Apr 2026 18:06:24 -0400 Subject: [PATCH 08/20] fix: Update havoc test to use truthiness checks in bugFinding mode Rewrite test to use if-truthiness checks instead of equality assertions, which are more sensitive to the havoc. Use bugFinding mode to detect unreachable paths. Verified havoc is emitted in Laurel intermediate: xs := after append, ys := <> after unknown_method. --- ...test_havoc_callee_after_hole_call.expected | 13 +++----- .../test_havoc_callee_after_hole_call.py | 33 +++++-------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 4ab3d9e0a2..50e733f105 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -1,9 +1,6 @@ -test_havoc_callee_after_hole_call.py(7, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(12, 0): ✅ pass - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(17, 0): ✅ pass - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(21, 0): ✅ pass - expected pass nothing should be havocked -test_havoc_callee_after_hole_call.py(23, 0): ✅ pass - (MyClass@__init__ requires) Type constraint of n -test_havoc_callee_after_hole_call.py(26, 0): ✅ pass - expected unknown because heap should be havocked -test_havoc_callee_after_hole_call.py(31, 0): ❓ unknown - expected unknown because argument locals should be havocked -DETAIL: 5 passed, 0 failed, 2 inconclusive +test_havoc_callee_after_hole_call.py(9, 4): ❓ unknown - set_x_calls_Any_get_0 +test_havoc_callee_after_hole_call.py(9, 4): ❓ unknown - assert(176) +test_havoc_callee_after_hole_call.py(14, 4): ❓ unknown - set_y_calls_Any_get_0 +test_havoc_callee_after_hole_call.py(14, 4): ❓ unknown - assert(270) +DETAIL: 0 passed, 0 failed, 4 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index c5588cfbb9..b3e29c4ddc 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -1,31 +1,14 @@ +# strata-args: --check-mode bugFinding --check-level full class MyClass: def __init__(self, n: int): self.val : int = n -xs = [1, 2] -xs.some_unmodeled_call_1(3) -assert xs == [1, 2], "expected unknown because xs should be havocked" +xs: list[int] = [] +xs.append(1) +if xs: + x: int = xs[0] - -xs = [1,2] -ys = xs.some_unmodeled_call_2() -assert xs == [1, 2], "expected unknown because xs should be havocked" - - -xs = [1,2] -xs.some_unmodeled_call_3.some_unmodeled_call_4() -assert xs == [1, 2], "expected unknown because xs should be havocked" - -xs = [1,2] -some_function().some_unmodeled_call_5() -assert xs == [1, 2], "expected pass nothing should be havocked" - -a : MyClass = MyClass(2) -a.val = 1 -some_unmodeled_call_6(a) -assert a.val == 1, "expected unknown because heap should be havocked" - -xs2: list[int] = [1, 2] ys: list[int] = [] -xs2.unknown_method_that_may_modify_arguments(ys) -assert ys == [], "expected unknown because argument locals should be havocked" +xs.unknown_method_that_may_modify_arguments(ys) +if ys: + y: int = ys[0] From 7953cb409029311e445536fc41093225c73eafd6 Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Tue, 28 Apr 2026 18:13:32 -0400 Subject: [PATCH 09/20] fix: Restore assertion-based havoc test Restore the original assertion-based test cases from PR 978. The test now accurately reflects what PR 1019 covers: - Line 8: standalone method call receiver havoc (unknown) - Line 30: argument havoc (unknown) - Lines 12, 16: assignment and chained calls (not yet covered) - Lines 20, 25: negative cases (correctly pass) --- ...test_havoc_callee_after_hole_call.expected | 12 ++++--- .../test_havoc_callee_after_hole_call.py | 34 ++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 50e733f105..4d91669de9 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -1,6 +1,8 @@ -test_havoc_callee_after_hole_call.py(9, 4): ❓ unknown - set_x_calls_Any_get_0 -test_havoc_callee_after_hole_call.py(9, 4): ❓ unknown - assert(176) -test_havoc_callee_after_hole_call.py(14, 4): ❓ unknown - set_y_calls_Any_get_0 -test_havoc_callee_after_hole_call.py(14, 4): ❓ unknown - assert(270) -DETAIL: 0 passed, 0 failed, 4 inconclusive +test_havoc_callee_after_hole_call.py(8, 0): ❓ unknown - expected unknown because xs should be havocked +test_havoc_callee_after_hole_call.py(12, 0): ✔️ always true if reached - expected unknown because xs should be havocked +test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - expected unknown because xs should be havocked +test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - expected pass nothing should be havocked +test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - expected unknown because heap should be havocked +test_havoc_callee_after_hole_call.py(30, 0): ❓ unknown - expected unknown because argument locals should be havocked +DETAIL: 4 passed, 0 failed, 2 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index b3e29c4ddc..710b71ee75 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -3,12 +3,28 @@ class MyClass: def __init__(self, n: int): self.val : int = n -xs: list[int] = [] -xs.append(1) -if xs: - x: int = xs[0] - -ys: list[int] = [] -xs.unknown_method_that_may_modify_arguments(ys) -if ys: - y: int = ys[0] +xs: list[int] = [1, 2] +xs.some_unmodeled_call_1(3) +assert xs == [1, 2], "expected unknown because xs should be havocked" + +xs = [1,2] +ys: list[int] = xs.some_unmodeled_call_2() +assert xs == [1, 2], "expected unknown because xs should be havocked" + +xs = [1,2] +xs.some_unmodeled_call_3.some_unmodeled_call_4() +assert xs == [1, 2], "expected unknown because xs should be havocked" + +xs = [1,2] +some_function().some_unmodeled_call_5() +assert xs == [1, 2], "expected pass nothing should be havocked" + +a : MyClass = MyClass(2) +a.val = 1 +some_unmodeled_call_6(a) +assert a.val == 1, "expected unknown because heap should be havocked" + +xs2: list[int] = [1, 2] +ys2: list[int] = [] +xs2.unknown_method_that_may_modify_arguments(ys2) +assert ys2 == [], "expected unknown because argument locals should be havocked" From 6558ed9362c7b66409659f218bb7b0157308e935 Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Tue, 28 Apr 2026 18:23:34 -0400 Subject: [PATCH 10/20] fix: Move havoc from Expr handler to translateCall Move receiver and argument havoc into translateCall where the Hole is first produced. This ensures havoc works for all call sites: standalone expressions, assignments, and chained calls. The havoc assignments are wrapped in a Block with the Hole as the last expression (the block's return value). Previously the havoc was only in the Expr handler, which missed the assignment case (ys = xs.method()) entirely. Update test_missing_models expected output for changed path counts. --- Strata/Languages/Python/PythonToLaurel.lean | 59 ++++++++++--------- ...test_havoc_callee_after_hole_call.expected | 4 +- .../test_missing_models.expected | 4 +- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index f71d90c856..07f9c28918 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -1133,7 +1133,35 @@ partial def translateCall (ctx : TranslationContext) | .Attribute range _ attr _ => (attr.val, range) | _ => (funcName, .none) throwUserError range s!"Unknown method '{methodName}'" - return mkStmtExprMd .Hole + -- Havoc the receiver and Any-typed arguments since the unmodeled call + -- may mutate them and value-typed locals are not reachable via heap havoc. + -- Note: composite-typed arguments are NOT havoc'd here. If the unmodeled + -- call mutates a composite's fields, the heap should be havoc'd, but that + -- requires coordination with HeapParameterization and is out of scope. + let receiverHavoc := match f with + | .Attribute _ (.Name _ receiverName _) _ _ => + if receiverName.val ∈ ctx.variableTypes.unzip.1 then + [mkStmtExprMd (StmtExpr.Assign + [mkStmtExprMd (StmtExpr.Identifier receiverName.val)] + (mkStmtExprMd .Hole))] + else [] + | _ => [] + let argHavoc := args.flatMap fun arg => + if let .Name _ n _ := arg then + match ctx.variableTypes.find? (λ v => Prod.fst v == n.val) with + | some (varName, ty) => + if ty == PyLauType.Any then + [mkStmtExprMd (StmtExpr.Assign + [mkStmtExprMd (StmtExpr.Identifier varName)] + (mkStmtExprMd (.Hole false none)))] + else [] + | _ => [] + else [] + let havocStmts := receiverHavoc ++ argHavoc + if havocStmts.isEmpty then + return mkStmtExprMd .Hole + else + return mkStmtExprMd (.Block (havocStmts ++ [mkStmtExprMd .Hole]) none) -- Step 3: translate the resolved call let methodName := match f with | .Attribute _ _ attr _ => attr.val @@ -1674,32 +1702,9 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang | _ => return (ctx, exceptionCheck ++ [expr]) -- Unmodeled call: skip exception checks (no model to check against), -- but havoc maybe_except since the call could throw. - -- Also havoc the receiver and Any-typed arguments since the unmodeled - -- call may mutate them and value-typed locals are not reachable via heap havoc. - -- Note: composite-typed arguments are NOT havoc'd here. If the unmodeled - -- call mutates a composite's fields, the heap should be havoc'd, but that - -- requires coordination with HeapParameterization and is out of scope. - | .Hole => - let receiverHavoc := match value with - | .Call _ (.Attribute _ (.Name _ receiverName _) _ _) _ _ => - if receiverName.val ∈ ctx.variableTypes.unzip.1 then - let target := mkStmtExprMd (StmtExpr.Identifier receiverName.val) - [mkStmtExprMdWithLoc (StmtExpr.Assign [target] (mkStmtExprMd .Hole)) md] - else [] - | _ => [] - let argHavoc := match value with - | .Call _ _ args _ => - args.val.toList.flatMap fun arg => - if let .Name _ n _ := arg then - match ctx.variableTypes.find? (λ v => Prod.fst v == n.val) with - | some (varName, ty) => - if ty == PyLauType.Any then - [mkStmtExprMdWithLoc (StmtExpr.Assign [freeVar varName] (mkStmtExprMd (.Hole false none))) md] - else [] - | _ => [] - else [] - | _ => [] - return (ctx, [expr] ++ receiverHavoc ++ argHavoc ++ holeExceptHavoc) + -- Unmodeled call: havoc is now handled in translateCall via Block. + | .Hole => return (ctx, [expr] ++ holeExceptHavoc) + | .Block _ _ => return (ctx, [expr] ++ holeExceptHavoc) | _ => return (ctx, exceptionCheck ++ [expr]) | .Import _ _ | .ImportFrom _ _ _ _ |.Pass _ => return (ctx, []) diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 4d91669de9..47f4add193 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -1,8 +1,8 @@ test_havoc_callee_after_hole_call.py(8, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(12, 0): ✔️ always true if reached - expected unknown because xs should be havocked +test_havoc_callee_after_hole_call.py(12, 0): ❓ unknown - expected unknown because xs should be havocked test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - expected unknown because xs should be havocked test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - expected pass nothing should be havocked test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - expected unknown because heap should be havocked test_havoc_callee_after_hole_call.py(30, 0): ❓ unknown - expected unknown because argument locals should be havocked -DETAIL: 4 passed, 0 failed, 2 inconclusive +DETAIL: 3 passed, 0 failed, 3 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/expected_laurel/test_missing_models.expected b/StrataTest/Languages/Python/expected_laurel/test_missing_models.expected index 4556fc901f..e0c6cdcdc4 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_missing_models.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_missing_models.expected @@ -1,7 +1,7 @@ -test_missing_models.py(12, 4): ❓ unknown (pass on 2 paths, unknown on 2 paths) - (Origin_test_helper_procedure_Requires)req_name_is_foo +test_missing_models.py(12, 4): ❓ unknown (pass on 2 paths, unknown on 1 path) - (Origin_test_helper_procedure_Requires)req_name_is_foo test_missing_models.py(12, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_str test_missing_models.py(12, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_bar -test_missing_models.py(15, 4): ❓ unknown (pass on 2 paths, unknown on 2 paths) - (Origin_test_helper_procedure_Requires)req_name_is_foo +test_missing_models.py(15, 4): ❓ unknown (pass on 2 paths, unknown on 1 path) - (Origin_test_helper_procedure_Requires)req_name_is_foo test_missing_models.py(15, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_str test_missing_models.py(15, 4): ✅ pass - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_bar test_missing_models.py(8, 4): ❓ unknown - init_calls_Any_get_0 From f54deb78fe3f35c4043215e250c62b0a4197ba2a Mon Sep 17 00:00:00 2001 From: Siva Somayyajula Date: Tue, 28 Apr 2026 18:29:19 -0400 Subject: [PATCH 11/20] fix: Correct test assertion messages to match actual behavior Fix misleading assertion messages for cases not covered by this PR: - Chained calls: receiver is not a bare Name, so not havocked - Composite args: heap havoc is out of scope --- .../test_havoc_callee_after_hole_call.expected | 6 +++--- .../Python/tests/test_havoc_callee_after_hole_call.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 47f4add193..9ffa2fc20a 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -1,8 +1,8 @@ test_havoc_callee_after_hole_call.py(8, 0): ❓ unknown - expected unknown because xs should be havocked test_havoc_callee_after_hole_call.py(12, 0): ❓ unknown - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - expected unknown because xs should be havocked -test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - expected pass nothing should be havocked -test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - expected unknown because heap should be havocked +test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - chained call: receiver not havocked (chained attribute is not a Name) +test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - unrelated variable: nothing should be havocked +test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - composite arg: heap not havocked (out of scope) test_havoc_callee_after_hole_call.py(30, 0): ❓ unknown - expected unknown because argument locals should be havocked DETAIL: 3 passed, 0 failed, 3 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index 710b71ee75..20623aa194 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -13,16 +13,16 @@ def __init__(self, n: int): xs = [1,2] xs.some_unmodeled_call_3.some_unmodeled_call_4() -assert xs == [1, 2], "expected unknown because xs should be havocked" +assert xs == [1, 2], "chained call: receiver not havocked (chained attribute is not a Name)" xs = [1,2] some_function().some_unmodeled_call_5() -assert xs == [1, 2], "expected pass nothing should be havocked" +assert xs == [1, 2], "unrelated variable: nothing should be havocked" a : MyClass = MyClass(2) a.val = 1 some_unmodeled_call_6(a) -assert a.val == 1, "expected unknown because heap should be havocked" +assert a.val == 1, "composite arg: heap not havocked (out of scope)" xs2: list[int] = [1, 2] ys2: list[int] = [] From 09cf4be5c36baee5ece216243f70c4f1e4ea66a1 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Wed, 29 Apr 2026 10:58:16 +0000 Subject: [PATCH 12/20] fix: Add expected_interpret file for test_havoc_callee_after_hole_call The pyInterpret test fails because the interpreter does not support classes with __init__ at top level. This is a pre-existing limitation (same pattern as test_class_decl and other class-based tests). --- .../test_havoc_callee_after_hole_call.expected | 1 + 1 file changed, 1 insertion(+) create mode 100644 StrataTest/Languages/Python/expected_interpret/test_havoc_callee_after_hole_call.expected diff --git a/StrataTest/Languages/Python/expected_interpret/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_interpret/test_havoc_callee_after_hole_call.expected new file mode 100644 index 0000000000..2410119fac --- /dev/null +++ b/StrataTest/Languages/Python/expected_interpret/test_havoc_callee_after_hole_call.expected @@ -0,0 +1 @@ +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 From 5ab9f5a70af6e57dcd8cbcfd30f6ae71e57b5fc7 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Wed, 29 Apr 2026 11:50:28 +0000 Subject: [PATCH 13/20] fix: Handle Hole in statement position in LaurelToCoreTranslator When translateCall wraps havoc statements in a Block, the trailing Hole inside the Block can end up in statement position during downstream processing. The LaurelToCoreTranslator's catch-all case tried to translate it as an expression via exprAsUnusedInit, which called translateExpr and hit the 'holes should have been eliminated' error. Add an explicit Hole case to translateStmt that treats it as a no-op (empty statement list), since a Hole in statement position has no observable effect. --- Strata/Languages/Laurel/LaurelToCoreTranslator.lean | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean index f3af1f95b6..c89521ce8c 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean @@ -504,6 +504,10 @@ def translateStmt (stmt : StmtExprMd) return [Imperative.Stmt.loop (.det condExpr) decreasingExprCore invExprs bodyStmts md] | .Exit target => return [Imperative.Stmt.exit (some target) md] + | .Hole _ _ => + -- Hole in statement position: treat as havoc (no-op). + -- This can occur when an unmodeled call's Block is flattened. + return [] | _ => -- Expression in statement position: preserve as an unused variable init exprAsUnusedInit stmt md From 9d7402ed88aec3808e7be34fd6d7fac26f278413 Mon Sep 17 00:00:00 2001 From: Juneyoung Lee <136006969+aqjune-aws@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:25:36 -0500 Subject: [PATCH 14/20] Add Strata internal benchmarks Github Action (#1070) (Moves https://github.com/strata-org/Strata/pull/1060 to a branch of this upstream repo) This pull request adds a Codebuild setup connected to Github Action for Strata internal benchmark setup. Only authorized people can access to the S3 bucket and download it (hence external users outside Amazon cannot get any info about the internal benchmarks). The Github Action log only shows that the Codebuild is running or finished. ~~Once successful, the CI check results will look like that of https://github.com/aqjune-aws/Strata/pull/3~~ This pull request's CI is successfully checking the internal benchmarks. You can see `Build / Run internal benchmarks of Strata (pull_request)` from Github workflow, and it will have printed the S3 log URL. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06a1cb8cc0..afb376b563 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,3 +321,48 @@ jobs: permissions: contents: read uses: ./.github/workflows/cbmc.yml + + strata-benchmarks: + name: Run internal benchmarks of Strata + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: arn:aws:iam::${{secrets.AWS_BENCHMARK_ACCOUNT}}:role/github-actions-codebuild-role + aws-region: us-east-2 + + - name: Trigger CodeBuild and wait + shell: bash + run: | + BUILD_ID=$(aws codebuild start-build \ + --project-name strata-benchmarks \ + --source-type-override GITHUB \ + --source-location-override https://github.com/strata-org/Strata.git \ + --query 'build.id' --output text \ + --region us-east-2) + echo "Build started: $BUILD_ID" + + LOG_KEY="logs/${BUILD_ID}.log" + echo "[View build log in S3](https://s3.console.aws.amazon.com/s3/object/strata-internal-benchmarks-logs?prefix=${LOG_KEY})" >> $GITHUB_STEP_SUMMARY + + while true; do + STATUS=$(aws codebuild batch-get-builds \ + --ids "$BUILD_ID" \ + --query 'builds[0].buildStatus' --output text \ + --region us-east-2) + echo "Current status: [$STATUS]" + case "$STATUS" in + SUCCEEDED) break;; + FAILED|FAULT|TIMED_OUT|STOPPED) echo "Build failed: $STATUS" ; break ;; + IN_PROGRESS) sleep 30 ;; + *) echo "Unexpected status: $STATUS"; sleep 10 ; break ;; + esac + done + + echo "View build log in S3: https://s3.console.aws.amazon.com/s3/object/strata-internal-benchmarks-logs?prefix=${LOG_KEY}" + + test "$STATUS" = "SUCCEEDED" From a546083a2606b37177526b7b7de7d671700fe85c Mon Sep 17 00:00:00 2001 From: Joe Hendrix Date: Tue, 28 Apr 2026 15:45:31 -0700 Subject: [PATCH 15/20] PySpec pipeline: fix type mismatches, add type assertions, and clean up translation (#1048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Overview Overhaul the PySpec-to-Laurel translation pipeline to fix type incompatibilities between pyspec and Python-source code paths, generate accurate type assertions in procedure bodies, and clean up dead/redundant code. These changes improve verification accuracy for pyspec-annotated libraries by ensuring type constraints are generated consistently with the Laurel runtime type system. ## Issues Fixed **Cross-module type references resolve incorrectly.** `specArgToFuncDeclArg` in PySpecPipeline.lean derived Laurel type names using the function's module prefix rather than the type's own `PythonIdent.pythonModule`. A pyspec function in module `foo` referencing a type from module `bar.baz` would produce `foo_MyClass` instead of `bar_baz_MyClass`. Fixed by introducing `PythonIdent.toLaurelName` which uses the type's own module, and `specArgLaurelType` which mirrors `specTypeToLaurelType`'s logic. **Redundant call-site type preconditions.** PySpec procedure bodies already generate `assert Any..isfrom_int(x)` etc. via `buildSpecBody`, but `specArgToFuncDeclArg` also populated `typeTesters`, causing PythonToLaurel to emit duplicate type constraints at each call site. Fixed by setting `typeTesters := #[]` for pyspec args and removing the now-unused `specArgTypeTesters` function. **`typeTester?` only handled singleton types.** The old `typeTester?` used `SpecType.asIdent` which requires exactly one atom, silently producing no assertion for union types like `Union[int, str]`. Replaced with `typeAssertion?` which iterates all atoms and builds a disjunction (`isfrom_int(v) || isfrom_str(v)`). **Missing type testers for common builtins.** `toTypeTester?` only covered `int`, `str`, `bool`, `float`, and `None`. Added mappings for `bytes` → `Any..isfrom_bytes`, `List`/`Sequence` → `Any..isfrom_ListAny`, `Dict`/`Mapping` → `Any..isfrom_DictStrAny`, and `Exception` → `Any..isexception`, matching the `Any` datatype constructors in the Laurel Python prelude. **No assertions for literal types.** `Literal[42]` and `Literal["hello"]` produced no type assertions. `atomAssertion?` now generates `isfrom_int(v) && as_int!(v) == 42` for int literals and `isfrom_str(v) && as_string!(v) == "hello"` for string literals. TypedDict atoms are approximated as `isfrom_DictStrAny` with a warning. **Unsupported types in unions silently dropped.** When a union contains atoms without testers (e.g., user-defined classes in `Union[None, MyClass]`), the old code silently skipped them. `typeAssertion?` now warns via `unsupportedUnion` for ident atoms without testers in unions and for TypedDict atoms. Singleton composite types (user-defined classes used alone) are not warned — they are expected to have no tester. **Redundant `!isfrom_None` checks for typed parameters.** `buildSpecBody` had separate loops for type assertions and required-param checks. A required `int` parameter would get both `assert isfrom_int(x)` and `assert !isfrom_None(x)`, but the first already implies the second. Combined into a single loop: typed params get only the type assertion; untyped required params still get the `!isfrom_None` check. **`PyArgInfo.tys` was a raw `List String` coupling pyspec and Python paths.** Replaced with structured `laurelType : HighTypeMd` and `typeTesters : Array String` fields. Similarly, `PythonFunctionDecl.ret` changed from `Option (List String × MetaData)` to `Option PyRetInfo`. All construction and consumption sites updated. ## Upstream Compatibility Fixes Three targeted fixes address regressions exposed by the more complete pipeline translation, all caused by pre-existing issues in upstream data or earlier translation stages: **Skip return-type `assume` for NoneType (ToLaurel.lean).** Generated Python stubs declare `-> None` for API methods that actually return response dicts. With transparent procedure bodies, `buildSpecBody` now generates `assume Any..isfrom_None(result)`, making all downstream field access on the return value unreachable — a false positive. The fix skips the return-type `assume` when the declared type is `NoneType`. This is a temporary workaround pending a stub-generator fix to emit correct return types. Resolves benchmark regressions. **Return Hole for module attribute access (PythonToLaurel.lean).** `sys.argv`, `os.path`, and similar module-level attribute accesses generated `FieldSelect (Identifier "sys") "argv"`, but imported modules are not declared as Laurel variables. The fix returns `Hole` when the object expression is a known package (via `isPackage`), matching the existing handling for unmodeled package function calls. Resolves benchmark regression. **Drop bare `raise` instead of emitting untyped Hole (PythonToLaurel.lean).** A bare `raise` inside an exception handler produced a Hole whose return type could not be inferred, leaving it as `Unknown` and failing the Laurel-to-Core translator. The fix emits no statements for `raise`, which is safe under the current exception model where propagation is tracked via `maybe_except`/`isError` checks. This is a placeholder pending proper exception modeling. Resolves benchmark regression. ## Additional Details - `toTypeTester?` refactored from an if-else chain to a `Std.HashMap` lookup (`typeTestersMap`). - `SpecType.mk` made private; callers use smart constructors (`ident`, `noneType`, `intLiteral`, `stringLiteral`, `typedDict`, `unionArray`). `sizeOf_atom_lt_of_mem` theorem added for termination proofs. - Duplicate-element bug fixed in `unionAux` (equal elements were double-counted). - Dead `pythonToLaurel` wrapper removed; `pythonToLaurel'` renamed to `pythonToLaurel`. - Dead `PythonIdent.toPyLauType?` removed (replaced by `toTypeTester?` / `specArgLaurelType`). - Dead `emptyType` warning kind and unreachable code branch removed. - Unified `transExpr` replaces ~10 separate expression translators (`extractSubject`, `transAssertExpr`, `transCondition`, `transMessageExpr`, `inferExprType`, `extractIntBound`, `extractFloatBound`, `collectEnumValues`, `isIntType`, `isFloatType`, `lookupTypedDictField`, `extractElementType`, `extractDictKeyValueTypes`) with a single recursive function that returns `(SpecExpr × SpecType)`. This enables type-directed translation (e.g., choosing int vs float comparison) and eliminates duplicate pattern matching. - `SpecExpr.len` renamed to `SpecExpr.stringLen` for clarity. - `extractFunctionSignatures` uses `Array` instead of `List` accumulator. - `runTest` in ToLaurelTest.lean now prints warnings before procedures so `#guard_msgs` can verify them. - `SpecAssertionContext.kwargs` simplified from separate `kwargsParamName`/`kwargsType` fields to a single `Option (String × SpecType)`. - `assumeCondition` simplified to always take a condition (no `Option` wrapper). - `PySpecMClass.runNoWarn` added to check that an action produces no errors or warnings. ## Test Changes - **ToLaurelTest.lean**: All `SpecAtomType` references replaced with `SpecType` smart constructors. Body structure tests updated to reflect combined type/required-param assertions (typed params no longer have `!isfrom_None`). Expected output updated for new warnings on `bytearray`, `complex`, user-defined types in unions, and TypedDict approximation. - **PySpecArgTypeTest.lean**: Updated to verify `typeTesters` is empty (body assertions make call-site ones redundant). Added end-to-end test using `formatProcedure` to verify full procedure body through DDM round-trip, confirming body contains `assert Any..isfrom_int(x)`, `assert Any..isfrom_str(y)`, and `assume Any..isfrom_float(result)`. - **DeclsTest.lean**: Migrated to use `unionArray` for dedup tests. - **SpecsTest.lean**: Minor import path update. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the MIT license. --------- Co-authored-by: Claude Opus 4.6 --- Strata/Languages/Python/PySpecPipeline.lean | 64 +- Strata/Languages/Python/PythonToLaurel.lean | 206 +++--- Strata/Languages/Python/Specs.lean | 655 ++++++++---------- Strata/Languages/Python/Specs/DDM.lean | 20 +- Strata/Languages/Python/Specs/Decls.lean | 181 ++++- Strata/Languages/Python/Specs/Error.lean | 1 - Strata/Languages/Python/Specs/ToLaurel.lean | 422 +++++------ .../Languages/Python/PySpecArgTypeTest.lean | 64 +- .../Languages/Python/Specs/DeclsTest.lean | 28 +- StrataTest/Languages/Python/ToLaurelTest.lean | 314 +++++---- .../test_datetime_now_tz.expected | 2 +- .../Languages/Python/SpecsTest.lean | 2 +- 12 files changed, 1036 insertions(+), 923 deletions(-) diff --git a/Strata/Languages/Python/PySpecPipeline.lean b/Strata/Languages/Python/PySpecPipeline.lean index 6fb4a620e0..ce4d12374c 100644 --- a/Strata/Languages/Python/PySpecPipeline.lean +++ b/Strata/Languages/Python/PySpecPipeline.lean @@ -51,18 +51,31 @@ public structure PySpecLaurelResult where private def specDefaultToExpr : Python.Specs.SpecDefault → Python.expr SourceRange | .none => .Constant default (.ConNone default) default -/-- Convert a pyspec Arg to a PythonFunctionDecl arg tuple. -/ -private def specArgToFuncDeclArg (arg : Python.Specs.Arg): Python.PyArgInfo := - -- Map each SpecType atom to its PyLauType tag. - -- Multi-atom types (e.g., Optional[str] = [NoneType, str]) produce - -- multiple entries so getUnionTypeConstraint generates a disjunction. - let tys := arg.type.atoms.toList.filterMap fun a => match a with - | .ident nm _ => Python.PythonIdent.toPyLauType? nm - | _ => none - -- Fall back to ["Any"] if no atoms were recognized (unknown types - -- should not generate constraints). - let tys := if tys.isEmpty then ["Any"] else tys - {name := arg.name, source := none, tys := tys, default := arg.default.map specDefaultToExpr} +/-- Compute `laurelType` for a pyspec parameter. + Mirrors `specTypeToLaurelType` in ToLaurel.lean: builtins → `Any`, + other single-ident types → `UserDefined(prefixedName)`. + Uses the type's own module (not the function's module) to derive the + Laurel prefix, so cross-module type references resolve correctly. -/ +private def specArgLaurelType (arg : Python.Specs.Arg) : Laurel.HighTypeMd := + match arg.type.asIdent with + | some id => + if id ∈ Python.Specs.ToLaurel.builtinIdents then + Python.AnyTy + else + Python.mkHighTypeMd (.UserDefined { text := id.toLaurelName }) + | none => Python.AnyTy + +/-- Convert a pyspec Arg to a PythonFunctionDecl arg info. + `typeTesters` is empty because `buildSpecBody` already generates type + assertions in the procedure body — call-site preconditions would be + redundant. -/ +private def specArgToFuncDeclArg (arg : Python.Specs.Arg) : Python.PyArgInfo := + { name := arg.name, + source := none, + laurelType := specArgLaurelType arg, + typeTesters := #[], + default := arg.default.map specDefaultToExpr + } /-- Build a PythonFunctionDecl from a PySpec FunctionDecl or class method, expanding `**kwargs` TypedDict fields into individual parameters. -/ @@ -70,29 +83,33 @@ private def funcDeclToFunctionDecl (name : String) (args : Python.Specs.ArgDecls : Except String Python.PythonFunctionDecl := do let kwargsArgs ← Python.Specs.ToLaurel.expandKwargsArgs args.kwargs let allArgs := args.args ++ args.kwonly ++ kwargsArgs - pure { name, args := allArgs.toList.map specArgToFuncDeclArg, - kwargsName := none, ret := none } + pure { + name, + args := allArgs.toList.map specArgToFuncDeclArg, + kwargsName := none, + ret := none + } /-- Extract PythonFunctionDecl entries from pyspec signatures. Handles both top-level functions and class methods. Strips `self` from class methods and expands `**kwargs` TypedDict fields. -/ private def extractFunctionSignatures (sigs : Array Python.Specs.Signature) - (modulePrefix : String) : Except String (List Python.PythonFunctionDecl) := do - let prefixName (n : String) := if modulePrefix.isEmpty then n else modulePrefix ++ "_" ++ n - let mut result : List Python.PythonFunctionDecl := [] + (modulePrefix : String) : Except String (Array Python.PythonFunctionDecl) := do + let funcPrefix := if modulePrefix.isEmpty then "" else modulePrefix ++ "_" + let mut result : Array Python.PythonFunctionDecl := #[] for sig in sigs do match sig with | .functionDecl func => if !func.isOverload then - result := result ++ [← funcDeclToFunctionDecl (prefixName func.name) func.args] + result := result.push (← funcDeclToFunctionDecl (funcPrefix ++ func.name) func.args) | .classDef cls => - let clsName := prefixName cls.name + let clsName := funcPrefix ++ cls.name for method in cls.methods do if method.args.args.size == 0 then throw s!"Method '{cls.name}.{method.name}' has no arguments (expected 'self' as first parameter)" let posArgs := method.args.args.extract 1 method.args.args.size -- strip self let decl ← funcDeclToFunctionDecl (clsName ++ "@" ++ method.name) { method.args with args := posArgs } - result := result ++ [decl] + result := result.push decl | _ => pure () return result @@ -121,7 +138,7 @@ public def buildPySpecLaurel (pyspecEntries : Array (String × String)) let mut combinedProcedures : Array (Laurel.Procedure × String) := #[] let mut combinedTypes : Array (Laurel.TypeDefinition × String) := #[] let mut allOverloads := overloads - let mut funcSigs : List Python.PythonFunctionDecl := [] + let mut funcSigs : Array Python.PythonFunctionDecl := #[] let mut allTypeAliases : Std.HashMap String String := {} let mut allExhaustiveClasses : Std.HashSet String := {} let mut allWarnings : Array Python.Specs.SpecError := #[] @@ -172,7 +189,8 @@ public def buildPySpecLaurel (pyspecEntries : Array (String × String)) constants := [] } return { laurelProgram := combinedLaurel, overloads := allOverloads - functionSignatures := funcSigs, typeAliases := allTypeAliases + functionSignatures := funcSigs.toList, + typeAliases := allTypeAliases exhaustiveClasses := allExhaustiveClasses pyspecWarnings := allWarnings } @@ -452,7 +470,7 @@ public def pythonAndSpecToLaurel let metadataPath := sourcePath.getD pythonIonPath let (laurelProgram, _ctx) ← profileStep profile "Translate Python to Laurel" do - match Python.pythonToLaurel' preludeInfo stmts metadataPath result.overloads with + match Python.pythonToLaurel preludeInfo stmts metadataPath result.overloads with | .error (.userPythonError range msg) => throw (.userCode range msg) | .error (.unsupportedConstruct msg ast) => throw (.knownLimitation s!"Unsupported construct: {msg}\nAST: {ast}") diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index 07f9c28918..2e820d9879 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -58,17 +58,21 @@ deriving Inhabited structure PyArgInfo where name : String source : Option FileRange - tys : List String + laurelType : HighTypeMd + typeTesters : Array String default : Option (Python.expr SourceRange) -deriving Repr + +structure PyRetInfo where + source : Option FileRange + laurelType : HighTypeMd + typeTesters : Array String structure PythonFunctionDecl where name : String - --args include name, type, default value args : List PyArgInfo kwargsName: Option String - ret : Option (List String × Option FileRange) -deriving Repr, Inhabited + ret : Option PyRetInfo +deriving Inhabited /-- A symbol imported from a PySpec module, carrying its Laurel-internal name and enough metadata for the translator to use it directly. -/ @@ -248,6 +252,10 @@ def AnyConstructor.None := "from_None" def isOfAnyType (ty: String): Bool := ty ∈ [PyLauType.None, PyLauType.Bool, PyLauType.Int, PyLauType.Float, PyLauType.Str, PyLauType.Datetime, PyLauType.Bytes, PyLauType.ListAny, PyLauType.DictStrAny, PyLauType.Any] +def pyLauTypeTesters (tys : List String) : Array String := + tys.foldl (init := #[]) fun acc ty => + if isOfAnyType ty && ty ≠ "Any" then acc.push ("Any..isfrom_" ++ ty) else acc + def PyLauFuncReturnVar := "LaurelResult" /-- Convert a Laurel HighType to a PyLauType string for type inference. -/ @@ -309,6 +317,11 @@ def compositeToStringAnyName (typeName : String) : String := "$composite_to_stri def isCompositeType (ctx : TranslationContext) (typeName : String) : Bool := typeName != PyLauType.Any && (ctx.importedSymbols[typeName]?.any fun s => match s with | .compositeType _ => true | _ => false) + +def pyArgLaurelType (ctx : TranslationContext) (tys : List String) : HighTypeMd := + match tys with + | [ty] => if isCompositeType ctx ty then mkHighTypeMd (.UserDefined { text := ty }) else AnyTy + | _ => AnyTy def strToAny (s: String) := mkStmtExprMd (.StaticCall "from_str" [mkStmtExprMd (StmtExpr.LiteralString s)]) def intToAny (i: Int) := mkStmtExprMd (.StaticCall "from_int" [mkStmtExprMd (StmtExpr.LiteralInt i)]) def boolToAny (b: Bool) := mkStmtExprMd (.StaticCall "from_bool" [mkStmtExprMd (StmtExpr.LiteralBool b)]) @@ -350,8 +363,7 @@ def floatToAny (d : Decimal) := mkStmtExprMd (.StaticCall "from_float" [mkStmtEx def Any_to_bool (b: StmtExprMd) := mkStmtExprMd (.StaticCall "Any_to_bool" [b]) /-- The set of PyLauType names that have runtime type-tester predicates - (`Any..isfrom_`). Keep in sync with `PythonIdent.toPyLauType?` - in Specs/ToLaurel.lean. -/ + (`Any..isfrom_`). -/ private def pyLauTypesWithTesters : List String := ["int", "str", "bool", "float"] /-- Return the Laurel type-tester predicate name for a Python type annotation, if known. @@ -480,6 +492,12 @@ def ListAny_mk (es: List StmtExprMd) : StmtExprMd := match es with | [] => mkStmtExprMd (.StaticCall "ListAny_nil" []) | e::t => mkStmtExprMd (.StaticCall "ListAny_cons" [e, ListAny_mk t]) +def createBoolOrExpr (exprs: List StmtExprMd) : StmtExprMd := + match exprs with + | [] => mkStmtExprMd (.LiteralBool false) + | [expr] => expr + | expr::exprs => mkStmtExprMd (.PrimitiveOp .Or [expr, createBoolOrExpr exprs]) + mutual partial def translateList (ctx : TranslationContext) (elmts: List (Python.expr SourceRange)) @@ -804,6 +822,10 @@ partial def translateExpr (ctx : TranslationContext) (e : Python.expr SourceRang return fieldExpr | _ => return fieldExpr + else if isPackage ctx obj then + -- FIXME: Module attribute (e.g., sys.argv): modules are not modeled as + -- Laurel values, so return Hole like we do for unmodeled package calls. + return mkStmtExprMd .Hole else -- Regular object.field access let objExpr ← translateExpr ctx obj @@ -921,9 +943,10 @@ partial def getFunctionReturnType (ctx : TranslationContext) (func: Python.expr | .ok none => let (fname, _) ← refineFunctionCallExpr ctx func match ctx.functionSignatures.find? (λ f => f.name == fname) with - | some funcDecl => match funcDecl.ret with - | some ([ty], _) => return ty - | _ => return PyLauType.Any + | some funcDecl => + match funcDecl.ret with + | some retInfo => return highTypeToPyLauType retInfo.laurelType.val + | none => return PyLauType.Any | _ => return PyLauType.Any @@ -1237,21 +1260,24 @@ partial def translateCall (ctx : TranslationContext) -- Emit type assertions: if a key is present in the dict, its value -- must match the declared parameter type. This catches {"key": None} -- where the parameter type is str/int/bool/float. - let mut typeAsserts : List StmtExprMd := [] + let mut typeAsserts : Array StmtExprMd := #[] let dictExpr := mkStmtExprMd (.StaticCall "Any..as_Dict!" [trans_dict]) for arg in remainingParams do - let argType := match arg.tys with | [ty] => ty | _ => "Any" - if let some testerName := typeTester? argType then + if arg.typeTesters.size > 0 then let keyPresent := mkStmtExprMd (.StaticCall "DictStrAny_contains" [dictExpr, mkStmtExprMd (.LiteralString arg.name)]) let val := DictStrAny_get_param trans_dict arg.name true - let isCorrectType := mkStmtExprMd (.StaticCall testerName [val]) + let checks := arg.typeTesters.map fun callee => + mkStmtExprMd (.StaticCall (mkId callee) [val]) + let isCorrectType := createBoolOrExpr checks.toList let cond := mkStmtExprMd (.PrimitiveOp .Implies [keyPresent, isCorrectType]) - typeAsserts := mkStmtExprMd (.Assert { condition := cond }) :: typeAsserts - let typeAssertsOrdered := typeAsserts.reverse + typeAsserts := typeAsserts.push (mkStmtExprMd (.Assert { condition := cond })) + let typeAssertsOrdered := typeAsserts.toList let call ← emitCall (allArgs ++ kwargsArg) - if typeAssertsOrdered.isEmpty then return call - else return mkStmtExprMd (.Block (typeAssertsOrdered ++ [call]) none) + if typeAssertsOrdered.isEmpty then + return call + else + return mkStmtExprMd (.Block (typeAsserts.toList ++ [call]) none) else let (args, kwords, funcdecl_hasKwargs) ← combinePositionalAndKeywordArgs args kwords funcDecl methodName callRange @@ -1749,7 +1775,9 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang let finalCtx := { bodyCtx with variableTypes := mergedVars } return (finalCtx, [tryBlock]) - | .Raise _ _ _ => return (ctx, [mkStmtExprMd .Hole]) + -- FIXME: Placeholder — `raise` is dropped so the Hole type inferrer doesn't + -- produce Unknown types. Must be replaced to correctly model exceptions later. + | .Raise _ _ _ => return (ctx, []) -- With statement: with EXPR as VAR: BODY -- Desugars to: mgr = EXPR; VAR = mgr.__enter__(); BODY; mgr.__exit__() @@ -1925,6 +1953,7 @@ def getUnionTypes (arg: Python.expr SourceRange) : List (Python.expr SourceRange | .BinOp _ left _ right => getUnionTypes left ++ getUnionTypes right | _ => [arg] +-- Could def checkValidInputTypeList (ctx : TranslationContext) (tys: List String) : Except TranslationError (List String) := do for ty in tys do let _ ← translateType ctx ty @@ -1974,8 +2003,7 @@ def unpackPyArguments (ctx : TranslationContext) (args: Python.arguments SourceR let listdefaults := (List.range (argscount - defaultscount)).map (λ _ => none) ++ defaults.val.toList.map (λ x => some x) let argsinfo := args.val.toList.zip listdefaults - let mut argtypes : List PyArgInfo := [] - let mut tys : List String := [] + let mut argtypes : Array PyArgInfo := #[] for (arg, default) in argsinfo do match arg with | .mk_arg sr name oty _ => @@ -1983,21 +2011,29 @@ def unpackPyArguments (ctx : TranslationContext) (args: Python.arguments SourceR let defaultType := match default.mapM (inferExprType ctx) with | .ok (some ty) => some ty | _ => none + let mut tys : List String := [] tys ← match oty.val with | .some ty => getArgumentTypes ty | _ => pure [PyLauType.Any] match defaultType with - | .some "None" => --Only None is allowed to add to type list - if tys != [PyLauType.Any] then - tys:= (PyLauType.None::tys).dedup - | .some defaultType => - if isOfAnyType defaultType && tys != [PyLauType.Any] && defaultType ∉ tys then - throw (.unsupportedConstruct "Default value type is invalid" (toString (repr arg))) - | _ => pure () + | .some "None" => --Only None is allowed to add to type list + if tys != [PyLauType.Any] then + tys:= (PyLauType.None::tys).dedup + | .some defaultType => + if isOfAnyType defaultType && tys != [PyLauType.Any] && defaultType ∉ tys then + throw (.unsupportedConstruct "Default value type is invalid" (toString (repr arg))) + | _ => + pure () tys ← checkValidInputTypeList ctx tys - argtypes := argtypes ++ [{name:= name.val, source := md, tys:= tys, default:= default}] + argtypes := argtypes.push { + name:= name.val, + source := md, + laurelType := pyArgLaurelType ctx tys, + typeTesters := pyLauTypeTesters tys, + default:= default + } let kwargsName := kwargs.val.map (λ kwarg => match kwarg with | .mk_arg _ name _ _ => name.val) - return (argtypes, kwargsName) + return (argtypes.toList, kwargsName) def pyFuncDefToPythonFunctionDecl (ctx : TranslationContext) (f : Python.stmt SourceRange) : Except TranslationError PythonFunctionDecl := do match f with @@ -2006,14 +2042,24 @@ def pyFuncDefToPythonFunctionDecl (ctx : TranslationContext) (f : Python.stmt S let args_trans ← unpackPyArguments ctx args let args := if ctx.currentClassName.isSome then args_trans.fst.tail else args_trans.fst let ret ← if name.endsWith "@__init__" then - let retMd := sourceRangeToSource ctx.filePath f.ann - pure $ some ([(name.dropEnd "@__init__".length).toString], retMd) + let retSource := sourceRangeToSource ctx.filePath f.ann + let className := (name.dropEnd "@__init__".length).toString + pure $ some { + source := retSource, + laurelType := mkHighTypeMd (.UserDefined { text := className }), + typeTesters := #[] + } else match returns.val with | some retTy => - let retMd := sourceRangeToSource ctx.filePath retTy.ann + let retSource := sourceRangeToSource ctx.filePath retTy.ann match checkValidInputTypeList ctx (← getArgumentTypes retTy) with - | .ok tys => pure (tys, retMd) + | .ok tys => + pure $ some { + source := retSource, + laurelType := pyArgLaurelType ctx tys, + typeTesters := pyLauTypeTesters tys + } | _ => pure none | none => pure none let kwargsName := args_trans.snd @@ -2029,25 +2075,16 @@ def pyFuncDefToPythonFunctionDecl (ctx : TranslationContext) (f : Python.stmt S for a mutable local copy inside the procedure body. -/ def paramInputPrefix : String := "$in_" -def getSingleTypeConstraint (var: String) (ty: String): Option StmtExprMd := - if isOfAnyType ty && ty ≠ "Any" then mkStmtExprMd (.StaticCall ("Any..isfrom_" ++ ty) [freeVar var]) else none - -def createBoolOrExpr (exprs: List StmtExprMd) : StmtExprMd := - match exprs with - | [] => mkStmtExprMd (.LiteralBool true) - | [expr] => expr - | expr::exprs => mkStmtExprMd (.PrimitiveOp .Or [expr, createBoolOrExpr exprs]) - -def getUnionTypeConstraint (var: String) (source: Option FileRange) (tys: List String) (funcname: String) - (displayName : String := var): Option Condition := - let type_constraints := tys.filterMap (getSingleTypeConstraint var) - if type_constraints.isEmpty then none else - let cond := createBoolOrExpr type_constraints - some { condition := { cond with source := source }, +def getTypeConstraint (var : String) (source : Option FileRange) (testers : Array String) + (funcname : String) (displayName : String := var) : Option Condition := + let constraints := testers.toList.map fun callee => + mkStmtExprMd (.StaticCall (mkId callee) [freeVar var]) + if constraints.isEmpty then none else + some { condition := { createBoolOrExpr constraints with source := source }, summary := some $ "(" ++ funcname ++ " requires) Type constraint of " ++ displayName } -def getReturnTypeEnsure (source: Option FileRange) (tys: List String) (funcname: String): Option Condition := - getUnionTypeConstraint PyLauFuncReturnVar source tys funcname +def getReturnTypeEnsure (source : Option FileRange) (testers : Array String) (funcname : String) : Option Condition := + getTypeConstraint PyLauFuncReturnVar source testers funcname |>.map fun c => { c with summary := some $ "(" ++ funcname ++ " ensures) Return type constraint" } @@ -2090,11 +2127,8 @@ def translateFunction (ctx : TranslationContext) (sourceRange: SourceRange) (fun -- Translate parameters let mut inputs : List Parameter := [] - inputs := funcDecl.args.map (fun arg => - if arg.tys.length == 1 && isCompositeType ctx arg.tys[0]! then - { name := arg.name, type := mkHighTypeMd (.UserDefined {text:= arg.tys[0]!}) } - else - { name := arg.name, type := AnyTy}) + inputs := funcDecl.args.map fun arg => + { name := arg.name, type := arg.laurelType } inputs := match ctx.currentClassName with | some className => @@ -2111,11 +2145,14 @@ def translateFunction (ctx : TranslationContext) (sourceRange: SourceRange) (fun | _ => pure () let typeConstraintPreconditions := funcDecl.args.filterMap - (fun arg => getUnionTypeConstraint (paramInputPrefix ++ arg.name) arg.source arg.tys funcDecl.name arg.name) + (fun arg => getTypeConstraint (paramInputPrefix ++ arg.name) arg.source arg.typeTesters funcDecl.name arg.name) let typeConstraintPostcondition := if funcDecl.name.endsWith "@__init__" then [] else - match funcDecl.ret.map fun (tys, source) => getReturnTypeEnsure source tys funcDecl.name with - | some (some constraint) => [constraint] + match funcDecl.ret with + | some retInfo => + match getReturnTypeEnsure retInfo.source retInfo.typeTesters funcDecl.name with + | some constraint => [constraint] + | none => [] | _ => [] -- Translate return type @@ -2123,8 +2160,8 @@ def translateFunction (ctx : TranslationContext) (sourceRange: SourceRange) (fun let outputs : List Parameter := [{ name := PyLauFuncReturnVar, type := AnyTy }] -- Translate function body - let inputTypes := funcDecl.args.map (λ arg => - match arg.tys with | [ty] => (arg.name, ty) | _ => (arg.name, PyLauType.Any)) + let inputTypes := funcDecl.args.map fun arg => + (arg.name, highTypeToPyLauType arg.laurelType.val) let ctx := {ctx with variableTypes:= ("nullcall_ret", PyLauType.Any)::inputTypes} let (bodyTrans, newCtx) ← match body with | some body => @@ -2182,15 +2219,25 @@ def preludeSignatureToPythonFunctionDecl (prelude : Core.Program) : List PythonF prelude.decls.filterMap fun decl => match Core.Program.Procedure.find? prelude decl.name with | some proc => - let inputTypes := proc.header.inputs.values.map getTypeName - let inputnames := proc.header.inputs.keys.map (λ n => n.name) - let outputtypes := proc.header.outputs.values.map getTypeName let noneexpr : Python.expr SourceRange := .Constant default (.ConNone default) default + let retParam := proc.header.outputs.head? + let ret := retParam.map fun (_, tp) => + let tys := [getTypeName tp] + { source := none, + laurelType := mkHighTypeMd (.UserDefined (mkId (getTypeName tp))), + typeTesters := pyLauTypeTesters tys : PyRetInfo } some { name:= proc.header.name.name - args:= (inputnames.zip inputTypes).map (λ(n,t) => {name:= n, source := none, tys:= [t], default:= noneexpr}) + args:= proc.header.inputs |>.map λ(nm,tp) => + { + name:= nm.name, + source := none, + laurelType := AnyTy, + typeTesters := pyLauTypeTesters [getTypeName tp], + default:= noneexpr + } kwargsName := none - ret := if outputtypes.length == 0 then none else some ([outputtypes[0]!], none) + ret } | none => none /-- Extract field declarations from class body (annotated assignments at class level) -/ @@ -2300,7 +2347,9 @@ def mkDefaultInitDecl (className : String) : PythonFunctionDecl × Procedure := -- where `self` is stripped via `.tail` for methods inside a class. args := [] kwargsName := none - ret := some ([className], none) + ret := some { source := none, + laurelType := mkHighTypeMd (.UserDefined { text := className }), + typeTesters := #[] } } -- Derive the procedure from the decl, mirroring translateMethod's convention let selfParam : Parameter := { @@ -2441,7 +2490,7 @@ def getPreludeProcedures (prelude: Core.Program) : List String := |.proc => some decl.name.name | _ => none) -/-- Information extracted from the prelude that `pythonToLaurel'` needs. +/-- Information extracted from the prelude that `pythonToLaurel` needs. This decouples the translation from a specific `Core.Program` representation, allowing the caller to supply prelude info from Laurel-level declarations. -/ structure PreludeInfo where @@ -2524,8 +2573,11 @@ def PreludeInfo.ofLaurelProgram (prog : Laurel.Program) : PreludeInfo where let argName := if param.name.text.startsWith paramInputPrefix then (param.name.text.drop paramInputPrefix.length).toString else param.name.text - {name:= argName, source := none, tys:= [getHighTypeName param.type.val], default:= some noneexpr} - let ret := p.outputs.head?.map fun param => ([getHighTypeName param.type.val], none) + {name:= argName, source := none, laurelType := param.type, typeTesters := pyLauTypeTesters [getHighTypeName param.type.val], default:= some noneexpr} + let ret := p.outputs.head?.map fun param => + let tys := [getHighTypeName param.type.val] + { source := none, laurelType := param.type, + typeTesters := pyLauTypeTesters tys : PyRetInfo } some { name := p.name.text, args := args, kwargsName := none, ret := ret } functions := let funcNames := prog.staticProcedures.filterMap fun p => @@ -2564,7 +2616,7 @@ def PreludeInfo.merge (a b : PreludeInfo) : PreludeInfo where importedSymbols := b.importedSymbols.fold (init := a.importedSymbols) fun m k v => m.insert k v /-- Translate Python module to Laurel Program using pre-extracted prelude info. -/ -def pythonToLaurel' (info : PreludeInfo) +def pythonToLaurel (info : PreludeInfo) (body : Array (stmt SourceRange)) (filePath : String := "") (overloadTable : OverloadTable := {}) @@ -2768,17 +2820,5 @@ def pythonToLaurel' (info : PreludeInfo) return (program, ctx) -/-- Translate Python module to Laurel Program. - Delegates to `pythonToLaurel'` after extracting prelude info, - then prepends External stubs so the Laurel resolve pass can - see prelude names. -/ -def pythonToLaurel (prelude: Core.Program) - (pyCommands : Array (stmt SourceRange)) - (filePath : String := "") - (overloadTable : OverloadTable := {}) - : Except TranslationError (Laurel.Program × TranslationContext) := do - let info := PreludeInfo.ofCoreProgram prelude - pythonToLaurel' info pyCommands filePath overloadTable - end -- public section end Strata.Python diff --git a/Strata/Languages/Python/Specs.lean b/Strata/Languages/Python/Specs.lean index 26c3bca963..00ec5f786d 100644 --- a/Strata/Languages/Python/Specs.lean +++ b/Strata/Languages/Python/Specs.lean @@ -23,8 +23,10 @@ public class PySpecMClass (m : Type → Type) where specWarning (loc : SourceRange) (message : String) : m Unit /-- Run an action and check if any new errors were reported. -/ runChecked {α} (act : m α) : m (Bool × α) + /-- Run an action and return `true` if no new errors or warnings were reported. -/ + runNoWarn {α} (act : m α) : m (Bool × α) -open PySpecMClass (specError specWarning runChecked) +open PySpecMClass (specError specWarning runChecked runNoWarn) /-- String identifier for event types. -/ public abbrev EventType := String @@ -368,6 +370,13 @@ instance : PySpecMClass PySpecM where let r ← act let new_cnt := (←get).errors.size return (cnt = new_cnt, r) + runNoWarn act := do + let s := ←get + let errCnt := s.errors.size + let warnCnt := s.warnings.size + let r ← act + let s' := ←get + return (errCnt = s'.errors.size ∧ warnCnt = s'.warnings.size, r) def getNameValue? (id : String) : PySpecM (Option SpecValue) := return (←get).nameMap[id]? @@ -415,7 +424,7 @@ def valueAsType (loc : SourceRange) (v : SpecValue) : PySpecM SpecType := do | .typeValue itp => pure itp | .noneConst => - return .ofAtom loc .noneType + return .noneType loc | .requiredType tp => return tp | .notRequiredType tp => return tp | .stringConst loc val => @@ -427,7 +436,7 @@ def valueAsType (loc : SourceRange) (v : SpecValue) : PySpecM SpecType := do recordTypeRef loc val let mod := toString (← read).currentModule let pyIdent : PythonIdent := { pythonModule := mod, name := val } - return .ofAtom loc (.ident pyIdent #[]) + return .ident loc pyIdent | _ => specError loc s!"Expected type instead of {repr v}." return default @@ -463,16 +472,16 @@ def literalTranslator : TypeTranslator where | arg => #[arg] let .isTrue _ := decideProp (args.size > 0) | specError loc s!"Union expects at least one argument."; return default - let trans (v : SpecValue) : PySpecM SpecAtomType := do + let trans (v : SpecValue) : PySpecM SpecType := do match v with | .intConst _ n => - pure <| .intLiteral n + pure <| .intLiteral loc n | .stringConst _ s => - pure <| .stringLiteral s + pure <| .stringLiteral loc s | _ => specError loc s!"Unsupported literal value {repr v}." pure default - return .ofArray loc (← args.mapM trans) + return .unionArray loc (← args.mapM trans) def metadataProcessor : MetadataType → TypeTranslator | .typingDict => fixedTranslator .typingDict 2 @@ -512,7 +521,7 @@ def translateCall (loc : SourceRange) (func : SpecValue) -- Bare type (no Required/NotRequired wrapper) — treat as required fieldTypes := fieldTypes.push (← valueAsType loc v) fieldRequired := fieldRequired.push true - return .typeValue <| .ofAtom loc <| .typedDict fields fieldTypes fieldRequired + return .typeValue <| .typedDict loc fields fieldTypes fieldRequired else let .isTrue kwargsSizep := decideProp (kwargs.size = 1) | specError loc "TypedDict expects 0 or 1 keywords"; return default @@ -523,7 +532,7 @@ def translateCall (loc : SourceRange) (func : SpecValue) | specError loc "TypedDict expects total bool"; return default let values ← fieldsPairs |>.mapM fun (_name, v) => valueAsType loc v let fieldRequired := values.map fun _ => total - return .typeValue <| .ofAtom loc <| .typedDict fields values fieldRequired + return .typeValue <| .typedDict loc fields values fieldRequired | _ => specError loc s!"Unknown call {repr func}." return default @@ -553,14 +562,10 @@ def translateSubscript (paramLoc : SourceRange) (paramType : String) specError paramLoc s!"Unknown parameterized type {paramType}." return default | some (.typeValue tpp) => - let .isTrue tpp_sizep := decideProp (tpp.atoms.size = 1) + let some tpId := tpp.asIdent | specError paramLoc s!"Expected type name" return default - let tpa := tpp.atoms[0] - let .ident tpId tpParams := tpa - | specError paramLoc "Expected an identifier" - return default - if tpId == .builtinsDict ∧ tpParams.size = 0 then + if tpId == .builtinsDict then .typeValue <$> (fixedTranslator .typingDict 2 |>.callback paramLoc sargs) else specError paramLoc s!"Unsupported type {repr tpId}" @@ -722,8 +727,7 @@ def pySpecArg (usedNames : Std.HashSet String) structure SpecAssertionContext where filePath : System.FilePath - kwargsParamName : Option String := none - kwargsType : Option SpecType := none + kwargs : Option (String × SpecType) := none /-- Local variable type bindings (e.g., from for-loop iteration variables). -/ localTypes : Std.HashMap String SpecType := {} @@ -751,315 +755,246 @@ instance : PySpecMClass SpecAssertionM where let r ← act let new_cnt := (←get).errors.size return (cnt = new_cnt, r) + runNoWarn act := do + let s := ←get + let errCnt := s.errors.size + let warnCnt := s.warnings.size + let r ← act + let s' := ←get + return (errCnt = s'.errors.size ∧ warnCnt = s'.warnings.size, r) /-- Match a subscript expression `param["field"]` against the kwargs parameter - name from context, returning `(paramName, fieldName)` on success. -/ -def extractKwargsField (e : expr SourceRange) - : SpecAssertionM (Option (String × String)) := do + name from context, returning `(paramName, fieldName, kwType)` on success. -/ +def extractKwargsField (ctx : SpecAssertionContext) (e : expr SourceRange) + : Option (String × String × SpecType) := do match e with - | .Subscript _ (.Name _ ⟨_, paramName⟩ (.Load _)) - (.Constant _ (.ConString _ ⟨_, fieldName⟩) _) (.Load _) => - match (←read).kwargsParamName with - | some kn => - if paramName == kn then return some (kn, fieldName) - else return none - | none => return none - | _ => return none - -/-- Extract a `SpecExpr` subject from a Python expression. - Recognizes kwargs subscripts (`kw["field"]` → `.getIndex (.var kn) fn`), - nested subscripts (`kw["a"]["b"]` → `.getIndex (.getIndex (.var kn) "a") "b"`), - and plain variable names (`.Name` → `.var name`). - Returns `none` for unsupported expression forms. -/ -partial def extractSubject (e : expr SourceRange) - : SpecAssertionM (Option SpecExpr) := do - match ← extractKwargsField e with - | some (kn, fn) => - return some (.getIndex (.var kn (loc := e.ann)) fn (loc := e.ann)) - | none => - pure () - match e with - | .Name _ ⟨_, name⟩ (.Load _) => return some (.var name (loc := e.ann)) - | .Subscript _ inner (.Constant _ (.ConString _ ⟨_, fieldName⟩) _) (.Load _) => - match ← extractSubject inner with - | some subj => return some (.getIndex subj fieldName (loc := e.ann)) - | none => return none - | _ => return none - -/-- Translate a Python `if` condition into a `SpecExpr`. - Currently recognizes `"key" in container` patterns. -/ -def transCondition (e : expr SourceRange) : SpecAssertionM (Option SpecExpr) := do - match e with - | .Compare _ (.Constant _ (.ConString _ ⟨_, key⟩) _) ⟨_, ops⟩ ⟨_, comparators⟩ => - if h₁ : ops.size = 1 then - if h₂ : comparators.size = 1 then - match ops[0] with - | .In _ => - match ← extractSubject comparators[0] with - | some subj => return some (.containsKey subj key (loc := e.ann)) - | none => pure () - | _ => pure () - pure () - | _ => pure () - return none + | .Subscript _ (.Name _ ⟨_, paramName⟩ (.Load _)) (.Constant _ (.ConString _ ⟨_, fieldName⟩) _) (.Load _) => + let (kwName, kwTp) ← ctx.kwargs + if kwName == paramName then + some (paramName, fieldName, kwTp) + else + none + | _ => + none /-- Run an action that may produce assertions, then wrap each new assertion's - formula with `implies cond ...` (or `implies (not cond) ...` for else branches). - If `cond` is `none`, assertions pass through unchanged. -/ -def assumeCondition (cond : Option SpecExpr) (loc : SourceRange) (act : SpecAssertionM Unit) + formula with `implies cond ...` (or `implies (not cond) ...` for else branches). -/ +def assumeCondition (cond : SpecExpr) (loc : SourceRange) (act : SpecAssertionM Unit) : SpecAssertionM Unit := do let prevAssertions := (←get).assertions modify fun s => { s with assertions := #[] } act let newAssertions := (←get).assertions - match cond with - | some c => - let wrapped := newAssertions.map fun a => - { a with formula := .implies c a.formula loc } - modify fun s => { s with assertions := prevAssertions ++ wrapped } - | none => - modify fun s => { s with assertions := prevAssertions ++ newAssertions } - -/-- Translate a Python expression from an f-string message into a `SpecExpr`. - Handles kwargs subscripts, variable names, and `len(...)` calls. - Falls back to `.placeholder` for unsupported patterns. -/ -def transMessageExpr (e : expr SourceRange) - : SpecAssertionM SpecExpr := do - match ← extractSubject e with - | some subj => return subj - | none => pure () - match e with - | .Call _ (.Name _ ⟨_, funcName⟩ (.Load _)) ⟨_, args⟩ _ => - if funcName == "len" && args.size == 1 then - match ← extractSubject args[0]! with - | some subj => return .len subj (loc := e.ann) - | none => return .placeholder (loc := e.ann) - else return .placeholder (loc := e.ann) - | _ => return .placeholder (loc := e.ann) - -/-- Look up a field in a TypedDict SpecType, returning its type if found. -/ -def lookupTypedDictField (tp : SpecType) (field : String) : Option SpecType := do - for atom in tp.atoms do - match atom with - | .typedDict fields fieldTypes _ => - for i in [:fields.size] do - if fields[i]! == field then return fieldTypes[i]! - | _ => pure () - none - -/-- Infer the type of a Python expression from available type context. - Handles kwargs subscripts, nested subscripts, and local variable bindings. -/ -partial def inferExprType (e : expr SourceRange) : SpecAssertionM (Option SpecType) := do - match e with - | .Name _ ⟨_, name⟩ (.Load _) => - return (←read).localTypes[name]? - | .Subscript _ (.Name _ ⟨_, paramName⟩ (.Load _)) - (.Constant _ (.ConString _ ⟨_, fieldName⟩) _) (.Load _) => - match (←read).kwargsParamName with - | some kn => - if paramName == kn then - match (←read).kwargsType with - | some kwTp => return lookupTypedDictField kwTp fieldName - | none => return none - else return none - | none => return none - | .Subscript _ inner (.Constant _ (.ConString _ ⟨_, fieldName⟩) _) (.Load _) => - match ← inferExprType inner with - | some innerTp => return lookupTypedDictField innerTp fieldName - | none => return none - | _ => return none - -/-- Extract the element type from an iterable SpecType (e.g., typing.List(T) → T). -/ -def extractElementType (tp : SpecType) : Option SpecType := do - for atom in tp.atoms do - match atom with - | .ident pyId args => - if (pyId == .typingList || pyId == .typingSequence) && args.size == 1 then - return args[0]! - | _ => pure () - none - -/-- Extract key and value types from a dict SpecType - (e.g., typing.Dict(K, V) → (K, V)). -/ -def extractDictKeyValueTypes (tp : SpecType) : Option (SpecType × SpecType) := do - for atom in tp.atoms do - match atom with - | .ident pyId args => - if (pyId == .typingDict || pyId == .typingMapping) && args.size == 2 then - return (args[0]!, args[1]!) - | _ => pure () - none - -/-- Collect all `== "value"` arms from a chain of `or`-ed comparisons, - returning the subject and string values, or `none` if the expression - doesn't fit the pattern - `subj == "A" or subj == "B" or ...`. -/ -partial def collectEnumValues (e : expr SourceRange) - : SpecAssertionM (Option (SpecExpr × Array String)) := do - match e with - | .BoolOp _ (.Or _) ⟨_, values⟩ => - let mut result : Option (SpecExpr × Array String) := none - for val in values do - match ← collectEnumValues val with - | some (subj, vals) => - match result with - | none => result := some (subj, vals) - | some (_prevSubj, acc) => - -- TODO: check subject equality once SpecExpr has BEq - result := some (subj, acc ++ vals) - | none => return none - return result - | .Compare _ lhs ⟨_, ops⟩ ⟨_, comparators⟩ => - let .isTrue _ := decideProp (ops.size = 1) - | return none - match ops[0] with - | .Eq _ => - let .isTrue _ := decideProp (comparators.size = 1) - | return none - match ← extractSubject lhs with - | some subj => - match comparators[0] with - | .Constant _ (.ConString _ ⟨_, strVal⟩) _ => - return some (subj, #[strVal]) - | _ => return none - | none => return none - | _ => return none - | _ => return none - -/-- Extract an integer literal from a Python expression. - Handles `Constant(ConPos n)`, `Constant(ConNeg n)`, - and `UnaryOp(USub, Constant(ConPos n))` (i.e., `-n`). -/ -def extractIntBound (e : expr SourceRange) : Option Int := - match e with - | .Constant _ (.ConPos _ ⟨_, n⟩) _ => some (Int.ofNat n) - | .Constant _ (.ConNeg _ ⟨_, n⟩) _ => some (Int.negOfNat n) - | .UnaryOp _ (.USub _) (.Constant _ (.ConPos _ ⟨_, n⟩) _) => - some (Int.negOfNat n) - | _ => none - -/-- Extract a float literal string from a Python expression. - Handles `Constant(ConFloat s)` and `UnaryOp(USub, Constant(ConFloat s))`. -/ -def extractFloatBound (e : expr SourceRange) : Option String := - match e with - | .Constant _ (.ConFloat _ ⟨_, s⟩) _ => some s - | .UnaryOp _ (.USub _) (.Constant _ (.ConFloat _ ⟨_, s⟩) _) => some s!"-{s}" - | _ => none - -/-- Check if a SpecType is the `builtins.int` type. -/ -def isIntType (tp : SpecType) : Bool := tp.isAtom (.ident .builtinsInt #[]) - -/-- Check if a SpecType is the `builtins.float` type. -/ -def isFloatType (tp : SpecType) : Bool := tp.isAtom (.ident .builtinsFloat #[]) + let wrapped := newAssertions.map fun a => + { a with formula := .implies cond a.formula loc } + modify fun s => { s with assertions := prevAssertions ++ wrapped } /-- Build a comparison expression dispatching to float or int variants based on type. -/ private def makeComparison (floatCtor intCtor : SpecExpr → SpecExpr → SpecExpr) - (isFloat isInt : Bool) - (subj : SpecExpr) (bound : expr SourceRange) - : SpecAssertionM (Option SpecExpr) := do - let loc := bound.ann - if isFloat then - match extractFloatBound bound with - | some s => return some (floatCtor subj (.floatLit s (loc := loc))) - | none => - match extractIntBound bound with - | some n => return some (floatCtor subj (.floatLit (toString n) (loc := loc))) - | none => return none - else if isInt then - match extractIntBound bound with - | some n => return some (intCtor subj (.intLit n (loc := loc))) - | none => return none + (subj : SpecExpr) (subjType : SpecType) (bound : SpecExpr) (boundType : SpecType) + : Option SpecExpr := + if subjType.isFloatType then + match bound with + | .floatLit .. => some (floatCtor subj bound) + | .intLit v loc => some (floatCtor subj (.floatLit (toString v) (loc := loc))) + | _ => none + else if subjType.isIntType then + match bound with + | .intLit .. => some (intCtor subj bound) + | _ => none + else if boundType.isFloatType then + match bound with + | .floatLit .. => some (floatCtor subj bound) + | _ => none + else if boundType.isIntType then + match bound with + | .intLit .. => some (intCtor subj bound) + | _ => none else - match extractIntBound bound with - | some n => return some (intCtor subj (.intLit n (loc := loc))) - | none => return none + none + +private def transCompare (loc : SourceRange) + (lhsExpr : SpecExpr) (lhsType : SpecType) + (ops : Array (cmpop SourceRange)) + (comparators : Array (expr SourceRange)) + (transExpr : expr SourceRange → SpecAssertionM (SpecExpr × SpecType)) + : SpecAssertionM (Option SpecExpr) := do + let .isTrue h₁ := decideProp (ops.size = 1) + | return none + let .isTrue h₂ := decideProp (comparators.size = 1) + | return none + match lhsExpr with + -- len(subject) >= N / len(subject) <= N + | .stringLen _ _ => + let (clean, (boundExpr, _)) ← runNoWarn (transExpr comparators[0]) + if clean then + match boundExpr with + | .intLit .. => + match ops[0] with + | .GtE _ => return some (.intGe lhsExpr boundExpr (loc := loc)) + | .LtE _ => return some (.intLe lhsExpr boundExpr (loc := loc)) + | _ => pure () + | _ => pure () + -- compile("pattern").search(subject) is not None + | .regexMatch .. => + match ops[0], comparators[0] with + | .IsNot _, .Constant _ (.ConNone _) _ => + return some lhsExpr + | _, _ => pure () + | _ => pure () + -- subject == "A" (single enum value) + match ops[0] with + | .Eq _ => + match comparators[0] with + | .Constant _ (.ConString _ ⟨_, strVal⟩) _ => + return some (.enumMember lhsExpr #[strVal] (loc := loc)) + | _ => pure () + | _ => pure () -def transAssertExpr (e : expr SourceRange) - : SpecAssertionM SpecExpr := do + -- subject >= N / subject <= N (type-checked: int or float) + let (clean, (boundExpr, boundType)) ← runNoWarn (transExpr comparators[0]) + if not clean then + return none + + match ops[0] with + | .GtE _ => + return makeComparison (.floatGe · · (loc := loc)) (.intGe · · (loc := loc)) lhsExpr lhsType boundExpr boundType + | .LtE _ => + return makeComparison (.floatLe · · (loc := loc)) (.intLe · · (loc := loc)) lhsExpr lhsType boundExpr boundType + | _ => + return none + +/-- Unified expression translator. Translates a Python expression into a `SpecExpr` + paired with its inferred `SpecType`. Handles value expressions (variables, + subscripts, literals, `len`), assertion expressions (`isinstance`, comparisons, + `"key" in container`, enum equality, regex, BoolOp or-merging), and + `compile(...).search(...)` call patterns. Falls back to `.placeholder` + + `typing.Any` with a warning for unrecognized forms. -/ +partial def transExpr (e : expr SourceRange) + : SpecAssertionM (SpecExpr × SpecType) := do let loc := e.ann - -- isinstance(subject, T) + let anyType := SpecType.ident loc .typingAny + let boolType := SpecType.ident loc .builtinsBool + let intType := SpecType.ident loc .builtinsInt + let floatType := SpecType.ident loc .builtinsFloat + -- kwargs subscript: kw["field"] + if let some (kn, fn, kwTp) := extractKwargsField (←read) e then + let fieldTp := kwTp.lookupTypedDictField fn + if fieldTp.isNone && kwTp.isTypedDict then + specWarning loc s!"kwargs field \"{fn}\" not found in TypedDict" + return (.getIndex (.var kn (loc := loc)) fn (loc := loc), fieldTp.getD anyType) + + let placeholder := (.placeholder (loc := loc), anyType) match e with + -- Variable name + | .Name _ ⟨_, name⟩ (.Load _) => + let tp := match (←read).localTypes[name]? with + | some tp => tp + | none => anyType + return (.var name (loc := loc), tp) + -- Nested subscript: x["field"] + | .Subscript _ inner (.Constant _ (.ConString _ ⟨_, fieldName⟩) _) (.Load _) => + let (innerExpr, innerTp) ← transExpr inner + let fieldTp := innerTp.lookupTypedDictField fieldName + if fieldTp.isNone then + if innerTp.isTypedDict then + specWarning loc s!"field \"{fieldName}\" not found in TypedDict" + else + specWarning loc s!"subscript subject is not a TypedDict" + return (.getIndex innerExpr fieldName (loc := loc), fieldTp.getD anyType) + -- Integer literal + | .Constant _ (.ConPos _ ⟨_, n⟩) _ => + return (.intLit (Int.ofNat n) (loc := loc), intType) + | .Constant _ (.ConNeg _ ⟨_, n⟩) _ => + return (.intLit (Int.negOfNat n) (loc := loc), intType) + -- Float literal + | .Constant _ (.ConFloat _ ⟨_, s⟩) _ => + return (.floatLit s (loc := loc), floatType) + -- UnaryOp(USub, literal) — negative literals + | .UnaryOp _ (.USub _) (.Constant _ (.ConPos _ ⟨_, n⟩) _) => + return (.intLit (Int.negOfNat n) (loc := loc), intType) + | .UnaryOp _ (.USub _) (.Constant _ (.ConFloat _ ⟨_, s⟩) _) => + return (.floatLit s!"-{s}" (loc := loc), floatType) + -- String literal (extract value for use in messages) + | .Constant _ (.ConString _ ⟨_, _s⟩) _ => + return (.placeholder (loc := loc), SpecType.ident loc .builtinsStr) + -- Call expressions: len(...), isinstance(...) | .Call _ (.Name _ ⟨_, funcName⟩ (.Load _)) ⟨_, args⟩ _ => + if funcName == "len" then + if h : args.size = 1 then + let (subjExpr, _subjTp) ← transExpr args[0] + return (.stringLen subjExpr (loc := loc), intType) + else + specWarning loc s!"len expected 1 argument, got {args.size}" + return placeholder if funcName == "isinstance" then if h : args.size = 2 then - match ← extractSubject args[0] with - | some subj => + let (clean, (subjExpr, _)) ← runNoWarn (transExpr args[0]) + if clean then match args[1] with | .Name _ ⟨_, typeName⟩ (.Load _) => - return .isInstanceOf subj typeName (loc := loc) + return (.isInstanceOf subjExpr typeName (loc := loc), boolType) | _ => - specWarning e.ann s!"isinstance: unsupported type argument" - return .placeholder (loc := loc) - | none => pure () -- fall through - if funcName == "len" && args.size == 1 then - -- This is just len(x), not a comparison; fall through - pure () - | _ => pure () - -- len(subject) >= N / len(subject) <= N - match e with - | .Compare _ (.Call _ (.Name _ ⟨_, funcName⟩ (.Load _)) ⟨_, callArgs⟩ _) - ⟨_, ops⟩ ⟨_, comparators⟩ => - if funcName == "len" then - if h₁ : callArgs.size = 1 then - if h₂ : ops.size = 1 then - if h₃ : comparators.size = 1 then - match ← extractSubject callArgs[0] with - | some subj => - match ops[0], extractIntBound comparators[0] with - | .GtE _, some n => return .intGe (.len subj (loc := loc)) (.intLit n (loc := comparators[0].ann)) (loc := loc) - | .LtE _, some n => return .intLe (.len subj (loc := loc)) (.intLit n (loc := comparators[0].ann)) (loc := loc) - | _, _ => pure () - | none => pure () - | _ => pure () - -- subject >= N / subject <= N (type-checked: int or float) - match e with + specWarning loc s!"isinstance: unsupported type argument" + return placeholder + specWarning loc s!"unsupported call: {funcName}(...)" + return placeholder + -- compile("pattern").search(subject) — translate to regexMatch + | .Call _ (.Attribute _ (.Call _ (.Name _ ⟨_, compileName⟩ (.Load _)) ⟨_, compileArgs⟩ _) + ⟨_, searchAttr⟩ (.Load _)) ⟨_, searchArgs⟩ _ => + if compileName == "compile" && searchAttr == "search" then + if h₃ : compileArgs.size = 1 then + if h₄ : searchArgs.size = 1 then + match compileArgs[0] with + | .Constant _ (.ConString _ ⟨_, pattern⟩) _ => + let (clean, (subjExpr, _)) ← runNoWarn (transExpr searchArgs[0]) + if clean then + return (.regexMatch subjExpr pattern (loc := loc), boolType) + | _ => pure () + specWarning loc s!"unsupported method call expression" + return placeholder + -- Compare: "key" in container, then dispatch to transCompare | .Compare _ lhs ⟨_, ops⟩ ⟨_, comparators⟩ => + -- "key" in container if h₁ : ops.size = 1 then if h₂ : comparators.size = 1 then - match ← extractSubject lhs with - | some subj => - let subjType ← inferExprType lhs - let isFloat := subjType.any isFloatType - let isInt := subjType.any isIntType - let cmp ← match ops[0] with - | .GtE _ => makeComparison (.floatGe · · (loc := loc)) (.intGe · · (loc := loc)) isFloat isInt subj comparators[0] - | .LtE _ => makeComparison (.floatLe · · (loc := loc)) (.intLe · · (loc := loc)) isFloat isInt subj comparators[0] - | _ => pure none - match cmp with - | some expr => return expr - | none => pure () - | none => pure () - | _ => pure () - -- subject == "A" or subject == "B" or ... - match ← collectEnumValues e with - | some (subj, vals) => - return .enumMember subj vals (loc := loc) - | none => pure () - -- compile("pattern").search(subject) is not None - match e with - | .Compare _ - (.Call _ (.Attribute _ (.Call _ (.Name _ ⟨_, compileName⟩ (.Load _)) ⟨_, compileArgs⟩ _) - ⟨_, searchAttr⟩ (.Load _)) ⟨_, searchArgs⟩ _) - ⟨_, ops⟩ ⟨_, comparators⟩ => - if compileName == "compile" && searchAttr == "search" then - if h₁ : compileArgs.size = 1 then - if h₂ : searchArgs.size = 1 then - if h₃ : ops.size = 1 then - if h₄ : comparators.size = 1 then - match compileArgs[0] with - | .Constant _ (.ConString _ ⟨_, pattern⟩) _ => - match ← extractSubject searchArgs[0] with - | some subj => - match ops[0], comparators[0] with - | .IsNot _, .Constant _ (.ConNone _) _ => - return .regexMatch subj pattern (loc := loc) - | _, _ => pure () - | none => pure () - | _ => pure () - | _ => pure () - -- Fallback: unrecognized pattern - specWarning e.ann s!"unrecognized assert pattern: {eformat e.toAst}" - return .placeholder (loc := loc) + match ops[0], lhs with + | .In _, .Constant _ (.ConString _ ⟨_, key⟩) _ => + let (clean, (containerExpr, _)) ← runNoWarn (transExpr comparators[0]) + if clean then + return (.containsKey containerExpr key (loc := loc), boolType) + | _, _ => pure () + let (clean, (lhsExpr, lhsType)) ← runNoWarn (transExpr lhs) + if clean then + match ← transCompare loc lhsExpr lhsType ops comparators transExpr with + | some expr => return (expr, boolType) + | none => pure () + specWarning loc s!"unsupported comparison: {eformat e.toAst}" + return placeholder + -- BoolOp(Or): try to merge enum values + | .BoolOp _ (.Or _) ⟨_, values⟩ => + let (clean, branches) ← runNoWarn <| + values.attach.mapM fun ⟨v, _⟩ => transExpr v + if clean then + let merged := branches.foldl (init := none) fun acc (branch, _) => + match branch with + | .enumMember subj vals _ => + match acc with + | none => some (subj, vals) + | some (prevSubj, prevVals) => + if prevSubj.softBEq subj then + some (prevSubj, prevVals ++ vals) + else + none + | _ => none + if let some (subj, vals) := merged then + return (.enumMember subj vals (loc := loc), boolType) + specWarning loc s!"unsupported or-expression: {eformat e.toAst}" + return placeholder + | _ => + specWarning loc s!"unsupported expression: {eformat e.toAst}" + return placeholder mutual @@ -1074,15 +1009,19 @@ def blockStmt (s : stmt SourceRange) : SpecAssertionM Unit := do | .Expr .. => specWarning s.ann "skipped Expr in function body" | .Assert _ test msg => - let formula ← transAssertExpr test + let (clean, (formula, _)) ← runNoWarn (transExpr test) + if !clean then pure () + else let message ← match msg with | ⟨_, some (.Constant _ (.ConString _ ⟨_, str⟩) _)⟩ => pure #[MessagePart.str str] | ⟨_, some (.JoinedStr _ ⟨_, values⟩)⟩ => values.attach.mapM fun ⟨v, _⟩ => match v with | .Constant _ (.ConString _ ⟨_, str⟩) _ => pure (MessagePart.str str) - | .FormattedValue _ value _ _ => - MessagePart.expr <$> transMessageExpr value + | .FormattedValue _ value _ _ => do + let (clean, (expr, _)) ← runNoWarn (transExpr value) + if clean then return MessagePart.expr expr + else return MessagePart.str "" | other => do specWarning other.ann "unsupported f-string part" pure (MessagePart.str "") @@ -1107,24 +1046,21 @@ def blockStmt (s : stmt SourceRange) : SpecAssertionM Unit := do match target, iter with -- for varName in iterable: | .Name _ ⟨_, varName⟩ (.Store _), _ => - match ← extractSubject iter with - | some listExpr => - let elemTp ← do - match ← inferExprType iter with - | some iterTp => pure (extractElementType iterTp) - | none => pure none - let prevAssertions := (←get).assertions - modify fun s => { s with assertions := #[] } - withReader (fun ctx => match elemTp with - | some tp => { ctx with localTypes := ctx.localTypes.insert varName tp } - | none => ctx) <| - blockStmts body - let bodyAssertions := (←get).assertions - let wrapped := bodyAssertions.map fun a => - { a with formula := .forallList listExpr varName a.formula s.ann } - modify fun s => { s with assertions := prevAssertions ++ wrapped } - | none => - specWarning s.ann s!"For: cannot extract iterable expression" + let (clean, (listExpr, iterTp)) ← runNoWarn (transExpr iter) + if not clean then + return () + let some elemTp := iterTp.extractElementType + | specWarning s.ann "For: iterable is not a List/Sequence type" + return + let prevAssertions := (←get).assertions + modify fun s => { s with assertions := #[] } + withReader (fun ctx => + { ctx with localTypes := ctx.localTypes.insert varName elemTp }) <| + blockStmts body + let bodyAssertions := (←get).assertions + let wrapped := bodyAssertions.map fun a => + { a with formula := .forallList listExpr varName a.formula s.ann } + modify fun s => { s with assertions := prevAssertions ++ wrapped } -- for keyVar, valVar in dictExpr.items(): | .Tuple _ ⟨_, elts⟩ (.Store _), .Call _ (.Attribute _ dictExpr ⟨_, "items"⟩ (.Load _)) ⟨_, args⟩ _ => @@ -1135,57 +1071,40 @@ def blockStmt (s : stmt SourceRange) : SpecAssertionM Unit := do else match elts[0]!, elts[1]! with | .Name _ ⟨_, keyVar⟩ (.Store _), .Name _ ⟨_, valVar⟩ (.Store _) => - match ← extractSubject dictExpr with - | some dictSubj => - -- Type-check: verify the subject is actually a dict type - let kvTypes ← do - match ← inferExprType dictExpr with - | some dictTp => - match extractDictKeyValueTypes dictTp with - | some kv => pure (some kv) - | none => - specWarning s.ann - s!"For: .items() subject is not a Dict/Mapping type" - pure none - | none => - specWarning s.ann - s!"For: cannot infer type of .items() subject" - pure none - let prevAssertions := (←get).assertions - modify fun st => { st with assertions := #[] } - withReader (fun ctx => match kvTypes with - | some (kTp, vTp) => - { ctx with localTypes := ctx.localTypes - |>.insert keyVar kTp - |>.insert valVar vTp } - | none => ctx) <| - blockStmts body - let bodyAssertions := (←get).assertions - let wrapped := bodyAssertions.map fun a => - { a with formula := .forallDict dictSubj keyVar valVar a.formula s.ann } - modify fun st => { st with assertions := prevAssertions ++ wrapped } - | none => - specWarning s.ann s!"For: cannot extract dict expression" + let (clean, (dictSubj, dictTp)) ← runNoWarn (transExpr dictExpr) + if not clean then + return () + let some (kTp, vTp) := dictTp.extractDictKeyValueTypes + | specWarning s.ann s!"For: .items() subject is not a Dict/Mapping type" + return + let prevAssertions := (←get).assertions + modify fun st => { st with assertions := #[] } + withReader (fun ctx => { ctx with + localTypes := ctx.localTypes |>.insert keyVar kTp |>.insert valVar vTp + }) + (blockStmts body) + let bodyAssertions := (←get).assertions + let wrapped := bodyAssertions.map fun a => + { a with formula := .forallDict dictSubj keyVar valVar a.formula s.ann } + modify fun st => { st with assertions := prevAssertions ++ wrapped } | _, _ => specWarning s.ann "For: dict unpacking requires Name targets" | _, _ => specWarning s.ann "For: unsupported target pattern" | .If _ pred ⟨_, t⟩ ⟨_, f⟩ => - let cond ← transCondition pred - if cond.isNone then - specWarning pred.ann s!"if: unrecognized condition pattern: {eformat pred.toAst}" - assumeCondition cond pred.ann <| blockStmts t - if f.size > 0 then - assumeCondition (cond.map (.not · pred.ann)) pred.ann <| blockStmts f + let (clean, (cond, _)) ← runNoWarn (transExpr pred) + if clean then + assumeCondition cond pred.ann <| blockStmts t + if f.size > 0 then + assumeCondition (.not cond pred.ann) pred.ann <| blockStmts f + else + blockStmts t + if f.size > 0 then + blockStmts f | .Pass _ => pure () | _ => specError s.ann s!"Unsupported statement: {eformat s.toAst}" termination_by sizeOf s -decreasing_by - · cases body; decreasing_tactic - · cases body; decreasing_tactic - · cases t; decreasing_tactic - · cases f; decreasing_tactic def blockStmts (as : Array (stmt SourceRange)) : SpecAssertionM Unit := do as.attach.forM fun ⟨b, _⟩ => blockStmt b @@ -1204,8 +1123,7 @@ def collectAssertions (decls : ArgDecls) (_returnType : SpecType) let filePath := (←read).pythonFile let ctx : SpecAssertionContext := { filePath - kwargsParamName := decls.kwargs.map Prod.fst - kwargsType := decls.kwargs.map Prod.snd } + kwargs := decls.kwargs } let ((), as) := action ctx { errors, warnings } modify fun s => { s with errors := as.errors, warnings := as.warnings } pure as @@ -1320,10 +1238,10 @@ private def resolveBaseClasses (bases : Array (expr SourceRange)) else match ← getNameValue? name with | some (.typeValue tp) => - match tp.asSingleton with - | some (.ident pyIdent _) => + match tp.asIdent with + | some pyIdent => result := result.push pyIdent - | _ => + | none => specError base.ann s!"Unknown base class '{name}'" | _ => specError base.ann s!"Unknown base class '{name}'" @@ -1351,8 +1269,7 @@ partial def pySpecClassBody (loc : SourceRange) (className : String) | .ClassDef innerLoc ⟨_, innerClassName⟩ ⟨_, innerBases⟩ _keywords ⟨_, innerStmts⟩ _decorators _typeParams => let innerBaseIdents ← resolveBaseClasses innerBases - let innerDef ← pySpecClassBody innerLoc innerClassName - innerBaseIdents innerStmts + let innerDef ← pySpecClassBody innerLoc innerClassName innerBaseIdents innerStmts subclasses := subclasses.push innerDef | .Assign _ ⟨_, targets⟩ value _typeAnn => if h : targets.size = 1 then diff --git a/Strata/Languages/Python/Specs/DDM.lean b/Strata/Languages/Python/Specs/DDM.lean index 2fa17b69a3..85d588a449 100644 --- a/Strata/Languages/Python/Specs/DDM.lean +++ b/Strata/Languages/Python/Specs/DDM.lean @@ -78,7 +78,7 @@ op getIndexExpr(subject : SpecExprDecl, field : Ident) : SpecExprDecl => @[prec(50)] subject "[" field "]"; op isInstanceOfExpr(subject : SpecExprDecl, typeName : Str) : SpecExprDecl => "isinstance" "(" subject ", " typeName ")"; -op lenExpr(subject : SpecExprDecl) : SpecExprDecl => +op stringLenExpr(subject : SpecExprDecl) : SpecExprDecl => "len" "(" subject ")"; op intExpr(value : Int) : SpecExprDecl => value; op intGeExpr(subject : SpecExprDecl, bound : SpecExprDecl) : SpecExprDecl => @@ -223,10 +223,10 @@ private def SpecType.toDDM (d : SpecType) : DDM.SpecType SourceRange := .typeUnion d.loc ⟨.none, d.atoms.map (·.toDDM)⟩ termination_by sizeOf d decreasing_by - all_goals { - cases d - decreasing_tactic - } + · have mem : d.atoms[0] ∈ d.atoms := by grind + exact SpecType.sizeOf_atom_lt_of_mem mem + · rename_i a mem + exact SpecType.sizeOf_atom_lt_of_mem mem end @@ -242,7 +242,7 @@ protected def SpecExpr.toDDM (e : SpecExpr) : DDM.SpecExprDecl SourceRange := | .var name loc => .varExpr loc ⟨loc, name⟩ | .getIndex subj field loc => .getIndexExpr loc subj.toDDM ⟨loc, field⟩ | .isInstanceOf subj tn loc => .isInstanceOfExpr loc subj.toDDM ⟨loc, tn⟩ - | .len subj loc => .lenExpr loc subj.toDDM + | .stringLen subj loc => .stringLenExpr loc subj.toDDM | .intLit v loc => .intExpr loc (toDDMInt loc v) | .intGe subj bound loc => .intGeExpr loc subj.toDDM bound.toDDM | .intLe subj bound loc => .intLeExpr loc subj.toDDM bound.toDDM @@ -341,13 +341,13 @@ private def DDM.SpecType.fromDDM (d : DDM.SpecType SourceRange) : Specs.SpecType .ident loc pyIdent a else panic! "Bad identifier" - | .typeIntLiteral loc i => .ofAtom loc <| .intLiteral i.ofDDM - | .typeStringLiteral loc ⟨_, s⟩ => .ofAtom loc <| .stringLiteral s + | .typeIntLiteral loc i => .intLiteral loc i.ofDDM + | .typeStringLiteral loc ⟨_, s⟩ => .stringLiteral loc s | .typeTypedDict loc ⟨_, fields⟩ => let names := fields.map fun (.mkDictFieldDecl _ ⟨_, name⟩ _ _) => name let types := fields.attach.map fun ⟨.mkDictFieldDecl _ _ tp _, mem⟩ => tp.fromDDM let required := fields.map fun (.mkDictFieldDecl _ _ _ ⟨_, r⟩) => r - .ofAtom loc <| .typedDict names types required + .typedDict loc names types required | .typeUnion loc ⟨_, args⟩ => if p : args.size > 0 then args.attach.foldl (init := args[0].fromDDM) (start := 1) @@ -382,7 +382,7 @@ private def DDM.SpecExprDecl.fromDDM (d : DDM.SpecExprDecl SourceRange) : Specs. | .varExpr loc ⟨_, name⟩ => .var name loc | .getIndexExpr loc subj ⟨_, field⟩ => .getIndex subj.fromDDM field loc | .isInstanceOfExpr loc subj ⟨_, tn⟩ => .isInstanceOf subj.fromDDM tn loc - | .lenExpr loc subj => .len subj.fromDDM loc + | .stringLenExpr loc subj => .stringLen subj.fromDDM loc | .intExpr loc i => .intLit i.ofDDM loc | .intGeExpr loc subj bound => .intGe subj.fromDDM bound.fromDDM loc | .intLeExpr loc subj bound => .intLe subj.fromDDM bound.fromDDM loc diff --git a/Strata/Languages/Python/Specs/Decls.lean b/Strata/Languages/Python/Specs/Decls.lean index 728e59923f..1c36374542 100644 --- a/Strata/Languages/Python/Specs/Decls.lean +++ b/Strata/Languages/Python/Specs/Decls.lean @@ -97,6 +97,7 @@ deriving Inhabited, Repr A PySpec type is a union of atom types. -/ structure SpecType where + private mk :: atoms : Array SpecAtomType /-- Source location of this type. May be `.none` for builtin types. -/ loc : SourceRange @@ -163,6 +164,45 @@ decreasing_by end +namespace SpecType + +theorem sizeOf_atom_lt_of_mem {a : SpecAtomType} {tp : SpecType} + (h : a ∈ tp.atoms) : sizeOf a < sizeOf tp := by + cases tp + decreasing_tactic + +end SpecType + +mutual + +protected def SpecAtomType.toString : SpecAtomType → String + | .ident nm args => + if args.size == 0 then s!"{nm}" + else s!"{nm}[{", ".intercalate (args.map (fun a => a.toString) |>.toList)}]" + | .intLiteral v => s!"Literal[{v}]" + | .stringLiteral v => s!"Literal[\"{v}\"]" + | .typedDict fields _ _ => s!"TypedDict({", ".intercalate fields.toList})" +termination_by tp => sizeOf tp +decreasing_by + · rename_i mem + decreasing_tactic + +protected def SpecType.toString (tp : SpecType) : String := + if h : tp.atoms.size = 1 then + tp.atoms[0].toString + else + s!"Union[{", ".intercalate (tp.atoms.map (fun a => a.toString) |>.toList)}]" +termination_by sizeOf tp +decreasing_by + · have mem : tp.atoms[0] ∈ tp.atoms := by grind + exact SpecType.sizeOf_atom_lt_of_mem mem + · rename_i mem + exact SpecType.sizeOf_atom_lt_of_mem mem +end + +instance : ToString SpecAtomType where toString := SpecAtomType.toString +instance : ToString SpecType where toString := SpecType.toString + instance : BEq SpecAtomType where beq x y := SpecAtomType.compare x y == .eq @@ -205,9 +245,9 @@ private partial def unionAux (x y : Array SpecAtomType) (i : Fin x.size) (j : Fi if yjp : j' < y.size then unionAux x y ⟨i', xip⟩ ⟨j', yjp⟩ (r.push xe) else - r.push xe ++ x.drop i' + r ++ x.drop i else - r.push xe ++ y.drop j + r ++ y.drop j | .gt => let j' := j.val + 1 if yjp : j' < y.size then @@ -215,17 +255,21 @@ private partial def unionAux (x y : Array SpecAtomType) (i : Fin x.size) (j : Fi else r.push ye ++ x.drop i.val -/-- Union two SpecTypes with a specified location for the result -/ -def union (loc : SourceRange) (x y : SpecType) : SpecType := - if xp : 0 < x.atoms.size then - if yp : 0 < y.atoms.size then - { loc := loc, atoms := unionAux x.atoms y.atoms ⟨0, xp⟩ ⟨0, yp⟩ #[] } +private partial def unionElts (x y : Array SpecAtomType) : Array SpecAtomType := + if xp : 0 < x.size then + if yp : 0 < y.size then + unionAux x y ⟨0, xp⟩ ⟨0, yp⟩ #[] else x else y -def ofAtom (loc : SourceRange) (atom : SpecAtomType) : SpecType := { loc := loc, atoms := #[atom] } + +/-- Union two SpecTypes with a specified location for the result -/ +def union (loc : SourceRange) (x y : SpecType) : SpecType := + { loc := loc, atoms := unionElts x.atoms y.atoms } + +private def ofAtom (loc : SourceRange) (atom : SpecAtomType) : SpecType := { loc := loc, atoms := #[atom] } @[specialize] private def removeAdjDupsAux {α} [BEq α] (a : Array α) (i : Nat) (r : Array α) (rne : r.size > 0) : Array α := @@ -249,19 +293,100 @@ private def removeAdjDups {α} [BEq α] (a : Array α) : Array α := /-- Construct a `SpecType` from an array of atoms by sorting and removing duplicates to produce a canonical representation. -/ -protected def ofArray (loc : SourceRange) (atoms : Array SpecAtomType) : SpecType := +private def ofArray (loc : SourceRange) (atoms : Array SpecAtomType) : SpecType := let elts := atoms.qsort (compare · · == .lt) { loc := loc, atoms := removeAdjDups elts } def ident (loc : SourceRange) (i : PythonIdent) (args : Array SpecType := #[]) : SpecType := ofAtom loc (.ident i args) -def asSingleton (tp : SpecType) : Option SpecAtomType := do - if tp.atoms.size = 1 then - for atp in tp.atoms do return atp - none +def noneType (loc : SourceRange) : SpecType := + ofAtom loc .noneType + +def intLiteral (loc : SourceRange) (value : Int) : SpecType := + ofAtom loc (.intLiteral value) + +def stringLiteral (loc : SourceRange) (value : String) : SpecType := + ofAtom loc (.stringLiteral value) -def isAtom (tp : SpecType) (atp : SpecAtomType) : Bool := tp.asSingleton.any (· == atp) +def typedDict (loc : SourceRange) (fields : Array String) + (fieldTypes : Array SpecType) (fieldRequired : Array Bool) : SpecType := + ofAtom loc (.typedDict fields fieldTypes fieldRequired) + +def unionArray (loc : SourceRange) (elts : Array SpecType) : SpecType := + { loc := loc, atoms := elts.foldl (init := #[]) (unionElts · ·.atoms) } + +private def asSingleton (tp : SpecType) : Option SpecAtomType := do + if h : tp.atoms.size = 1 then + some tp.atoms[0] + else + none + +def asIdent (tp : SpecType) : Option PythonIdent := do + let atom ← tp.asSingleton + match atom with + | .ident id #[] => some id + | _ => none + +def isIntType (tp : SpecType) : Bool := tp.asIdent == some .builtinsInt + +def isFloatType (tp : SpecType) : Bool := tp.asIdent == some .builtinsFloat + +def isStringType (tp : SpecType) : Bool := tp.asIdent == some .builtinsStr + +def isBoolType (tp : SpecType) : Bool := tp.asIdent == some .builtinsBool + +def isTypedDict (tp : SpecType) : Bool := + match tp.asSingleton with + | some (.typedDict ..) => true + | _ => false + +def lookupTypedDictField (tp : SpecType) (field : String) : Option SpecType := do + let atom ← tp.asSingleton + match atom with + | .typedDict fields fieldTypes _ => + for i in [:fields.size] do + if fields[i]! == field then return fieldTypes[i]! + none + | _ => none + +def extractElementType (tp : SpecType) : Option SpecType := do + let atom ← tp.asSingleton + match atom with + | .ident pyId args => + if (pyId == .typingList || pyId == .typingSequence) && args.size == 1 then + return args[0]! + none + | _ => none + +def extractDictKeyValueTypes (tp : SpecType) : Option (SpecType × SpecType) := do + let atom ← tp.asSingleton + match atom with + | .ident pyId args => + if (pyId == .typingDict || pyId == .typingMapping) && args.size == 2 then + return (args[0]!, args[1]!) + none + | _ => none + +def asStringLiteral (tp : SpecType) : Option String := do + let atom ← tp.asSingleton + match atom with + | .stringLiteral v => some v + | _ => none + +structure DictField where + name : String + type : SpecType + required : Bool +deriving Inhabited + +def asTypedDict (tp : SpecType) : Option (Array DictField) := do + let atom ← tp.asSingleton + match atom with + | .typedDict fields fieldTypes fieldRequired => + some <| fields.mapIdx fun i name => + { name, type := fieldTypes.getD i default, required := fieldRequired.getD i true } + | _ => none end SpecType @@ -304,7 +429,9 @@ inductive SpecExpr where | var (name : String) (loc : SourceRange) | getIndex (subject : SpecExpr) (field : String) (loc : SourceRange) | isInstanceOf (subject : SpecExpr) (typeName : String) (loc : SourceRange) -| len (subject : SpecExpr) (loc : SourceRange) +/-- `stringLen subject` represents `len(subject)` where `subject` is a string. + Used in preconditions like `assert len(name) >= 1`. -/ +| stringLen (subject : SpecExpr) (loc : SourceRange) | intLit (value : Int) (loc : SourceRange) | intGe (subject : SpecExpr) (bound : SpecExpr) (loc : SourceRange) | intLe (subject : SpecExpr) (bound : SpecExpr) (loc : SourceRange) @@ -335,6 +462,30 @@ inductive SpecExpr where | forallDict (dict : SpecExpr) (keyVar : String) (valVar : String) (body : SpecExpr) (loc : SourceRange) deriving Inhabited +/-- Structural equality ignoring source locations. -/ +def SpecExpr.softBEq : SpecExpr → SpecExpr → Bool + | .placeholder _, .placeholder _ => true + | .var n₁ _, .var n₂ _ => n₁ == n₂ + | .getIndex s₁ f₁ _, .getIndex s₂ f₂ _ => s₁.softBEq s₂ && f₁ == f₂ + | .isInstanceOf s₁ t₁ _, .isInstanceOf s₂ t₂ _ => s₁.softBEq s₂ && t₁ == t₂ + | .stringLen s₁ _, .stringLen s₂ _ => s₁.softBEq s₂ + | .intLit v₁ _, .intLit v₂ _ => v₁ == v₂ + | .intGe s₁ b₁ _, .intGe s₂ b₂ _ => s₁.softBEq s₂ && b₁.softBEq b₂ + | .intLe s₁ b₁ _, .intLe s₂ b₂ _ => s₁.softBEq s₂ && b₁.softBEq b₂ + | .floatLit v₁ _, .floatLit v₂ _ => v₁ == v₂ + | .floatGe s₁ b₁ _, .floatGe s₂ b₂ _ => s₁.softBEq s₂ && b₁.softBEq b₂ + | .floatLe s₁ b₁ _, .floatLe s₂ b₂ _ => s₁.softBEq s₂ && b₁.softBEq b₂ + | .enumMember s₁ v₁ _, .enumMember s₂ v₂ _ => s₁.softBEq s₂ && v₁ == v₂ + | .regexMatch s₁ p₁ _, .regexMatch s₂ p₂ _ => s₁.softBEq s₂ && p₁ == p₂ + | .containsKey c₁ k₁ _, .containsKey c₂ k₂ _ => c₁.softBEq c₂ && k₁ == k₂ + | .implies c₁ b₁ _, .implies c₂ b₂ _ => c₁.softBEq c₂ && b₁.softBEq b₂ + | .not e₁ _, .not e₂ _ => e₁.softBEq e₂ + | .forallList l₁ v₁ b₁ _, .forallList l₂ v₂ b₂ _ => + l₁.softBEq l₂ && v₁ == v₂ && b₁.softBEq b₂ + | .forallDict d₁ k₁ v₁ b₁ _, .forallDict d₂ k₂ v₂ b₂ _ => + d₁.softBEq d₂ && k₁ == k₂ && v₁ == v₂ && b₁.softBEq b₂ + | _, _ => false + inductive MessagePart where | str (s : String) | expr (e : SpecExpr) diff --git a/Strata/Languages/Python/Specs/Error.lean b/Strata/Languages/Python/Specs/Error.lean index 3363aeb652..4894a24b0a 100644 --- a/Strata/Languages/Python/Specs/Error.lean +++ b/Strata/Languages/Python/Specs/Error.lean @@ -27,7 +27,6 @@ instance (a b : WarningKind) : Decidable (a < b) := namespace WarningKind -- Type translation warnings -def emptyType : WarningKind := { phase := "pySpecToLaurel", category := "emptyType" } def unsupportedUnion : WarningKind := { phase := "pySpecToLaurel", category := "unsupportedUnion" } -- Unsupported Optional patterns diff --git a/Strata/Languages/Python/Specs/ToLaurel.lean b/Strata/Languages/Python/Specs/ToLaurel.lean index 75690ef32c..217a5fd053 100644 --- a/Strata/Languages/Python/Specs/ToLaurel.lean +++ b/Strata/Languages/Python/Specs/ToLaurel.lean @@ -34,17 +34,26 @@ namespace Strata.Python public section -/-- Map a PythonIdent to its PyLauType string name, if it's a recognized - builtin type. This is the single source of truth for which Python builtins - map to PyLauType names. Used by PySpecPipeline (for type extraction). - Keep in sync with `pyLauTypesWithTesters` in PythonToLaurel.lean. -/ -def PythonIdent.toPyLauType? (id : PythonIdent) : Option String := - if id == PythonIdent.builtinsInt then some "int" - else if id == PythonIdent.builtinsStr then some "str" - else if id == PythonIdent.builtinsBool then some "bool" - else if id == PythonIdent.builtinsFloat then some "float" - else if id == PythonIdent.noneType then some "None" - else none +private def typeTestersMap : Std.HashMap PythonIdent String := + .ofList [ + (.builtinsInt, "Any..isfrom_int"), + (.builtinsStr, "Any..isfrom_str"), + (.builtinsBool, "Any..isfrom_bool"), + (.builtinsFloat, "Any..isfrom_float"), + (.noneType, "Any..isfrom_None"), + (.builtinsBytes, "Any..isfrom_bytes"), + (.typingList, "Any..isfrom_ListAny"), + (.typingSequence, "Any..isfrom_ListAny"), + (.typingDict, "Any..isfrom_DictStrAny"), + (.typingMapping, "Any..isfrom_DictStrAny"), + (.builtinsException, "Any..isexception") + ] + +/-- Fully qualified Laurel name for a `PythonIdent`: module dots become + underscores. E.g., `"mylib.sub"` / `"Foo"` → `"mylib_sub_Foo"`. -/ +def PythonIdent.toLaurelName (id : PythonIdent) : String := + let pfx := "_".intercalate (id.pythonModule.splitOn ".") + if pfx.isEmpty then id.name else pfx ++ "_" ++ id.name end -- public section end Strata.Python @@ -129,192 +138,82 @@ private def mkTy (ty : HighType) : HighTypeMd := private def mkUserDefined (s : String) : HighTypeMd := { val := .UserDefined (mkId s), source := none } -/-- Placeholder for types not yet supported in CorePrelude. - Returns TString so translation can proceed. Callers should - report a warning via `reportError` so the gap is visible. -/ -private def unsupportedType : HighTypeMd := - { val := .TString, source := none } - -/-! ### Laurel type constants - -Named constants for Laurel `HighTypeMd` values used in type translation. -Prelude types (`Any`, `Error`, `DictStrAny`, etc.) use `UserDefined` so -they participate in Laurel resolution. -/ - -private def tyBool : HighTypeMd := mkTy .TBool -private def tyInt : HighTypeMd := mkTy .TInt -private def tyReal : HighTypeMd := mkTy .TReal -private def tyString : HighTypeMd := mkTy .TString -private def tyVoid : HighTypeMd := mkTy .TVoid +/-! ### Laurel type constants -/ private def tyAny : HighTypeMd := mkUserDefined "Any" private def tyDictStrAny : HighTypeMd := mkUserDefined "DictStrAny" -private def tyError : HighTypeMd := mkUserDefined "Error" -private def tyListStr : HighTypeMd := mkUserDefined "ListStr" -private def tyStrOrNone : HighTypeMd := mkUserDefined "StrOrNone" -private def tyIntOrNone : HighTypeMd := mkUserDefined "IntOrNone" -private def tyBoolOrNone : HighTypeMd := mkUserDefined "BoolOrNone" - -mutual - -/-- Convert a SpecAtomType to a string for error messages. -/ -def atomTypeToString (a : SpecAtomType) : String := - match a with - | .ident nm args => - if nm == PythonIdent.noneType && args.isEmpty then "None" - else if args.isEmpty then toString nm - else - let argStrs := args.map specTypeToString - s!"{nm}[{String.intercalate ", " argStrs.toList}]" - | .intLiteral v => s!"Literal[{v}]" - | .stringLiteral v => s!"Literal[\"{v}\"]" - | .typedDict _ _ _ => "TypedDict" -termination_by sizeOf a - -/-- Convert a SpecType to a string for error messages. -/ -def specTypeToString (t : SpecType) : String := - if h : t.atoms.size = 1 then - atomTypeToString t.atoms[0] - else - let strs := t.atoms.map atomTypeToString - String.intercalate " | " strs.toList -termination_by sizeOf t -decreasing_by - · cases t - decreasing_tactic - · cases t - decreasing_tactic - -end - -/-- Pretty print a union type. -/ -def formatUnionType (atoms : Array SpecAtomType) : String := - let strs := atoms.map atomTypeToString - String.intercalate " | " strs.toList /-! ## Type Translation -/ -/-- -Detect if a SpecType is a Union[None, T] pattern and return the appropriate Laurel type. -Handles: -- Union[None, str] → UserDefined "StrOrNone" -- Union[None, int] → UserDefined "IntOrNone" -- Union[None, bool] → UserDefined "BoolOrNone" -- Union[None, Literal["A"], ...] → UserDefined "StrOrNone" -- Union[None, Literal[1], ...] → UserDefined "IntOrNone" -- Union[None, TypedDict] → UserDefined "DictStrAny" -- Union[None, float/List/Dict/Any/bytes] → TString (unsupported, pending CorePrelude) --/ -def detectOptionalType (ty : SpecType) : ToLaurelM (Option HighTypeMd) := do - let isNoneType (atom : SpecAtomType) : Bool := - match atom with - | .ident nm args => nm == PythonIdent.noneType && args.isEmpty - | _ => false - - if !ty.atoms.any isNoneType then - return none - - let otherAtoms := ty.atoms.filter (fun a => !isNoneType a) - - -- All non-None string literals → StrOrNone - if otherAtoms.all (fun a => match a with | .stringLiteral _ => true | _ => false) then - return some tyStrOrNone - - -- All non-None int literals → IntOrNone - if otherAtoms.all (fun a => match a with | .intLiteral _ => true | _ => false) then - return some tyIntOrNone - - -- All non-None TypedDicts → DictStrAny - if otherAtoms.all (fun a => match a with | .typedDict _ _ _ => true | _ => false) then - return some tyDictStrAny - - if otherAtoms.size == 1 then - match otherAtoms[0]! with - | .ident nm _ => - if nm == PythonIdent.builtinsStr then return some tyStrOrNone - else if nm == PythonIdent.builtinsInt then return some tyIntOrNone - else if nm == PythonIdent.builtinsBool then return some tyBoolOrNone - -- TODO: add CorePrelude types for these Optional patterns - else if nm == PythonIdent.builtinsFloat then - reportError .unsupportedOptionalFloat default s!"Optional[float] mapped to TString" - return some unsupportedType - else if nm == PythonIdent.typingList then - reportError .unsupportedOptionalList default s!"Optional[List] mapped to TString" - return some unsupportedType - else if nm == PythonIdent.typingDict then - reportError .unsupportedOptionalDict default s!"Optional[Dict] mapped to TString" - return some unsupportedType - else if nm == PythonIdent.typingAny then - reportError .unsupportedOptionalAny default s!"Optional[Any] mapped to TString" - return some unsupportedType - else if nm == PythonIdent.builtinsBytes then - reportError .unsupportedOptionalBytes default s!"Optional[bytes] mapped to TString" - return some unsupportedType - else return none - | .typedDict _ _ _ => return some tyDictStrAny - | .intLiteral _ => return some tyIntOrNone - | _ => return none - else - return none - -/-- Known PythonIdent → Laurel type mappings for single-atom ident types. - Matches PythonToLaurel's type mapping: only int, str, bool, float get - concrete Laurel types; everything else maps to Any. -/ -private def knownIdentTypes : Std.HashMap PythonIdent HighTypeMd := +public def builtinIdents : Std.HashSet PythonIdent := .ofList [ - (.builtinsBool, tyBool), - (.builtinsBytearray, tyAny), - (.builtinsBytes, tyAny), - (.builtinsComplex, tyAny), - (.builtinsDict, tyAny), - (.builtinsException, tyAny), - (.builtinsFloat, tyReal), - (.builtinsInt, tyInt), - (.builtinsStr, tyString), - (.noneType, tyVoid), - (.typingAny, tyAny), - (.typingBinaryIO, tyAny), - (.typingDict, tyAny), - (.typingList, tyAny), + .builtinsBool, .builtinsBytearray, .builtinsBytes, .builtinsComplex, + .builtinsDict, .builtinsException, .builtinsFloat, .builtinsInt, + .builtinsStr, .noneType, .typingAny, .typingBinaryIO, .typingDict, + .typingList ] -/-- Convert a SpecType to a Laurel HighTypeMd. -/ +/-- Convert a SpecType to a Laurel HighTypeMd. + Composites → `UserDefined`, everything else → `Any`. -/ def specTypeToLaurelType (ty : SpecType) : ToLaurelM HighTypeMd := do - match ty.atoms.size with - | 0 => - reportError .emptyType default "Empty type (no atoms) encountered in Laurel conversion" - return tyString - | _ => - -- Check for union types - if ty.atoms.size > 1 then - -- All string literals → TString - if ty.atoms.all (fun a => match a with | .stringLiteral _ => true | _ => false) then - return tyString - -- All int literals → TInt - if ty.atoms.all (fun a => match a with | .intLiteral _ => true | _ => false) then - return tyInt - -- All TypedDicts → DictStrAny - if ty.atoms.all (fun a => match a with | .typedDict _ _ _ => true | _ => false) then - return tyDictStrAny - -- Check Union[None, T] patterns - match ← detectOptionalType ty with - | some laurelType => return laurelType - | none => - let unionStr := formatUnionType ty.atoms - reportError .unsupportedUnion default s!"Union type ({unionStr}) not yet supported in Laurel" - return tyString - else - pure () - -- Single atom type - match ty.atoms[0]! with - | .ident nm _args => - if let some ty := knownIdentTypes[nm]? then - return ty - let prefixed ← prefixName nm.name - return mkTy (.UserDefined { text := prefixed }) - | .intLiteral _ => return tyInt - | .stringLiteral _ => return tyString - | .typedDict _ _ _ => return tyDictStrAny + match ty.asIdent with + | some nm => + if nm ∈ builtinIdents then + return tyAny + return mkTy (.UserDefined { text := nm.toLaurelName }) + | none => return tyAny + +/-- Build the assertion for a single atom: type tester for idents, + `isfrom_X(v) && as_X!(v) == literal` for literals. + When `isUnion` is true, warns on ident atoms that lack testers. + Always warns on TypedDict (needs a dedicated checker). -/ +private def atomAssertion? (atom : SpecAtomType) (ty : SpecType) + (value : StmtExprMd) (source : Option FileRange) + (isUnion : Bool) : ToLaurelM (Option StmtExprMd) := do + let mk (e : StmtExpr) : StmtExprMd := { val := e, source := source } + match atom with + | .ident nm _ => + match typeTestersMap[nm]? with + | some testerName => + return some <| mk (.StaticCall (mkId testerName) [value]) + | none => + if nm != .typingAny && isUnion then + reportError .unsupportedUnion ty.loc s!"No type tester for '{nm}' in type '{ty}'" + return none + | .intLiteral v => + let typeCheck := mk (.StaticCall (mkId "Any..isfrom_int") [value]) + let unwrap := mk (.StaticCall (mkId "Any..as_int!") [value]) + let eqCheck := mk (.PrimitiveOp .Eq [unwrap, mk (.LiteralInt v)]) + return some <| mk (.PrimitiveOp .And [typeCheck, eqCheck]) + | .stringLiteral v => + let typeCheck := mk (.StaticCall (mkId "Any..isfrom_str") [value]) + let unwrap := mk (.StaticCall (mkId "Any..as_string!") [value]) + let eqCheck := mk (.PrimitiveOp .Eq [unwrap, mk (.LiteralString v)]) + return some <| mk (.PrimitiveOp .And [typeCheck, eqCheck]) + | .typedDict .. => + reportError .unsupportedUnion ty.loc s!"TypedDict '{atom}' approximated as DictStrAny in type '{ty}'" + return some <| mk (.StaticCall (mkId "Any..isfrom_DictStrAny") [value]) + +/-- Build a type-assertion expression for `value` given its declared `SpecType`. + Returns `none` when no assertion is needed (all atoms are Any/composites). + For union types, builds a disjunction over per-atom assertions. -/ +private def typeAssertion? (ty : SpecType) (value : StmtExprMd) + (source : Option FileRange) : ToLaurelM (Option StmtExprMd) := do + let mut result : Option StmtExprMd := none + for atom in ty.atoms do + match atom with + | .ident nm _ => + if nm = .typingAny then + return none + | _ => pure () + match ← atomAssertion? atom ty value source (ty.atoms.size > 1) with + | some call => + match result with + | none => result := some call + | some prev => + result := some { val := .PrimitiveOp .Or [prev, call], source := source } + | none => pure () + return result /-! ## SpecExpr to Laurel Translation -/ @@ -427,7 +326,7 @@ def specExprToLaurel (e : SpecExpr) (source : Option FileRange) | .isInstanceOf _ typeName loc => do reportError .isinstanceUnsupported loc s!"isinstance check for '{typeName}' not yet supported in preconditions" return default - | .len subject loc => do + | .stringLen subject loc => do let src ← nodeSource loc let s ← asAny loc <| specExprToLaurel subject src return .mkSome <| .fromInt (.strLength (.anyAsString s)) @@ -471,7 +370,6 @@ def specExprToLaurel (e : SpecExpr) (source : Option FileRange) let src ← nodeSource loc match container with | .var "kwargs" .. => - -- FIXME: Check this. We may want to move this up let keyAny ← asAny loc <| lookupIdentifier key loc src return .mkSome <| .not (.anyIsfromNone keyAny) | _ => @@ -508,23 +406,43 @@ def SpecAssertMsg.render : SpecAssertMsg → String | .userAssertion text => text | .unnamed index => s!"precondition {index}" -/-- Build a procedure body that asserts preconditions. - Outputs are already initialized non-deterministically. -/ -def buildSpecBody (preconditions : Array Assertion) +/-- Build a Transparent procedure body with havoc, type assertions, + required-param checks, user preconditions, and return-type assumption. -/ +def buildSpecBody (allArgs : Array Arg) + (preconditions : Array Assertion) + (postconditions : Array SpecExpr) + (returnType : SpecType) (source : Option FileRange) (ctx : SpecExprContext) - (requiredParams : Array String := #[]) : ToLaurelM Body := do let fileSource ← mkFileSource let mut stmts : Array StmtExprMd := #[] + -- 1. Havoc the result: result := Hole(nondet) + let holeExpr : StmtExprMd := { val := .Hole (deterministic := false), source := source } + let resultId : StmtExprMd := { val := .Identifier (mkId "result"), source := source } + let assignStmt ← mkStmtWithLoc (.Assign [resultId] holeExpr) default + stmts := stmts.push assignStmt + -- 2. Assert type / required-param preconditions + for arg in allArgs do + let paramId : StmtExprMd := { val := .Identifier (mkId arg.name), source := source } + match ← typeAssertion? arg.type paramId source with + | some assertion => + if arg.default.isSome then + let noneCheck : StmtExprMd := { val := .StaticCall (mkId "Any..isfrom_None") [paramId], source := source } + let orExpr : StmtExprMd := { val := .PrimitiveOp .Or [noneCheck, assertion], source := source } + let assertStmt ← mkStmtWithLoc (.Assert { condition := orExpr, summary := none }) default + stmts := stmts.push assertStmt + else + let assertStmt ← mkStmtWithLoc (.Assert { condition := assertion, summary := none }) default + stmts := stmts.push assertStmt + | none => + if arg.default.isNone then + let cond : TypedStmtExpr _ := .not (.anyIsfromNone (.identifier arg.name Laurel.tyAny)) + let msg := SpecAssertMsg.requiredParam arg.name |>.render + let assertStmt ← mkStmtWithLoc (.Assert { condition := cond.stmt, summary := some msg }) default + stmts := stmts.push assertStmt + -- 3. Assert user pyspec preconditions let mut idx := 0 - -- Assert that required parameters are provided (not None) - for param in requiredParams do - let cond : TypedStmtExpr _ := .not (.anyIsfromNone (.identifier param Laurel.tyAny)) - let msg := SpecAssertMsg.requiredParam param |>.render - let assertStmt ← mkStmtWithLoc (.Assert { condition := cond.stmt, summary := some msg }) default - stmts := stmts.push assertStmt - idx := idx + 1 for assertion in preconditions do let formattedMsg := formatAssertionMessage assertion.message let msg := if formattedMsg.isEmpty @@ -539,6 +457,24 @@ def buildSpecBody (preconditions : Array Assertion) reportError .typeError default s!"Precondition expression is not Bool in '{ctx.procName}' (skipping): {msg}" idx := idx + 1 + -- 4. Assert user pyspec postconditions + for postExpr in postconditions do + let (⟨condType, condExpr⟩, success) ← runChecked <| specExprToLaurel postExpr source ctx + if success then + if let .TBool := condType then + let assumeStmt ← mkStmtWithLoc (.Assume condExpr.stmt) default + stmts := stmts.push assumeStmt + else + reportError .typeError default + s!"Postcondition expression is not Bool in '{ctx.procName}' (skipping)" + -- 5. Assume return type postcondition + -- NOTE. Skip NoneType: generated stubs currently declare `-> None` even for methods + -- that return values. Assuming isfrom_None would make callers unreachable. + if returnType.asIdent != some .noneType then + let resultRef : StmtExprMd := { val := .Identifier (mkId "result"), source := source } + if let some retAssertion ← typeAssertion? returnType resultRef source then + let assumeStmt ← mkStmtWithLoc (.Assume retAssertion) default + stmts := stmts.push assumeStmt let body := { val := .Block stmts.toList none, source := fileSource @@ -547,11 +483,6 @@ def buildSpecBody (preconditions : Array Assertion) /-! ## Declaration Translation -/ -/-- Convert an Arg to a Laurel Parameter. -/ -def argToParameter (arg : Arg) : ToLaurelM Parameter := do - let ty ← specTypeToLaurelType arg.type - return { name := arg.name, type := ty } - /-- Expand a `**kwargs: Unpack[TypedDict]` into individual `Arg` entries. Returns an error if kwargs is present but not a TypedDict. -/ public def expandKwargsArgs (kwargs : Option (String × SpecType)) @@ -559,13 +490,13 @@ public def expandKwargsArgs (kwargs : Option (String × SpecType)) match kwargs with | none => .ok #[] | some (name, specType) => - match specType.atoms.find? fun a => match a with | .typedDict .. => true | _ => false with - | some (.typedDict fields fieldTypes fieldRequired) => - .ok <| fields.mapIdx fun i name => - { name := name - type := fieldTypes.getD i default - default := if fieldRequired.getD i true then none else some .none } - | _ => .error s!"**{name} has non-TypedDict type; kwargs not expanded" + match specType.asTypedDict with + | some fields => + .ok <| fields.map fun f => + { name := f.name + type := f.type + default := if f.required then none else some .none } + | none => .error s!"**{name} has non-TypedDict type; kwargs not expanded" /-- Convert a function declaration to a Laurel Procedure. When `isMethod` is true, the first positional arg (`self`) is stripped. -/ @@ -580,30 +511,16 @@ def funcDeclToLaurel (procName : String) (func : FunctionDecl) | .ok args => pure args | .error msg => do reportError .kwargsExpansionError default msg; pure #[] let allArgs := posArgs ++ func.args.kwonly ++ kwargsArgs - let inputs ← allArgs.mapM argToParameter - let retType ← specTypeToLaurelType func.returnType - let outputs : List Parameter := - [{ name := "result", type := match retType.val with - | .TVoid => tyAny - | _ => retType }] - if func.postconditions.size > 0 then - reportError .postconditionUnsupported func.loc "Postconditions not yet supported" - -- When preconditions exist, use TCore "Any" for all parameters and outputs - -- to match the Python→Laurel pipeline's Any-wrapping convention. - let (inputs, outputs, body) ← - if func.preconditions.size > 0 then do - let anyTy : HighTypeMd := tyAny - let anyInputs := inputs.map fun p => { p with type := anyTy } - let anyOutputs := outputs.map fun p => { p with type := anyTy } - let argTypes := allArgs.foldl (init := {}) fun m a => - m.insert a.name Laurel.tyAny - let specCtx : SpecExprContext := { procName, argTypes } - let body ← buildSpecBody func.preconditions none specCtx - (requiredParams := allArgs.filterMap fun a => - if a.default.isNone then some a.name else none) - pure (anyInputs, anyOutputs, body) - else - pure (inputs, outputs, Body.Opaque [] none []) + let inputs ← allArgs.mapM fun a => do + let ty ← specTypeToLaurelType a.type + return ({ name := a.name, type := ty } : Parameter) + let outputs : List Parameter := [{ name := "result", type := tyAny }] + let argTypes : Std.HashMap String HighType := + inputs.foldl (init := ({} : Std.HashMap String HighType).insert "result" Laurel.tyAny) fun m p => + m.insert p.name.text p.type.val + let specCtx : SpecExprContext := { procName, argTypes } + let body ← buildSpecBody allArgs func.preconditions func.postconditions + func.returnType none specCtx let src ← mkSourceWithFileRange func.loc return { name := { text := procName, source := src } @@ -626,11 +543,8 @@ def classDefToLaurel (cls : ClassDef) : ToLaurelM Unit := do let laurelFields ← cls.fields.toList.mapM fun f => do let ty ← specTypeToLaurelType f.type pure { name := f.name, isMutable := true, type := ty : Laurel.Field } - let prefixedBases ← cls.bases.toList.mapM fun cd => do - -- Local bases (empty pythonModule) get prefixed; external ones don't - let baseName ← if cd.pythonModule.isEmpty then prefixName cd.name - else pure (toString cd) - return mkId baseName + let prefixedBases := cls.bases.toList.map fun cd => + mkId cd.toLaurelName pushType (.Composite { name := prefixedName extending := prefixedBases @@ -666,33 +580,23 @@ def extractOverloadEntry (func : FunctionDecl) : ToLaurelM Unit := do s!"Overloaded function '{func.name}' has no arguments" return let firstArgType := args[0].type - let .isTrue _ := decideProp (firstArgType.atoms.size = 1) - | reportError .overloadArgArity func.loc - s!"Overloaded function '{func.name}': first argument \ - has {firstArgType.atoms.size} type atoms, expected 1" - return let literalValue ← - match firstArgType.atoms[0] with - | .stringLiteral v => pure v - | _ => + match firstArgType.asStringLiteral with + | some v => pure v + | none => reportError .overloadArgNotStringLiteral func.loc s!"Overloaded function '{func.name}': first argument \ - type '{specTypeToString firstArgType}' is not a \ + type '{firstArgType}' is not a \ string literal (only string literal dispatch is \ currently supported)" return - let .isTrue _ := decideProp (func.returnType.atoms.size = 1) - | reportError .overloadReturnArity func.loc - s!"Overloaded function '{func.name}': return type \ - has {func.returnType.atoms.size} type atoms, expected 1" - return let retType ← - match func.returnType.atoms[0] with - | .ident nm _ => pure nm - | _ => + match func.returnType.asIdent with + | some nm => pure nm + | none => reportError .overloadReturnNotClass func.loc s!"Overloaded function '{func.name}': return type \ - '{specTypeToString func.returnType}' is not a \ + '{func.returnType}' is not a \ class type" return -- args[0].name is the formal parameter name from the PySpec (not a call-site argument) diff --git a/StrataTest/Languages/Python/PySpecArgTypeTest.lean b/StrataTest/Languages/Python/PySpecArgTypeTest.lean index 5fc487d65d..fdef9d4345 100644 --- a/StrataTest/Languages/Python/PySpecArgTypeTest.lean +++ b/StrataTest/Languages/Python/PySpecArgTypeTest.lean @@ -7,23 +7,23 @@ import Strata.Languages.Python.PySpecPipeline import Strata.Languages.Python.Specs.DDM -/-! ## Test: specArgToFuncDeclArg preserves original parameter types +/-! ## Test: specArgToFuncDeclArg preserves parameter type info -Verifies that `buildPySpecLaurel` extracts concrete type names from PySpec -`SpecType` atoms (builtins.str → "str", builtins.int → "int", etc.) -instead of hardcoding "Any" for all parameter types. +Verifies that `buildPySpecLaurel` populates `laurelType` on `PyArgInfo` +and that `typeTesters` is empty (type assertions live in the procedure body). -/ namespace Strata.Python.PySpecArgTypeTest open Strata.Python.Specs open Strata (buildPySpecLaurel) -open Strata.Python (OverloadTable PythonFunctionDecl PyArgInfo) +open Strata.Python (OverloadTable PythonFunctionDecl PyArgInfo highTypeToPyLauType) +open Strata.Laurel (Procedure formatProcedure) private def loc : SourceRange := default private def identType (nm : PythonIdent) : SpecType := - SpecType.ofAtom loc (.ident nm #[]) + SpecType.ident loc nm private def mkArg (name : String) (type : SpecType) : Specs.Arg := { name, type } @@ -38,24 +38,27 @@ private def mkFunc (name : String) (args : Array Specs.Arg) (ret : SpecType) : S } /-- Build PySpec signatures, write to temp Ion, read back via buildPySpecLaurel, - and return the extracted PythonFunctionDecl list. -/ -private def getFuncSigs (sigs : Array Signature) : IO (List PythonFunctionDecl) := do + and return the full result. -/ +private def buildSpecs (sigs : Array Signature) : IO Strata.PySpecLaurelResult := do IO.FS.withTempDir fun dir => do let ionFile := dir / "test.pyspec.ion" writeDDM ionFile sigs let result ← buildPySpecLaurel #[("", ionFile.toString)] {} |>.toBaseIO match result with - | .ok r => pure r.functionSignatures + | .ok r => pure r | .error msg => throw <| .userError msg -private def unionType (atoms : Array SpecAtomType) : SpecType := - { atoms, loc } +private def getFuncSigs (sigs : Array Signature) : IO (List PythonFunctionDecl) := do + return (← buildSpecs sigs).functionSignatures + +private def unionType (elts : Array SpecType) : SpecType := + SpecType.unionArray loc elts /-- -info: typed_func: x=[int], y=[str], z=[bool], w=[float] -untyped_func: a=[Any] -mixed_func: p=[str], q=[Any] -optional_func: s=[None, str], n=[None, int] +info: typed_func: x=Any[], y=Any[], z=Any[], w=Any[] +untyped_func: a=Any[] +mixed_func: p=Any[], q=Any[] +optional_func: s=Any[], n=Any[] -/ #guard_msgs in #eval do @@ -74,12 +77,37 @@ optional_func: s=[None, str], n=[None, int] mkArg "q" (identType .typingAny)] (identType .noneType), mkFunc "optional_func" - #[mkArg "s" (unionType #[.ident .noneType #[], .ident .builtinsStr #[]]), - mkArg "n" (unionType #[.ident .noneType #[], .ident .builtinsInt #[]])] + #[mkArg "s" (unionType #[identType .noneType, identType .builtinsStr]), + mkArg "n" (unionType #[identType .noneType, identType .builtinsInt])] (identType .noneType) ] for f in sigs do - let argStrs := ", ".intercalate (f.args.map fun (a : PyArgInfo) => s!"{a.name}={a.tys}") + let argStrs := ", ".intercalate (f.args.map fun (a : PyArgInfo) => + s!"{a.name}={highTypeToPyLauType a.laurelType.val}{a.typeTesters.toList}") IO.println s!"{f.name}: {argStrs}" +/-! ## Test: buildSpecBody generates type assertions in procedure body + +Verifies that even though `typeTesters` on `PythonFunctionDecl` is empty, +the pyspec Laurel procedure body contains the type assertions generated by +`buildSpecBody`. This confirms the body-side checks make call-site +preconditions redundant. -/ + +/-- +info: procedure typed_func(x: Any, y: Any): Any +{ result := ; assert Any..isfrom_int(x); assert Any..isfrom_str(y); assume Any..isfrom_float(result) }; +-/ +#guard_msgs in +#eval do + let result ← buildSpecs #[ + mkFunc "typed_func" + #[mkArg "x" (identType .builtinsInt), + mkArg "y" (identType .builtinsStr)] + (identType .builtinsFloat) + ] + let procs := result.laurelProgram.staticProcedures + let some proc := procs.find? (fun (p : Procedure) => p.name.text == "typed_func") + | throw <| IO.userError "typed_func not found" + IO.println (toString (formatProcedure proc)) + end Strata.Python.PySpecArgTypeTest diff --git a/StrataTest/Languages/Python/Specs/DeclsTest.lean b/StrataTest/Languages/Python/Specs/DeclsTest.lean index c2905daff7..e2d7edecb3 100644 --- a/StrataTest/Languages/Python/Specs/DeclsTest.lean +++ b/StrataTest/Languages/Python/Specs/DeclsTest.lean @@ -9,30 +9,8 @@ open Strata.Python.Specs namespace DeclsTest -private abbrev mk1 (a : SpecAtomType) : SpecType := ⟨#[a], ⟨0, 0⟩⟩ -private abbrev mk2 (a : SpecAtomType) : SpecType := ⟨#[a], ⟨⟨1⟩, ⟨2⟩⟩⟩ - --- Atom ordering: ident < intLiteral < stringLiteral < typedDict -#guard compare (SpecAtomType.ident .builtinsInt #[]) (.intLiteral 0) == .lt -#guard compare (SpecAtomType.intLiteral 0) (.stringLiteral "a") == .lt - --- Same variant compares by fields -#guard compare (SpecAtomType.intLiteral 1) (.intLiteral 2) == .lt -#guard compare (SpecAtomType.intLiteral 1) (.intLiteral 1) == .eq -#guard compare (SpecAtomType.stringLiteral "a") (.stringLiteral "b") == .lt - --- ident compares by PythonIdent then args -#guard compare (SpecAtomType.ident .builtinsBool #[]) (.ident .builtinsInt #[]) == .lt - --- SpecType comparison ignores loc -#guard compare (mk1 (.intLiteral 1)) (mk2 (.intLiteral 1)) == .eq --- BEq also ignores loc (consistent with Ord) -#guard mk1 (.intLiteral 1) == mk2 (.intLiteral 1) - --- SpecType compares by atoms content -#guard compare (mk1 (.intLiteral 1)) (mk1 (.intLiteral 2)) == .lt - --- ofArray deduplicates -#guard 1 == (SpecType.ofArray ⟨0, 0⟩ #[.intLiteral 0, .intLiteral 0] |>.atoms.size) +-- unionArray deduplicates +#guard (SpecType.unionArray default + #[SpecType.intLiteral ⟨0, 0⟩ 0, SpecType.intLiteral ⟨0, 0⟩ 0]).atoms.size == 1 end DeclsTest diff --git a/StrataTest/Languages/Python/ToLaurelTest.lean b/StrataTest/Languages/Python/ToLaurelTest.lean index 27c0e9213a..dacf9f58a6 100644 --- a/StrataTest/Languages/Python/ToLaurelTest.lean +++ b/StrataTest/Languages/Python/ToLaurelTest.lean @@ -20,17 +20,13 @@ private def assertEq [BEq α] [ToString α] (actual expected : α) : IO Unit := private def loc : SourceRange := default -private def mkType (atom : SpecAtomType) : SpecType := - SpecType.ofAtom default atom - -private def mkUnion (atoms : Array SpecAtomType) : SpecType := - { atoms := atoms, loc := default } +private def identType (nm : PythonIdent) : SpecType := + SpecType.ident default nm -private def identAtom (nm : PythonIdent) : SpecAtomType := - .ident nm #[] +private def noneType : SpecType := SpecType.noneType default -private def identType (nm : PythonIdent) : SpecType := - mkType (identAtom nm) +private def mkUnion (types : Array SpecType) : SpecType := + SpecType.unionArray default types private def mkArg (name : String) (type : SpecType) (default : Option SpecDefault := none) : Arg := { name, type, default := default } @@ -84,10 +80,12 @@ private def fmtTypeDef : TypeDefinition → String | .Datatype ty => s!"datatype {ty.name}" | .Alias ty => s!"alias {ty.name}" -/-- Run signaturesToLaurel and print formatted output. Asserts no errors. -/ +/-- Run signaturesToLaurel and print formatted output. + Prints warnings (if any) before procedures so `#guard_msgs` can verify them. -/ private def runTest (sigs : Array Signature) (modulePrefix : String := "") : IO Unit := do let result := signaturesToLaurel "" sigs modulePrefix - assert! result.errors.size = 0 + for err in result.errors do + IO.println s!"warning: {err.kind.phase}.{err.kind.category}: {err.message}" for td in result.program.types do IO.println (fmtTypeDef td) for proc in result.program.staticProcedures do @@ -129,15 +127,14 @@ private def mkFuncSigWithPostcond (name : String) (returnType : SpecType) preconditions := #[], postconditions := postconditions } -private def noneAtom := SpecAtomType.noneType -/-! ## Primitive and builtin types as args and return types -/ +/-! ## All function params and returns map to Any -/ /-- -info: procedure returns_int(x:TString) returns(result:TInt) -procedure returns_bool(a:TInt, b:TReal) returns(result:TBool) -procedure returns_real(flag:TBool) returns(result:TReal) -procedure with_kwonly(x:TInt, verbose:TBool) returns(result:TString) +info: procedure returns_int(x:UserDefined(Any)) returns(result:UserDefined(Any)) +procedure returns_bool(a:UserDefined(Any), b:UserDefined(Any)) returns(result:UserDefined(Any)) +procedure returns_real(flag:UserDefined(Any)) returns(result:UserDefined(Any)) +procedure with_kwonly(x:UserDefined(Any), verbose:UserDefined(Any)) returns(result:UserDefined(Any)) -/ #guard_msgs in #eval runTest #[ @@ -156,8 +153,8 @@ procedure with_kwonly(x:TInt, verbose:TBool) returns(result:TString) /-! ## Complex types (Any, List, Dict, bytes) -/ /-- -info: procedure takes_any(x:UserDefined(Any)) returns(result:TInt) -procedure takes_list(items:UserDefined(Any)) returns(result:TBool) +info: procedure takes_any(x:UserDefined(Any)) returns(result:UserDefined(Any)) +procedure takes_list(items:UserDefined(Any)) returns(result:UserDefined(Any)) procedure returns_dict() returns(result:UserDefined(Any)) procedure typed_list() returns(result:UserDefined(Any)) procedure typed_dict() returns(result:UserDefined(Any)) @@ -170,67 +167,69 @@ procedure typed_dict() returns(result:UserDefined(Any)) (args := #[mkArg "items" (identType .typingList)]), mkFuncSig "returns_dict" (identType .typingDict), mkFuncSig "typed_list" - (mkType (.ident .typingList #[identType .builtinsStr])), + (SpecType.ident loc .typingList #[identType .builtinsStr]), mkFuncSig "typed_dict" - (mkType (.ident .typingDict - #[identType .builtinsStr, identType .builtinsInt])) + (SpecType.ident loc .typingDict + #[identType .builtinsStr, identType .builtinsInt]) ] -/-! ## Literal types, TypedDict, and string-literal unions -/ +/-! ## Literal types, TypedDict, and string-literal unions → Any -/ /-- -info: procedure int_literal_ret() returns(result:TInt) -procedure str_literal_ret() returns(result:TString) -procedure typed_dict_ret() returns(result:UserDefined(DictStrAny)) -procedure str_enum() returns(result:TString) +info: warning: pySpecToLaurel.unsupportedUnion: TypedDict 'TypedDict(f)' approximated as DictStrAny in type 'TypedDict(f)' +procedure int_literal_ret() returns(result:UserDefined(Any)) +procedure str_literal_ret() returns(result:UserDefined(Any)) +procedure typed_dict_ret() returns(result:UserDefined(Any)) +procedure str_enum() returns(result:UserDefined(Any)) -/ #guard_msgs in #eval runTest #[ - mkFuncSig "int_literal_ret" (mkType (.intLiteral 42)), + mkFuncSig "int_literal_ret" (SpecType.intLiteral loc 42), mkFuncSig "str_literal_ret" - (mkType (.stringLiteral "hello")), + (SpecType.stringLiteral loc "hello"), mkFuncSig "typed_dict_ret" - (mkType (.typedDict #["f"] - #[identType .builtinsStr] #[true])), + (SpecType.typedDict loc #["f"] + #[identType .builtinsStr] #[true]), mkFuncSig "str_enum" - (mkUnion #[.stringLiteral "A", .stringLiteral "B", - .stringLiteral "C"]) + (mkUnion #[SpecType.stringLiteral loc "A", SpecType.stringLiteral loc "B", + SpecType.stringLiteral loc "C"]) ] -/-! ## Optional type patterns (Union[None, T]) -/ +/-! ## Optional type patterns (Union[None, T]) → Any -/ /-- -info: procedure opt_str() returns(result:UserDefined(StrOrNone)) -procedure opt_int() returns(result:UserDefined(IntOrNone)) -procedure opt_bool(x:UserDefined(StrOrNone)) returns(result:UserDefined(BoolOrNone)) -procedure opt_typed_dict() returns(result:UserDefined(DictStrAny)) -procedure opt_str_enum() returns(result:UserDefined(StrOrNone)) -procedure opt_int_enum() returns(result:UserDefined(IntOrNone)) +info: warning: pySpecToLaurel.unsupportedUnion: TypedDict 'TypedDict(x)' approximated as DictStrAny in type 'Union[_types.NoneType, TypedDict(x)]' +procedure opt_str() returns(result:UserDefined(Any)) +procedure opt_int() returns(result:UserDefined(Any)) +procedure opt_bool(x:UserDefined(Any)) returns(result:UserDefined(Any)) +procedure opt_typed_dict() returns(result:UserDefined(Any)) +procedure opt_str_enum() returns(result:UserDefined(Any)) +procedure opt_int_enum() returns(result:UserDefined(Any)) -/ #guard_msgs in #eval runTest #[ mkFuncSig "opt_str" - (mkUnion #[noneAtom, identAtom .builtinsStr]), + (mkUnion #[noneType, identType .builtinsStr]), mkFuncSig "opt_int" - (mkUnion #[noneAtom, identAtom .builtinsInt]), + (mkUnion #[noneType, identType .builtinsInt]), mkFuncSig "opt_bool" - (mkUnion #[noneAtom, identAtom .builtinsBool]) + (mkUnion #[noneType, identType .builtinsBool]) (args := #[mkArg "x" - (mkUnion #[noneAtom, identAtom .builtinsStr])]), + (mkUnion #[noneType, identType .builtinsStr])]), mkFuncSig "opt_typed_dict" - (mkUnion #[noneAtom, - .typedDict #["x"] #[identType .builtinsStr] #[true]]), + (mkUnion #[noneType, + SpecType.typedDict loc #["x"] #[identType .builtinsStr] #[true]]), mkFuncSig "opt_str_enum" - (mkUnion #[noneAtom, .stringLiteral "A", - .stringLiteral "B"]), + (mkUnion #[noneType, SpecType.stringLiteral loc "A", + SpecType.stringLiteral loc "B"]), mkFuncSig "opt_int_enum" - (mkUnion #[noneAtom, .intLiteral 1, .intLiteral 2]) + (mkUnion #[noneType, SpecType.intLiteral loc 1, SpecType.intLiteral loc 2]) ] /-! ## Error cases (updated to verify WarningKind) -/ /-- -info: procedure f() returns(result:UserDefined(Bar)) +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in #eval runTest @@ -238,37 +237,31 @@ info: procedure f() returns(result:UserDefined(Bar)) (identType (PythonIdent.mk "foo" "Bar"))] /-- -info: pySpecToLaurel.emptyType: Empty type (no atoms) encountered in Laurel conversion --/ -#guard_msgs in -#eval runTestWarningKinds - #[mkFuncSig "f" { atoms := #[], loc := default }] - -/-- -info: pySpecToLaurel.unsupportedUnion: Union type (builtins.str | builtins.int) not yet supported in Laurel +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds +#eval runTest #[mkFuncSig "f" - (mkUnion #[identAtom .builtinsStr, - identAtom .builtinsInt])] + (mkUnion #[identType .builtinsStr, + identType .builtinsInt])] /-- -info: pySpecToLaurel.unsupportedUnion: Union type (None | foo.Bar) not yet supported in Laurel +info: warning: pySpecToLaurel.unsupportedUnion: No type tester for 'foo.Bar' in type 'Union[_types.NoneType, foo.Bar]' +procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds +#eval runTest #[mkFuncSig "f" - (mkUnion #[noneAtom, - identAtom (PythonIdent.mk "foo" "Bar")])] + (mkUnion #[noneType, + identType (PythonIdent.mk "foo" "Bar")])] /-! ## Class and type definitions -/ /-- info: type MyClass type MyAlias -procedure my_func(x:TInt, y:TString) returns(result:TBool) -procedure MyClass@get_value() returns(result:TString) +procedure my_func(x:UserDefined(Any), y:UserDefined(Any)) returns(result:UserDefined(Any)) +procedure MyClass@get_value() returns(result:UserDefined(Any)) -/ #guard_msgs in #eval runTest #[ @@ -297,20 +290,20 @@ procedure MyClass@get_value() returns(result:TString) /-- info: procedure returns_none() returns(result:UserDefined(Any)) -procedure takes_none(x:TVoid) returns(result:UserDefined(Any)) +procedure takes_none(x:UserDefined(Any)) returns(result:UserDefined(Any)) -/ #guard_msgs in #eval runTest #[ - mkFuncSig "returns_none" (mkType .noneType), - mkFuncSig "takes_none" (identType .noneType) - (args := #[mkArg "x" (mkType .noneType)]) + mkFuncSig "returns_none" noneType, + mkFuncSig "takes_none" noneType + (args := #[mkArg "x" noneType]) ] /-! ## Class types as UserDefined -/ /-- info: type Foo -procedure uses_class(x:UserDefined(Foo)) returns(result:UserDefined(Foo)) +procedure uses_class(x:UserDefined(Foo)) returns(result:UserDefined(Any)) -/ #guard_msgs in #eval runTest #[ @@ -318,8 +311,8 @@ procedure uses_class(x:UserDefined(Foo)) returns(result:UserDefined(Foo)) loc := loc, name := "Foo" methods := #[] }, - mkFuncSig "uses_class" (mkType (.ident (PythonIdent.mk "" "Foo") #[])) - (args := #[mkArg "x" (mkType (.ident (PythonIdent.mk "" "Foo") #[]))]) + mkFuncSig "uses_class" (identType (PythonIdent.mk "" "Foo")) + (args := #[mkArg "x" (identType (PythonIdent.mk "" "Foo"))]) ] /-! ## Empty input -/ @@ -378,8 +371,8 @@ private def runDispatchTest (sigs : Array Signature) : IO Unit := do -- and a regular function. /-- info: type SvcClient -procedure SvcClient@do_thing(x:TString) returns(result:TInt) -procedure helper() returns(result:TBool) +procedure SvcClient@do_thing(x:UserDefined(Any)) returns(result:UserDefined(Any)) +procedure helper() returns(result:UserDefined(Any)) dispatch create_client: "svc_a" -> mod.client.SvcClient "svc_b" -> mod.other.OtherClient @@ -389,11 +382,11 @@ dispatch create_client: .externTypeDecl "SvcClient" (PythonIdent.mk "mod.client" "SvcClient"), .externTypeDecl "OtherClient" (PythonIdent.mk "mod.other" "OtherClient"), mkOverload "create_client" - (mkType (.ident (PythonIdent.mk "mod.client" "SvcClient") #[])) - (args := #[mkArg "name" (mkType (.stringLiteral "svc_a"))]), + (identType (PythonIdent.mk "mod.client" "SvcClient")) + (args := #[mkArg "name" (SpecType.stringLiteral loc "svc_a")]), mkOverload "create_client" - (mkType (.ident (PythonIdent.mk "mod.other" "OtherClient") #[])) - (args := #[mkArg "name" (mkType (.stringLiteral "svc_b"))]), + (identType (PythonIdent.mk "mod.other" "OtherClient")) + (args := #[mkArg "name" (SpecType.stringLiteral loc "svc_b")]), .classDef { loc := loc, name := "SvcClient" methods := #[ @@ -420,10 +413,10 @@ dispatch make: #eval runFullTest #[ .classDef { loc := loc, name := "Alpha", methods := #[] }, .classDef { loc := loc, name := "Beta", methods := #[] }, - mkOverload "make" (mkType (.ident (PythonIdent.mk "" "Alpha") #[])) - (args := #[mkArg "kind" (mkType (.stringLiteral "a"))]), - mkOverload "make" (mkType (.ident (PythonIdent.mk "" "Beta") #[])) - (args := #[mkArg "kind" (mkType (.stringLiteral "b"))]) + mkOverload "make" (identType (PythonIdent.mk "" "Alpha")) + (args := #[mkArg "kind" (SpecType.stringLiteral loc "a")]), + mkOverload "make" (identType (PythonIdent.mk "" "Beta")) + (args := #[mkArg "kind" (SpecType.stringLiteral loc "b")]) ] -- extractOverloads only processes externTypeDecl and @overload functions, @@ -436,8 +429,8 @@ info: dispatch factory: #eval runDispatchTest #[ .externTypeDecl "Foo" (PythonIdent.mk "pkg" "Foo"), mkOverload "factory" - (mkType (.ident (PythonIdent.mk "pkg" "Foo") #[])) - (args := #[mkArg "k" (mkType (.stringLiteral "x"))]), + (identType (PythonIdent.mk "pkg" "Foo")) + (args := #[mkArg "k" (SpecType.stringLiteral loc "x")]), .classDef { loc := loc, name := "Ignored", methods := #[] }, mkFuncSig "also_ignored" (identType .builtinsInt), .typeDef { loc := loc, nameLoc := loc, @@ -472,7 +465,7 @@ body contains FieldSelect: false let strTy := identType .builtinsStr let dictTy := identType .typingDict -- kwargs must be a TypedDict so expandKwargsArgs can expand it - let kwargsTy := SpecType.ofAtom loc (.typedDict #["Outer"] #[dictTy] #[true]) + let kwargsTy := SpecType.typedDict loc #["Outer"] #[dictTy] #[true] let result := signaturesToLaurel "" #[ .functionDecl { loc := loc, nameLoc := loc, name := "f" @@ -525,41 +518,41 @@ info: procedure f() returns(result:UserDefined(Any)) #eval runTest #[mkFuncSig "f" (identType .builtinsComplex)] --- Unsupported Optional patterns +-- Optional patterns now map to Any without warnings /-- -info: pySpecToLaurel.unsupportedOptionalFloat: Optional[float] mapped to TString +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds - #[mkFuncSig "f" (mkUnion #[noneAtom, identAtom .builtinsFloat])] +#eval runTest + #[mkFuncSig "f" (mkUnion #[noneType, identType .builtinsFloat])] /-- -info: pySpecToLaurel.unsupportedOptionalList: Optional[List] mapped to TString +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds - #[mkFuncSig "f" (mkUnion #[noneAtom, identAtom .typingList])] +#eval runTest + #[mkFuncSig "f" (mkUnion #[noneType, identType .typingList])] /-- -info: pySpecToLaurel.unsupportedOptionalDict: Optional[Dict] mapped to TString +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds - #[mkFuncSig "f" (mkUnion #[noneAtom, identAtom .typingDict])] +#eval runTest + #[mkFuncSig "f" (mkUnion #[noneType, identType .typingDict])] /-- -info: pySpecToLaurel.unsupportedOptionalAny: Optional[Any] mapped to TString +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds - #[mkFuncSig "f" (mkUnion #[noneAtom, identAtom .typingAny])] +#eval runTest + #[mkFuncSig "f" (mkUnion #[noneType, identType .typingAny])] /-- -info: pySpecToLaurel.unsupportedOptionalBytes: Optional[bytes] mapped to TString +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds - #[mkFuncSig "f" (mkUnion #[noneAtom, identAtom .builtinsBytes])] +#eval runTest + #[mkFuncSig "f" (mkUnion #[noneType, identType .builtinsBytes])] -- Precondition: placeholderExpr /-- @@ -638,12 +631,12 @@ info: pySpecToLaurel.kwargsExpansionError: **kw has non-TypedDict type; kwargs n preconditions := #[], postconditions := #[] }] --- Declaration: postconditionUnsupported +-- Declaration: postconditions now translated (no warning) /-- -info: pySpecToLaurel.postconditionUnsupported: Postconditions not yet supported +info: procedure f() returns(result:UserDefined(Any)) -/ #guard_msgs in -#eval runTestWarningKinds +#eval runTest #[mkFuncSigWithPostcond "f" (identType .builtinsStr) #[.intGe (.var "result" loc) (.intLit 0 loc) loc]] @@ -655,14 +648,14 @@ info: pySpecToLaurel.overloadNoArgs: Overloaded function 'bad' has no arguments #eval runTestWarningKinds #[mkOverload "bad" (identType .builtinsStr)] --- Overload: overloadArgArity +-- Overload: union arg type (not a singleton) → overloadArgNotStringLiteral /-- -info: pySpecToLaurel.overloadArgArity: Overloaded function 'bad': first argument has 2 type atoms, expected 1 +info: pySpecToLaurel.overloadArgNotStringLiteral: Overloaded function 'bad': first argument type 'Union[Literal["a"], Literal["b"]]' is not a string literal (only string literal dispatch is currently supported) -/ #guard_msgs in #eval runTestWarningKinds #[mkOverload "bad" (identType .builtinsStr) - (args := #[mkArg "x" (mkUnion #[.stringLiteral "a", .stringLiteral "b"])])] + (args := #[mkArg "x" (mkUnion #[SpecType.stringLiteral loc "a", SpecType.stringLiteral loc "b"])])] -- Overload: overloadArgNotStringLiteral /-- @@ -673,15 +666,15 @@ info: pySpecToLaurel.overloadArgNotStringLiteral: Overloaded function 'bad': fir #[mkOverload "bad" (identType .builtinsStr) (args := #[mkArg "x" (identType .builtinsStr)])] --- Overload: overloadReturnArity +-- Overload: union return type (not a singleton) → overloadReturnNotClass /-- -info: pySpecToLaurel.overloadReturnArity: Overloaded function 'bad': return type has 2 type atoms, expected 1 +info: pySpecToLaurel.overloadReturnNotClass: Overloaded function 'bad': return type 'Union[builtins.int, builtins.str]' is not a class type -/ #guard_msgs in #eval runTestWarningKinds #[mkOverload "bad" - (mkUnion #[identAtom .builtinsStr, identAtom .builtinsInt]) - (args := #[mkArg "x" (mkType (.stringLiteral "a"))])] + (mkUnion #[identType .builtinsStr, identType .builtinsInt]) + (args := #[mkArg "x" (SpecType.stringLiteral loc "a")])] -- Overload: overloadReturnNotClass /-- @@ -690,8 +683,8 @@ info: pySpecToLaurel.overloadReturnNotClass: Overloaded function 'bad': return t #guard_msgs in #eval runTestWarningKinds #[mkOverload "bad" - (mkType (.stringLiteral "hello")) - (args := #[mkArg "x" (mkType (.stringLiteral "a"))])] + (SpecType.stringLiteral loc "hello") + (args := #[mkArg "x" (SpecType.stringLiteral loc "a")])] /-! ## Precondition integration tests @@ -760,8 +753,7 @@ private def translatePrecond (preconditions : Array Assertion) -- not via containsKey on kwargs: `!` prefix syntax #eval do let strTy := identType .builtinsStr - let kwargsTy := SpecType.ofAtom loc - (.typedDict #["key"] #[strTy] #[false]) + let kwargsTy := SpecType.typedDict loc #["key"] #[strTy] #[false] let result := signaturesToLaurel "" #[ .functionDecl { loc := loc, nameLoc := loc, name := "f" @@ -774,7 +766,10 @@ private def translatePrecond (preconditions : Array Assertion) postconditions := #[] }] "" let body := getBody result |>.getD "" assertEq result.errors.size 0 - assertEq body "{ assert !Any..isfrom_None(key) summary \"precondition 0\" }" + assert! body.contains "result := " + assert! body.contains "Any..isfrom_None(key) | Any..isfrom_str(key)" + assert! body.contains "assert !Any..isfrom_None(key) summary \"precondition 0\"" + assert! body.contains "assume Any..isfrom_str(result)" -- containsKey on a non-kwargs dict: DictStrAny_contains in an assert -- (would have been silently dropped before fix #2) @@ -804,4 +799,87 @@ private def hasTypeError (result : TranslationResult) : Bool := #[{ message := #[], formula := .intLit 42 loc }] assert! hasTypeError result +/-! ## Body structure tests + +Verify the havoc + assert + assume pattern generated by `buildSpecBody`. -/ + +/-- Translate a function declaration and return `(bodyString, errorCount)`. -/ +private def translateFunc (args : Array Arg := #[]) + (returnType : SpecType := identType .builtinsStr) + (preconditions : Array Assertion := #[]) + (postconditions : Array SpecExpr := #[]) : String × Nat := + let result := signaturesToLaurel "" #[ + .functionDecl { + loc := loc, nameLoc := loc, name := "f" + args := { args := args, kwonly := #[] } + returnType, isOverload := false + preconditions, postconditions + }] "" + (getBody result |>.getD "", result.errors.size) + +-- No args, no preconditions: body has havoc + return type assume +#eval do + let (body, errs) := translateFunc + assert! errs == 0 + assert! body.contains "result := " + assert! body.contains "assume Any..isfrom_str(result)" + +-- Int arg with no default: type assert (implies not-None, so no separate check) +#eval do + let (body, errs) := translateFunc + (args := #[mkArg "x" (identType .builtinsInt)]) + assert! errs == 0 + assert! body.contains "assert Any..isfrom_int(x)" + assert! !body.contains "isfrom_None" + +-- Optional bool arg (has default): type assert uses Or, no required-param assert +#eval do + let (body, errs) := translateFunc + (args := #[mkArg "flag" (identType .builtinsBool) (some .none)]) + assert! errs == 0 + assert! body.contains "Any..isfrom_None(flag) | Any..isfrom_bool(flag)" + assert! !body.contains "'flag' is required" + +-- Float return type: assume Any..isfrom_float(result) +#eval do + let (body, errs) := translateFunc + (returnType := identType .builtinsFloat) + assert! errs == 0 + assert! body.contains "assume Any..isfrom_float(result)" + +-- Composite return type: no assume (no tester for user-defined types) +#eval do + let (body, errs) := translateFunc + (returnType := identType (PythonIdent.mk "mod" "Cls")) + assert! errs == 0 + assert! !body.contains "assume" + +-- Postcondition: assume in body +#eval do + let (body, errs) := translateFunc + (args := #[mkArg "x" (identType .builtinsInt)]) + (postconditions := #[.intGe (.var "result" loc) (.intLit 0 loc) loc]) + assert! errs == 0 + assert! body.contains "assume" + assert! body.contains "Any..as_int!" + +-- Precondition and postcondition together +#eval do + let geZero (v : String) : SpecExpr := .intGe (.var v loc) (.intLit 0 loc) loc + let pre : Assertion := { message := #[.str "n >= 0"], formula := geZero "n" } + let (body, errs) := translateFunc + (args := #[mkArg "n" (identType .builtinsInt)]) + (preconditions := #[pre]) + (postconditions := #[geZero "result"]) + assert! errs == 0 + -- type assert for n (implies not-None, so no separate check) + assert! body.contains "assert Any..isfrom_int(n)" + assert! !body.contains "isfrom_None(n)" + -- user precondition + assert! body.contains "assert" && body.contains "summary \"n >= 0\"" + -- postcondition as assume + assert! body.contains "assume" + -- return type assume + assert! body.contains "assume Any..isfrom_str(result)" + end Strata.Python.Specs.ToLaurel.Tests diff --git a/StrataTest/Languages/Python/expected_interpret/test_datetime_now_tz.expected b/StrataTest/Languages/Python/expected_interpret/test_datetime_now_tz.expected index 2410119fac..4fdbad5449 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_datetime_now_tz.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_datetime_now_tz.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] assert \(assert\(162\)\) condition did not reduce to bool diff --git a/StrataTestExtra/Languages/Python/SpecsTest.lean b/StrataTestExtra/Languages/Python/SpecsTest.lean index a469d301dc..744160af5a 100644 --- a/StrataTestExtra/Languages/Python/SpecsTest.lean +++ b/StrataTestExtra/Languages/Python/SpecsTest.lean @@ -275,7 +275,7 @@ meta def warningTestCase : IO Unit := withPython fun pythonCmd => do throw <| IO.userError "Expected warnings from warnings.py but got none" -- Check for specific expected warning substrings let expectedWarnings := #[ - "unrecognized assert pattern", -- assert kw["x"] == 1 + "unsupported comparison", -- assert kw["x"] == 1 "unsupported __init__ assignment", -- self.name = "hello" "skipped Assign in function body", -- x = kw["a"] "For: else clause not supported", -- for/else loop From 94e6ba87b6d609486ac950d3a42a94f62e6c1f50 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Wed, 29 Apr 2026 12:12:02 +0000 Subject: [PATCH 16/20] fix: Remove argHavoc from unmodeled calls to fix CI The argHavoc logic was havocking all non-composite arguments passed to unmodeled calls, but variableTypes stores 'Any' for all non-composite types (str, int, list, etc.), making it impossible to distinguish mutable from immutable arguments. This caused spurious havocs of immutable arguments (e.g., str parameters passed to open()), which broke the Laurel-to-Core translation pipeline. Remove argHavoc and keep only receiverHavoc, which correctly havocs the method receiver on unmodeled method calls. --- Strata/Languages/Python/PythonToLaurel.lean | 20 +++---------------- ...test_havoc_callee_after_hole_call.expected | 4 ++-- .../test_havoc_callee_after_hole_call.py | 2 +- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index 2e820d9879..331dac90d8 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -1156,11 +1156,8 @@ partial def translateCall (ctx : TranslationContext) | .Attribute range _ attr _ => (attr.val, range) | _ => (funcName, .none) throwUserError range s!"Unknown method '{methodName}'" - -- Havoc the receiver and Any-typed arguments since the unmodeled call - -- may mutate them and value-typed locals are not reachable via heap havoc. - -- Note: composite-typed arguments are NOT havoc'd here. If the unmodeled - -- call mutates a composite's fields, the heap should be havoc'd, but that - -- requires coordination with HeapParameterization and is out of scope. + -- Havoc the receiver since the unmodeled call may mutate it and + -- value-typed locals are not reachable via heap havoc. let receiverHavoc := match f with | .Attribute _ (.Name _ receiverName _) _ _ => if receiverName.val ∈ ctx.variableTypes.unzip.1 then @@ -1169,18 +1166,7 @@ partial def translateCall (ctx : TranslationContext) (mkStmtExprMd .Hole))] else [] | _ => [] - let argHavoc := args.flatMap fun arg => - if let .Name _ n _ := arg then - match ctx.variableTypes.find? (λ v => Prod.fst v == n.val) with - | some (varName, ty) => - if ty == PyLauType.Any then - [mkStmtExprMd (StmtExpr.Assign - [mkStmtExprMd (StmtExpr.Identifier varName)] - (mkStmtExprMd (.Hole false none)))] - else [] - | _ => [] - else [] - let havocStmts := receiverHavoc ++ argHavoc + let havocStmts := receiverHavoc if havocStmts.isEmpty then return mkStmtExprMd .Hole else diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 9ffa2fc20a..58b884dea2 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -3,6 +3,6 @@ test_havoc_callee_after_hole_call.py(12, 0): ❓ unknown - expected unknown beca test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - chained call: receiver not havocked (chained attribute is not a Name) test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - unrelated variable: nothing should be havocked test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - composite arg: heap not havocked (out of scope) -test_havoc_callee_after_hole_call.py(30, 0): ❓ unknown - expected unknown because argument locals should be havocked -DETAIL: 3 passed, 0 failed, 3 inconclusive +test_havoc_callee_after_hole_call.py(30, 0): ✔️ always true if reached - argument not havocked (type system cannot distinguish mutable from immutable) +DETAIL: 4 passed, 0 failed, 2 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index 20623aa194..9a500e539c 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -27,4 +27,4 @@ def __init__(self, n: int): xs2: list[int] = [1, 2] ys2: list[int] = [] xs2.unknown_method_that_may_modify_arguments(ys2) -assert ys2 == [], "expected unknown because argument locals should be havocked" +assert ys2 == [], "argument not havocked (type system cannot distinguish mutable from immutable)" From a6ed10dda85e0f6bb3f34efb8bb3e3f289c3db7b Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Wed, 29 Apr 2026 13:12:16 +0000 Subject: [PATCH 17/20] fix: Restore argument havoc for unmodeled calls Revert the test and expected output changes from commit 94e6ba8 and restore the argHavoc logic that havocs Any-typed arguments passed to unmodeled method calls. An unknown method is a black box that could mutate its arguments when they are not primitive types. The argHavoc checks the tracked type in variableTypes: only variables with type 'Any' (which includes list, dict, and other mutable containers in the value-type model) are havocked. Primitive types (int, bool, str, float) are tracked with their specific type names and are not affected. --- Strata/Languages/Python/PythonToLaurel.lean | 20 ++++++++++++++++--- ...test_havoc_callee_after_hole_call.expected | 4 ++-- .../test_havoc_callee_after_hole_call.py | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index 331dac90d8..2e820d9879 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -1156,8 +1156,11 @@ partial def translateCall (ctx : TranslationContext) | .Attribute range _ attr _ => (attr.val, range) | _ => (funcName, .none) throwUserError range s!"Unknown method '{methodName}'" - -- Havoc the receiver since the unmodeled call may mutate it and - -- value-typed locals are not reachable via heap havoc. + -- Havoc the receiver and Any-typed arguments since the unmodeled call + -- may mutate them and value-typed locals are not reachable via heap havoc. + -- Note: composite-typed arguments are NOT havoc'd here. If the unmodeled + -- call mutates a composite's fields, the heap should be havoc'd, but that + -- requires coordination with HeapParameterization and is out of scope. let receiverHavoc := match f with | .Attribute _ (.Name _ receiverName _) _ _ => if receiverName.val ∈ ctx.variableTypes.unzip.1 then @@ -1166,7 +1169,18 @@ partial def translateCall (ctx : TranslationContext) (mkStmtExprMd .Hole))] else [] | _ => [] - let havocStmts := receiverHavoc + let argHavoc := args.flatMap fun arg => + if let .Name _ n _ := arg then + match ctx.variableTypes.find? (λ v => Prod.fst v == n.val) with + | some (varName, ty) => + if ty == PyLauType.Any then + [mkStmtExprMd (StmtExpr.Assign + [mkStmtExprMd (StmtExpr.Identifier varName)] + (mkStmtExprMd (.Hole false none)))] + else [] + | _ => [] + else [] + let havocStmts := receiverHavoc ++ argHavoc if havocStmts.isEmpty then return mkStmtExprMd .Hole else diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 58b884dea2..9ffa2fc20a 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -3,6 +3,6 @@ test_havoc_callee_after_hole_call.py(12, 0): ❓ unknown - expected unknown beca test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - chained call: receiver not havocked (chained attribute is not a Name) test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - unrelated variable: nothing should be havocked test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - composite arg: heap not havocked (out of scope) -test_havoc_callee_after_hole_call.py(30, 0): ✔️ always true if reached - argument not havocked (type system cannot distinguish mutable from immutable) -DETAIL: 4 passed, 0 failed, 2 inconclusive +test_havoc_callee_after_hole_call.py(30, 0): ❓ unknown - expected unknown because argument locals should be havocked +DETAIL: 3 passed, 0 failed, 3 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index 9a500e539c..20623aa194 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -27,4 +27,4 @@ def __init__(self, n: int): xs2: list[int] = [1, 2] ys2: list[int] = [] xs2.unknown_method_that_may_modify_arguments(ys2) -assert ys2 == [], "argument not havocked (type system cannot distinguish mutable from immutable)" +assert ys2 == [], "expected unknown because argument locals should be havocked" From 88f4e6560762464d98bde6565cdc03bc268efb2f Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Wed, 29 Apr 2026 13:37:00 +0000 Subject: [PATCH 18/20] fix: Handle Hole RHS in Assign as havoc in LaurelToCoreTranslator When an unmodeled call generates a Block with Assign statements that have Hole as their RHS (to havoc receiver/arguments), the Assign handler in translateStmt was falling through to translateExpr for the Hole value, which is disallowed in expression position. Add an explicit case for Hole RHS that translates to a havoc statement instead. --- Strata/Languages/Laurel/LaurelToCoreTranslator.lean | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean index c89521ce8c..6b8be0fd11 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean @@ -430,6 +430,9 @@ def translateStmt (stmt : StmtExprMd) | .InstanceCall .. => -- Instance method call: havoc the target variable return [Core.Statement.havoc ident md] + | .Hole _ _ => + -- Hole RHS: havoc the target (unmodeled call side-effect). + return [Core.Statement.havoc ident md] | _ => let coreExpr ← translateExpr value return [Core.Statement.set ident coreExpr md] From 07d3556a9e14132922784876c32a50c2ecc8562a Mon Sep 17 00:00:00 2001 From: Olivier Bouissou Date: Thu, 30 Apr 2026 03:09:03 +0000 Subject: [PATCH 19/20] Fix regression when unmodelled function in a loop iterator --- Strata/Languages/Python/PythonToLaurel.lean | 18 +++++++++++++++--- .../test_class_decl.expected | 2 +- .../test_class_empty.expected | 2 +- .../test_class_field_any.expected | 2 +- .../test_class_field_init.expected | 2 +- ...test_class_inheritance_no_dispatch.expected | 2 +- .../test_class_method_call_from_main.expected | 2 +- .../test_class_methods.expected | 2 +- .../test_class_mixed_init.expected | 2 +- .../test_class_no_init.expected | 2 +- .../test_class_no_init_extra_args.expected | 2 +- .../test_class_no_init_multi_field.expected | 2 +- .../test_class_no_init_with_method.expected | 2 +- .../test_class_with_methods.expected | 2 +- .../test_field_write.expected | 2 +- .../test_float_literal.expected | 2 +- .../expected_interpret/test_for_range.expected | 2 +- .../expected_interpret/test_fstrings.expected | 2 +- .../expected_interpret/test_int_pow.expected | 2 +- .../test_invalid_client_type.expected | 2 +- .../test_is_non_none.expected | 2 +- .../test_is_not_non_none.expected | 2 +- .../expected_interpret/test_lshift.expected | 2 +- .../test_method_call_with_kwargs.expected | 2 +- .../test_missing_models.expected | 2 +- .../test_missing_required_param.expected | 2 +- .../test_param_reassign_cross_module.expected | 2 +- .../expected_interpret/test_power.expected | 2 +- .../test_regex_negative.expected | 2 +- .../test_regex_positive.expected | 2 +- .../expected_interpret/test_rshift.expected | 2 +- .../test_timedelta_expr.expected | 2 +- .../test_truthiness_bool_eq.expected | 2 +- .../test_truthiness_float.expected | 2 +- .../test_truthiness_not_eq.expected | 2 +- .../test_unsupported_config.expected | 2 +- .../test_user_error_metadata.expected | 2 +- .../test_havoc_callee_after_hole_call.expected | 4 +++- .../tests/test_havoc_callee_after_hole_call.py | 7 +++++++ 39 files changed, 61 insertions(+), 40 deletions(-) diff --git a/Strata/Languages/Python/PythonToLaurel.lean b/Strata/Languages/Python/PythonToLaurel.lean index 6ebabaaa90..592a5b1ad0 100644 --- a/Strata/Languages/Python/PythonToLaurel.lean +++ b/Strata/Languages/Python/PythonToLaurel.lean @@ -1868,8 +1868,20 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang -- and Any_iter_index is only called inside the loop body where that condition is satisfied, -- so it is sound to not put it inside AnyMaybeExceptionList | .For _ target iter body _orelse _ => do - -- The iterator expression (we abstract it away) - let iterExpr ← translateExpr ctx iter + -- The iterator expression (we abstract it away). + -- When the expression contains side-effect statements (e.g. a block with + -- receiver havoc from an unmodeled method call), bind it to a temporary + -- variable so the side effects execute once and the clean variable + -- reference can be used in PIn / Any_len / the while condition. + -- This mirrors Python semantics where the iterator is evaluated once. + let iterRaw ← translateExpr ctx iter + let (iterPreamble, iterExpr) := match iterRaw.val with + | .Block (_ :: _ :: _) _ => + let varName := s!"$for_iter_{iter.toAst.ann.start.byteIdx}" + let varDecl := mkStmtExprMd (StmtExpr.LocalVariable varName AnyTy (some iterRaw)) + let varRef := mkStmtExprMd (StmtExpr.Identifier varName) + ([varDecl], varRef) + | _ => ([], iterRaw) if let .Call _ (.Name _ {val:= "range",..} _) _ _ := iter then if let .StaticCall "range" _ := iterExpr.val then pure () @@ -1927,7 +1939,7 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang let loopStmt := mkStmtExprMdWithLoc (StmtExpr.While counterLtLen [] none innerBlock) md let loopBlock := mkStmtExprMdWithLoc (StmtExpr.Block [loopStmt] (some breakLabel)) md let (preamble, _) := getExceptionCheckPreamble ctx iterExpr s!"$for_iter_{iter.toAst.ann.start.byteIdx}" - return (finalCtx, preamble ++ [counterDecl] ++ [loopBlock]) + return (finalCtx, iterPreamble ++ preamble ++ [counterDecl] ++ [loopBlock]) | .Break _ => match ctx.loopBreakLabel with diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected b/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected b/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected b/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected b/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected b/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected b/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected b/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected b/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected index c77f7e4087..6543ad9b92 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected @@ -1 +1 @@ -Run strata --help for additional help\. +Run strata --help for additional help. diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected b/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_field_write.expected b/StrataTest/Languages/Python/expected_interpret/test_field_write.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_field_write.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_field_write.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected b/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected index e4dbc1f5dd..3373cfc452 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(36\)\) condition did not reduce to bool +[ERROR] assert (assert(36)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_for_range.expected b/StrataTest/Languages/Python/expected_interpret/test_for_range.expected index d7b87db228..e57a4f8eba 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_for_range.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_for_range.expected @@ -1 +1 @@ -\[ERROR\] assume \(assume\(23\)\) condition did not reduce to bool +[ERROR] assume (assume(23)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected b/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected index bc73866ddc..1638972db5 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(196\)\) condition did not reduce to bool +[ERROR] assert (assert(196)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected b/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected index fdfd8eb9f5..488721a8e7 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(75\)\) condition did not reduce to bool +[ERROR] assert (assert(75)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected b/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected b/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected index c77f7e4087..6543ad9b92 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected @@ -1 +1 @@ -Run strata --help for additional help\. +Run strata --help for additional help. diff --git a/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected b/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected index c77f7e4087..6543ad9b92 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected @@ -1 +1 @@ -Run strata --help for additional help\. +Run strata --help for additional help. diff --git a/StrataTest/Languages/Python/expected_interpret/test_lshift.expected b/StrataTest/Languages/Python/expected_interpret/test_lshift.expected index aab564ef27..cb9faa9712 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_lshift.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_lshift.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(51\)\) condition did not reduce to bool +[ERROR] assert (assert(51)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected b/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected b/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected index 6d3acd12ef..e9da797741 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected @@ -1 +1 @@ -\[ERROR\] expression contains stuck redex +[ERROR] expression contains stuck redex diff --git a/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected b/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected index 2410119fac..e1a7246547 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected @@ -1 +1 @@ -\[ERROR\] procedure '__main__': expected 1 arguments, got 0 +[ERROR] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected b/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected index e5bfdd2c78..9c5f42607b 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(59\)\) condition did not reduce to bool +[ERROR] assert (assert(59)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_power.expected b/StrataTest/Languages/Python/expected_interpret/test_power.expected index c633e6e0ed..0a028e6470 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_power.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_power.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(65\)\) condition did not reduce to bool +[ERROR] assert (assert(65)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected b/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected index 4b1cfd8776..a5c4b3fef9 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(272\)\) condition did not reduce to bool +[ERROR] assert (assert(272)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected b/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected index 886f2f679d..fe0c97d08d 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(215\)\) condition did not reduce to bool +[ERROR] assert (assert(215)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_rshift.expected b/StrataTest/Languages/Python/expected_interpret/test_rshift.expected index 38def5ef4b..3c2fb31a55 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_rshift.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_rshift.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(52\)\) condition did not reduce to bool +[ERROR] assert (assert(52)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected b/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected index 148c22345a..c47ad8f5e1 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(140\)\) condition did not reduce to bool +[ERROR] assert (assert(140)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected b/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected index cdbd192a4f..ad14628026 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(673\)\) condition did not reduce to bool +[ERROR] assert (assert(673)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected b/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected index e6750a00d3..a0414ed9f5 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(187\)\) condition did not reduce to bool +[ERROR] assert (assert(187)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected b/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected index 3186479c6d..5071d1ac9c 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(288\)\) condition did not reduce to bool +[ERROR] assert (assert(288)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected b/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected index f5f86ae44d..7ca2eaefbe 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected @@ -1 +1 @@ -\[ERROR\] assert \(assert\(178\)\) condition did not reduce to bool +[ERROR] assert (assert(178)) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected b/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected index c77f7e4087..6543ad9b92 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected @@ -1 +1 @@ -Run strata --help for additional help\. +Run strata --help for additional help. diff --git a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected index 9ffa2fc20a..9a320c707c 100644 --- a/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataTest/Languages/Python/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -4,5 +4,7 @@ test_havoc_callee_after_hole_call.py(16, 0): ✔️ always true if reached - cha test_havoc_callee_after_hole_call.py(20, 0): ✔️ always true if reached - unrelated variable: nothing should be havocked test_havoc_callee_after_hole_call.py(25, 0): ✔️ always true if reached - composite arg: heap not havocked (out of scope) test_havoc_callee_after_hole_call.py(30, 0): ❓ unknown - expected unknown because argument locals should be havocked -DETAIL: 3 passed, 0 failed, 3 inconclusive +test_havoc_callee_after_hole_call.py(36, 0): ❓ unknown - assume_assume(1193)_calls_PIn_0 +test_havoc_callee_after_hole_call.py(37, 4): ✔️ always true if reached - for-loop over unmodeled iterator should not crash +DETAIL: 4 passed, 0 failed, 4 inconclusive RESULT: Inconclusive diff --git a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py index 20623aa194..6e45f7c6d8 100644 --- a/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py +++ b/StrataTest/Languages/Python/tests/test_havoc_callee_after_hole_call.py @@ -28,3 +28,10 @@ def __init__(self, n: int): ys2: list[int] = [] xs2.unknown_method_that_may_modify_arguments(ys2) assert ys2 == [], "expected unknown because argument locals should be havocked" + +# Regression: unmodeled method call as for-loop iterator. +# The receiver havoc must be lifted out of the iterator expression so +# the block does not end up in expression position inside the assume. +response: dict = {"messages": []} +for msg in response.unmodeled_iter_method(): + assert True, "for-loop over unmodeled iterator should not crash" From 65a0fdbf27ed2453027ab4c1d57aa531ca04bcab Mon Sep 17 00:00:00 2001 From: Olivier Bouissou Date: Thu, 30 Apr 2026 03:10:37 +0000 Subject: [PATCH 20/20] restore expected files --- .../Python/expected_interpret/test_class_decl.expected | 2 +- .../Python/expected_interpret/test_class_empty.expected | 2 +- .../Python/expected_interpret/test_class_field_any.expected | 2 +- .../Python/expected_interpret/test_class_field_init.expected | 2 +- .../test_class_inheritance_no_dispatch.expected | 2 +- .../test_class_method_call_from_main.expected | 2 +- .../Python/expected_interpret/test_class_methods.expected | 2 +- .../Python/expected_interpret/test_class_mixed_init.expected | 2 +- .../Python/expected_interpret/test_class_no_init.expected | 2 +- .../expected_interpret/test_class_no_init_extra_args.expected | 2 +- .../expected_interpret/test_class_no_init_multi_field.expected | 2 +- .../expected_interpret/test_class_no_init_with_method.expected | 2 +- .../Python/expected_interpret/test_class_with_methods.expected | 2 +- .../Python/expected_interpret/test_field_write.expected | 2 +- .../Python/expected_interpret/test_float_literal.expected | 2 +- .../Languages/Python/expected_interpret/test_for_range.expected | 2 +- .../Languages/Python/expected_interpret/test_fstrings.expected | 2 +- .../Languages/Python/expected_interpret/test_int_pow.expected | 2 +- .../Python/expected_interpret/test_invalid_client_type.expected | 2 +- .../Python/expected_interpret/test_is_non_none.expected | 2 +- .../Python/expected_interpret/test_is_not_non_none.expected | 2 +- .../Languages/Python/expected_interpret/test_lshift.expected | 2 +- .../expected_interpret/test_method_call_with_kwargs.expected | 2 +- .../Python/expected_interpret/test_missing_models.expected | 2 +- .../expected_interpret/test_missing_required_param.expected | 2 +- .../test_param_reassign_cross_module.expected | 2 +- .../Languages/Python/expected_interpret/test_power.expected | 2 +- .../Python/expected_interpret/test_regex_negative.expected | 2 +- .../Python/expected_interpret/test_regex_positive.expected | 2 +- .../Languages/Python/expected_interpret/test_rshift.expected | 2 +- .../Python/expected_interpret/test_timedelta_expr.expected | 2 +- .../Python/expected_interpret/test_truthiness_bool_eq.expected | 2 +- .../Python/expected_interpret/test_truthiness_float.expected | 2 +- .../Python/expected_interpret/test_truthiness_not_eq.expected | 2 +- .../Python/expected_interpret/test_unsupported_config.expected | 2 +- .../Python/expected_interpret/test_user_error_metadata.expected | 2 +- 36 files changed, 36 insertions(+), 36 deletions(-) diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected b/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_decl.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected b/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_empty.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected b/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_field_any.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected b/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_field_init.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected b/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_inheritance_no_dispatch.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected b/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_method_call_from_main.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected b/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_methods.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected b/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_mixed_init.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected index 6543ad9b92..c77f7e4087 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_extra_args.expected @@ -1 +1 @@ -Run strata --help for additional help. +Run strata --help for additional help\. diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_multi_field.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_no_init_with_method.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected b/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_class_with_methods.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_field_write.expected b/StrataTest/Languages/Python/expected_interpret/test_field_write.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_field_write.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_field_write.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected b/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected index 3373cfc452..e4dbc1f5dd 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_float_literal.expected @@ -1 +1 @@ -[ERROR] assert (assert(36)) condition did not reduce to bool +\[ERROR\] assert \(assert\(36\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_for_range.expected b/StrataTest/Languages/Python/expected_interpret/test_for_range.expected index e57a4f8eba..d7b87db228 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_for_range.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_for_range.expected @@ -1 +1 @@ -[ERROR] assume (assume(23)) condition did not reduce to bool +\[ERROR\] assume \(assume\(23\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected b/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected index 1638972db5..bc73866ddc 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_fstrings.expected @@ -1 +1 @@ -[ERROR] assert (assert(196)) condition did not reduce to bool +\[ERROR\] assert \(assert\(196\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected b/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected index 488721a8e7..fdfd8eb9f5 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_int_pow.expected @@ -1 +1 @@ -[ERROR] assert (assert(75)) condition did not reduce to bool +\[ERROR\] assert \(assert\(75\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected b/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_invalid_client_type.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected b/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected index 6543ad9b92..c77f7e4087 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_is_non_none.expected @@ -1 +1 @@ -Run strata --help for additional help. +Run strata --help for additional help\. diff --git a/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected b/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected index 6543ad9b92..c77f7e4087 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_is_not_non_none.expected @@ -1 +1 @@ -Run strata --help for additional help. +Run strata --help for additional help\. diff --git a/StrataTest/Languages/Python/expected_interpret/test_lshift.expected b/StrataTest/Languages/Python/expected_interpret/test_lshift.expected index cb9faa9712..aab564ef27 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_lshift.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_lshift.expected @@ -1 +1 @@ -[ERROR] assert (assert(51)) condition did not reduce to bool +\[ERROR\] assert \(assert\(51\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected b/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_method_call_with_kwargs.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected b/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected index e9da797741..6d3acd12ef 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_missing_models.expected @@ -1 +1 @@ -[ERROR] expression contains stuck redex +\[ERROR\] expression contains stuck redex diff --git a/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected b/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected index e1a7246547..2410119fac 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_missing_required_param.expected @@ -1 +1 @@ -[ERROR] procedure '__main__': expected 1 arguments, got 0 +\[ERROR\] procedure '__main__': expected 1 arguments, got 0 diff --git a/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected b/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected index 9c5f42607b..e5bfdd2c78 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_param_reassign_cross_module.expected @@ -1 +1 @@ -[ERROR] assert (assert(59)) condition did not reduce to bool +\[ERROR\] assert \(assert\(59\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_power.expected b/StrataTest/Languages/Python/expected_interpret/test_power.expected index 0a028e6470..c633e6e0ed 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_power.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_power.expected @@ -1 +1 @@ -[ERROR] assert (assert(65)) condition did not reduce to bool +\[ERROR\] assert \(assert\(65\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected b/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected index a5c4b3fef9..4b1cfd8776 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_regex_negative.expected @@ -1 +1 @@ -[ERROR] assert (assert(272)) condition did not reduce to bool +\[ERROR\] assert \(assert\(272\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected b/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected index fe0c97d08d..886f2f679d 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_regex_positive.expected @@ -1 +1 @@ -[ERROR] assert (assert(215)) condition did not reduce to bool +\[ERROR\] assert \(assert\(215\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_rshift.expected b/StrataTest/Languages/Python/expected_interpret/test_rshift.expected index 3c2fb31a55..38def5ef4b 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_rshift.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_rshift.expected @@ -1 +1 @@ -[ERROR] assert (assert(52)) condition did not reduce to bool +\[ERROR\] assert \(assert\(52\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected b/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected index c47ad8f5e1..148c22345a 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_timedelta_expr.expected @@ -1 +1 @@ -[ERROR] assert (assert(140)) condition did not reduce to bool +\[ERROR\] assert \(assert\(140\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected b/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected index ad14628026..cdbd192a4f 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_truthiness_bool_eq.expected @@ -1 +1 @@ -[ERROR] assert (assert(673)) condition did not reduce to bool +\[ERROR\] assert \(assert\(673\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected b/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected index a0414ed9f5..e6750a00d3 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_truthiness_float.expected @@ -1 +1 @@ -[ERROR] assert (assert(187)) condition did not reduce to bool +\[ERROR\] assert \(assert\(187\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected b/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected index 5071d1ac9c..3186479c6d 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_truthiness_not_eq.expected @@ -1 +1 @@ -[ERROR] assert (assert(288)) condition did not reduce to bool +\[ERROR\] assert \(assert\(288\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected b/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected index 7ca2eaefbe..f5f86ae44d 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_unsupported_config.expected @@ -1 +1 @@ -[ERROR] assert (assert(178)) condition did not reduce to bool +\[ERROR\] assert \(assert\(178\)\) condition did not reduce to bool diff --git a/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected b/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected index 6543ad9b92..c77f7e4087 100644 --- a/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected +++ b/StrataTest/Languages/Python/expected_interpret/test_user_error_metadata.expected @@ -1 +1 @@ -Run strata --help for additional help. +Run strata --help for additional help\.