Skip to content

Commit 9bba297

Browse files
tautschnigkiro-agentshigoel
authored
Emit Hole for Composite return values assigned to Any (#727)
### Problem When a Python function returns a Composite-typed value (e.g., a service client) but the Laurel return variable `LaurelResult` has type `Any`, Core type checking fails with "Impossible to unify Any with Composite". ### Approach Per design discussion with @keyboardDrummer and @joehendrix: emit a Hole (unconstrained value) when a Composite-typed expression is returned from a function with Any return type. This avoids the type unification crash while being honest about the limitation — the verifier treats the return value as unconstrained, which limits bug-finding but doesn't produce unsound results. Auto-generated Composite -> Any coercions can be revisited once the broader Composite/Any typing design is resolved. **Changes:** - Add `coerceToAny` helper that replaces Composite-typed expressions with a Hole when assigned to an Any-typed context - Apply coercion in return statement translation before assigning to `LaurelResult` - Add regression tests in `VerifyPythonTest.lean` and CBMC pipeline By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Kiro <kiro-agent@users.noreply.github.com> Co-authored-by: Shilpi Goel <shigoel@gmail.com>
1 parent 8881eb5 commit 9bba297

4 files changed

Lines changed: 46 additions & 0 deletions

File tree

Strata/Languages/Python/PythonToLaurel.lean

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,17 @@ If the function_name is a class, add __init__ into it
738738
The following function return a tuple (translated function name, first argument, is the first argument of unknown type)
739739
-/
740740

741+
/-- Coerce an expression to Any if its inferred type is a Composite class.
742+
Composite values are replaced with a Hole (unconstrained Any value)
743+
since Composite→Any coercion is not yet modeled. This limits
744+
bug-finding ability but avoids type unification errors. -/
745+
partial def coerceToAny (ctx : TranslationContext) (expr : Python.expr SourceRange)
746+
(translated : StmtExprMd) : Except TranslationError StmtExprMd := do
747+
let ty ← inferExprType ctx expr
748+
if isCompositeType ctx ty then
749+
pure <| mkStmtExprMd (.Hole)
750+
else pure translated
751+
741752
partial def refineFunctionCallExpr (ctx : TranslationContext) (func: Python.expr SourceRange) :
742753
Except TranslationError (String × Option (Python.expr SourceRange) × Bool) := do
743754
match func with
@@ -1207,6 +1218,8 @@ partial def translateStmt (ctx : TranslationContext) (s : Python.stmt SourceRang
12071218
let stmts ← match value.val with
12081219
| some expr => do
12091220
let e ← translateExpr ctx expr
1221+
-- Coerce Composite return values to Any for LaurelResult : Any
1222+
let e ← coerceToAny ctx expr e
12101223
let assign := mkStmtExprMd (StmtExpr.Assign [mkStmtExprMd (StmtExpr.Identifier PyLauFuncReturnVar)] e)
12111224
.ok [assign, mkStmtExprMd (StmtExpr.Exit "$body")]
12121225
| none => .ok [mkStmtExprMd (StmtExpr.Exit "$body")]

StrataTest/Languages/Python/VerifyPythonTest.lean

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,25 @@ open Strata.Parser (stringInputContext)
130130
if diags.size ≠ 0 then
131131
throw <| .userError s!"Expected 0 diagnostics, got {diags.size}"
132132

133+
-- Returning a Composite-typed value from a function with Any return type
134+
-- should not crash; the Composite is replaced with a Hole (unconstrained value).
135+
#guard_msgs in
136+
#eval withPython (warnOnSkip := false) fun pythonCmd => do
137+
let program :=
138+
"from typing import Any
139+
140+
class MyService:
141+
name: str
142+
143+
def __init__(self, name: str) -> None:
144+
self.name = name
145+
146+
def create_service() -> Any:
147+
svc: MyService = MyService(\"test\")
148+
return svc
149+
"
150+
let diags ← processPythonFile pythonCmd (stringInputContext "test.py" program)
151+
if diags.size ≠ 0 then
152+
throw <| .userError s!"Expected 0 diagnostics, got {diags.size}"
153+
133154
end Strata.Python.VerifyPythonTest

StrataTest/Languages/Python/tests/cbmc_expected.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ test_class_with_methods.py.ion SKIP
3131
test_module_level.py.ion SKIP
3232
test_if_elif.py.ion SKIP
3333
test_variable_reassign.py.ion SKIP
34+
test_composite_return.py.ion PASS
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import Any
2+
3+
class MyService:
4+
name: str
5+
6+
def __init__(self, name: str) -> None:
7+
self.name = name
8+
9+
def create_service() -> Any:
10+
svc: MyService = MyService("test")
11+
return svc

0 commit comments

Comments
 (0)