Skip to content

Python: skip Any_to_bool for bool-typed assert conditions (#1102)#1119

Open
julesmt wants to merge 1 commit intostrata-org:mainfrom
julesmt:fix/isinstance-assert-1102
Open

Python: skip Any_to_bool for bool-typed assert conditions (#1102)#1119
julesmt wants to merge 1 commit intostrata-org:mainfrom
julesmt:fix/isinstance-assert-1102

Conversation

@julesmt
Copy link
Copy Markdown
Member

@julesmt julesmt commented May 5, 2026

When an assert condition translates to a .Hole (e.g. a call to an unmodelled function like isinstance), the assert translator hoists it into a fresh bool-typed variable. Unconditionally wrapping that variable in Any_to_bool is ill-typed since Any_to_bool : Any -> bool, producing:

Impossible to unify (arrow Any bool) with (arrow bool $__ty…)

Check the invariant directly: if the translated condition is already a reference to a bool-typed local, skip the Any_to_bool coercion. This handles the Hole-hoisting path and any future path producing a bool-typed identifier uniformly.

Adds regression fixtures under StrataTestExtra/Languages/Python/Issue1102/ and a Lean test harness that asserts the translation pipeline produces no type-check or Laurel-to-Core crash on the minimal fuzz reproductions from issue #1102 (seed 1777894483, fuzz_semantic_0004).

Issue #, if available:

Description of changes:

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Copy link
Copy Markdown
Contributor

@tautschnig tautschnig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix is in the right place and the diagnosis is correct. The hoisted variable is declared TBool, so wrapping it in Any_to_bool : Any → bool produces the (arrow Any bool) vs (arrow bool $__ty…) unification error from #1102. Skipping the coercion when the condition is already a bool-typed local is a minimal, surgical fix. My concerns are about how the check is structured (fragile), the scope of the .Var (.Local …) predicate (narrow), and the strength of the new regression tests (thin).

1. The check is done via a runtime variableTypes.find? + == PyLauType.Bool string comparison on a name the same block just created. The Hole branch at lines 1745–1750 literally writes TBool plus (freshVar, "bool") into the context, so the code already knows at that point that the hoisted variable is bool-typed. Re-deriving that knowledge a few lines later via a linear lookup and a string equality is unnecessary work, and it makes the check vulnerable to drift — if PyLauType.Bool is ever renamed, or if the context insertion ever uses a different casing/constant, the lookup silently fails and we're back to ill-typed Any_to_bool(bool). Inline comment suggests returning an alreadyBool : Bool flag from the pattern match directly.

2. The check's pattern is narrower than the invariant it's enforcing. What we really mean is "don't apply Any_to_bool to something whose type is already bool." The check only catches .Var (.Local name) where name's recorded type is bool. That happens to be exactly what the Hole-hoisting path produces, so the current callers are all covered. But:

  • Any future translation path that produces a bool-typed non-.Var expression — a comparison primitive, a bool-valued .StaticCall, an .IfThenElse over bools, etc. — would silently hit Any_to_bool(bool) again.
  • .Var (.Field self bool_field) for a bool-typed self field also wouldn't match, though that's fixable.

Not a blocker — the check is correct for today's translator. But a short doc comment on the predicate (e.g. -- Today only the Hole-hoisting branch above produces this shape; if a future translation path produces a bool-typed non-Var, extend the predicate) documents the assumption.

3. The same Any_to_bool condExpr wrapping exists for .If (line 1701) and .While (line 1715). Neither currently does Hole-hoisting (PR #608's 77318d1f8 removed it from If/While but kept it for Assert), so they don't hit #1102's bug today. But if Hole-hoisting is ever reintroduced for If or While — or if a non-Hole translation path produces a bool — the same unification error will resurface. The fix here could be factored into a tiny helper so all three condition sites use the same logic:

private def coerceToBool (ctx : TranslationContext) (e : StmtExprMd) : StmtExprMd :=
  match e.val with
  | .Var (.Local name) =>
    match ctx.variableTypes.find? (fun (n, _) => n == name) with
    | some (_, ty) => if ty == PyLauType.Bool then e else Any_to_bool e
    | none => Any_to_bool e
  | _ => Any_to_bool e

All three of IfThenElse (Any_to_bool condRef), While (Any_to_bool condRef), and Assert { condition := Any_to_bool finalCondExpr } then become coerceToBool ctx condRef/finalCondExpr. Not blocking; follow-up-able.

4. The new test detects "no pipeline crash" via a string-prefix match on diagnostic messages. Lines 32–34 of Issue1102Test.lean:

private def isPipelineCrash (msg : String) : Bool :=
  msg.startsWith "❌ Type checking error" ||
  msg.startsWith "Laurel to Core translation failed"

Two problems with this:

  • Fragile. Rewording either of those diagnostics (something this repo does frequently — e.g. PR #1104) silently turns the test from "no crash" to "match nothing, pass vacuously", at which point a #1102 regression would land green. Matching on a structural property (diagnostic type / severity, or the presence of DiagnosticType.StrataBug) is more robust if the processing pipeline exposes it.
  • Passes vacuously on the happy path. The whole regression is a type-unification failure; the assertion itself is already unprovable (since isinstance is unmodelled). Nothing in the test distinguishes "fix worked, so translation ran through and verifier reported the assertion as unprovable" from "fix silently dropped the assertion". A stronger check would #guard_msgs on the actual verifier output: the expected line for assert isinstance(5, int) should report the assertion as "unprovable" (or unknown), not pass or vanish.

This repo has existing precedent for this stronger pattern in StrataTest/Languages/Python/expected_laurel/*.expected — one snapshot per fixture, matched via a test runner. Worth adding.

5. Coverage gaps in the test fixtures. Two fixtures, both of the form assert isinstance(v, T) at top level of main. Things that would materially strengthen coverage:

  • Non-Hole path. Add a fixture like assert x == 1 where x : Any (so the condExpr is a bool-typed comparison, not a Hole). Confirms the _ => … arm at line 1748 still wraps in Any_to_bool correctly. Today this case already works, but the fix introduces a conditional that could in principle mis-fire and silently drop the wrapper.
  • Negated / combined. assert not isinstance(x, int) and assert isinstance(x, int) and x > 0. Both exercise the Hole-hoisting path differently (the and routes through .BoolOp which wraps each argument; the not is a .UnaryOp).
  • Inside a conditional. if isinstance(x, int): assert x > 0 — exercises the .If translation's handling of Hole (which doesn't hoist, so Any_to_bool(Hole) is emitted directly). Confirms my observation above that If/While don't hit the bug today.
  • Property summary. assert isinstance(x, int), "x must be int" — the Hole-hoisting plus summary extraction happen in the same .Assert arm. The summary path isn't exercised by either current fixture.

Comment thread Strata/Languages/Python/PythonToLaurel.lean
Comment thread StrataTestExtra/Languages/Python/Issue1102Test.lean Outdated
…#1102)

When an assert condition translates to a .Hole (e.g. a call to an
unmodelled function like isinstance), the assert translator hoists it
into a fresh bool-typed variable. Unconditionally wrapping that
variable in Any_to_bool is ill-typed since Any_to_bool : Any -> bool,
producing:

  Impossible to unify (arrow Any bool) with (arrow bool $__ty…)

Check the invariant directly: if the translated condition is already a
reference to a bool-typed local, skip the Any_to_bool coercion. This
handles the Hole-hoisting path and any future path producing a
bool-typed identifier uniformly.

Adds regression fixtures under StrataTestExtra/Languages/Python/Issue1102/
and a Lean test harness that asserts the translation pipeline produces
no type-check or Laurel-to-Core crash on the minimal fuzz reproductions
from issue strata-org#1102 (seed 1777894483, fuzz_semantic_0004).
@julesmt julesmt force-pushed the fix/isinstance-assert-1102 branch from 55adf3c to 8a1c7b0 Compare May 5, 2026 17:37
@julesmt
Copy link
Copy Markdown
Member Author

julesmt commented May 5, 2026

@tautschnig I think I understood what you wanted, I modified the Test.lean, is it ok now?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants