Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion implementations/go/provekit-lift-go/lift.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ func liftFunc(fset *token.FileSet, file *ast.File, pkg *types.Package, info *typ
if err != nil {
return FunctionContract{}, nil, refuse("unsupported-syntax", errPos(err, fn.Pos()), err.Error())
}
pre, err := formulaValue(ir.And())
pre, err := formulaValue(l.liftLeadingGuardPreconditions(fn.Body.List))
if err != nil {
return FunctionContract{}, nil, refuse("internal-error", fn.Pos(), err.Error())
}
Expand Down Expand Up @@ -353,6 +353,66 @@ func liftFunc(fset *token.FileSet, file *ast.File, pkg *types.Package, info *typ
return contract, body.term, nil
}

// liftLeadingGuardPreconditions lifts a function's leading defensive guards
// (`if <cond> { panic(...) }`) into a precondition. A guard panics when <cond>
// holds, so the caller's obligation to avoid the panic is ¬<cond>. This brings
// the Go production lifter to parity with the Rust (lift_function_precondition)
// and C# (LiftMethodPrecondition) walkers; without it every function lifted
// `pre = true`, so a consumer's defensive contract was invisible at call sites.
// Scans only leading guards and stops at the first non-guard statement, so it
// never fabricates a precondition from arbitrary control flow.
func (l *lifter) liftLeadingGuardPreconditions(stmts []ast.Stmt) ir.IrFormula {
var atoms []ir.IrFormula
for _, stmt := range stmts {
ifStmt, ok := stmt.(*ast.IfStmt)
if !ok || ifStmt.Init != nil || ifStmt.Else != nil || !isPanicOnlyBlock(ifStmt.Body) {
break
}
atom, ok := l.guardPreconditionAtom(ifStmt.Cond)
if !ok {
break
}
atoms = append(atoms, atom)
}
return ir.And(atoms...)
}

// guardPreconditionAtom lifts ¬cond as a precondition atom. `if !P { panic }`
// lifts to the clean `P == true`; a bare `if cond { panic }` lifts to
// `cond == false`. Returns false when the condition is not liftable, so the
// caller refuses to invent a precondition rather than emit a wrong one.
func (l *lifter) guardPreconditionAtom(cond ast.Expr) (ir.IrFormula, bool) {
if unary, ok := cond.(*ast.UnaryExpr); ok && unary.Op == token.NOT {
inner, err := l.liftExpr(unary.X)
if err != nil {
return nil, false
}
return ir.Eq(inner.term, ir.BoolConst(true)), true
}
lifted, err := l.liftExpr(cond)
if err != nil {
return nil, false
}
return ir.Eq(lifted.term, ir.BoolConst(false)), true
}

// isPanicOnlyBlock reports whether a block is exactly `{ panic(...) }`.
func isPanicOnlyBlock(b *ast.BlockStmt) bool {
if b == nil || len(b.List) != 1 {
return false
}
exprStmt, ok := b.List[0].(*ast.ExprStmt)
if !ok {
return false
}
call, ok := exprStmt.X.(*ast.CallExpr)
if !ok {
return false
}
ident, ok := call.Fun.(*ast.Ident)
return ok && ident.Name == "panic"
Comment on lines +412 to +413
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve panic before lifting a guard

When a package-level symbol or parameter named panic shadows the predeclared builtin (for example func F(panic func(string), x int) int { if x < 0 { panic("bad") }; return x }), this syntactic name check treats the callback call as an aborting guard and emits x >= 0 as a precondition even though the call may return normally. The rest of the lifter already resolves callees through types.Info and would not record a panics effect for this shadowed call, so the new precondition is inconsistent with the modeled body; check that the callee resolves to the builtin panic before lifting the guard.

Useful? React with 👍 / 👎.

}

func extractFormals(info *types.Info, fn *ast.FuncDecl) ([]string, []any, map[types.Object]bool, error) {
locals := map[types.Object]bool{}
var names []string
Expand Down
Loading