diff --git a/Strata/Languages/Laurel/ConstrainedTypeElim.lean b/Strata/Languages/Laurel/ConstrainedTypeElim.lean new file mode 100644 index 000000000..86751ccf3 --- /dev/null +++ b/Strata/Languages/Laurel/ConstrainedTypeElim.lean @@ -0,0 +1,257 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +import Strata.Languages.Laurel.Laurel +import Strata.Languages.Laurel.Resolution + +/-! +# Constrained Type Elimination + +A Laurel-to-Laurel pass that eliminates constrained types by: +1. Generating a constraint function per constrained type (e.g. `nat$constraint(x: int): bool`) +2. Adding `requires constraintFunc(param)` for constrained-typed inputs +3. Adding `ensures constraintFunc(result)` for constrained-typed outputs + - Skipped for `isFunctional` procedures since the Laurel translator does not yet support + function postconditions. Constrained return types on functions are not checked. +4. Inserting `assert constraintFunc(var)` for local variable init and reassignment +5. Assuming the constraint for uninitialized constrained-typed variables (havoc + assume) +6. Adding a synthetic witness-validation procedure per constrained type +7. Injecting constraint function calls into quantifier bodies (`forall` → `implies`, `exists` → `and`) +8. Resolving all constrained type references to their base types +-/ + +namespace Strata.Laurel + +open Strata + +abbrev ConstrainedTypeMap := Std.HashMap String ConstrainedType +/-- Map from variable name to its constrained HighType (e.g. UserDefined "nat") -/ +abbrev PredVarMap := Std.HashMap String HighType + +def buildConstrainedTypeMap (types : List TypeDefinition) : ConstrainedTypeMap := + types.foldl (init := {}) fun m td => + match td with | .Constrained ct => m.insert ct.name.text ct | _ => m + +partial def resolveBaseType (ptMap : ConstrainedTypeMap) (ty : HighType) : HighType := + match ty with + | .UserDefined name => match ptMap.get? name.text with + | some ct => resolveBaseType ptMap ct.base.val | none => ty + | .Applied ctor args => + .Applied ctor (args.map fun a => ⟨resolveBaseType ptMap a.val, a.md⟩) + | _ => ty + +def resolveType (ptMap : ConstrainedTypeMap) (ty : HighTypeMd) : HighTypeMd := + ⟨resolveBaseType ptMap ty.val, ty.md⟩ + +def isConstrainedType (ptMap : ConstrainedTypeMap) (ty : HighType) : Bool := + match ty with | .UserDefined name => ptMap.contains name.text | _ => false + +/-- Build a call to the constraint function for a constrained type, or `none` if not constrained -/ +def constraintCallFor (ptMap : ConstrainedTypeMap) (ty : HighType) + (varName : Identifier) (md : Imperative.MetaData Core.Expression) : Option StmtExprMd := + match ty with + | .UserDefined name => if ptMap.contains name.text then + some ⟨.StaticCall (mkId s!"{name.text}$constraint") [⟨.Identifier varName, md⟩], md⟩ + else none + | _ => none + +/-- Generate a constraint function for a constrained type. + For nested types, the function calls the parent's constraint function. -/ +def mkConstraintFunc (ptMap : ConstrainedTypeMap) (ct : ConstrainedType) : Procedure := + let md : Imperative.MetaData Core.Expression := #[] + let baseType := resolveType ptMap ct.base + let bodyExpr := match ct.base.val with + | .UserDefined parent => + if ptMap.contains parent.text then + let parentCall : StmtExprMd := + ⟨.StaticCall (mkId s!"{parent.text}$constraint") [⟨.Identifier { ct.valueName with uniqueId := none }, md⟩], md⟩ + ⟨.PrimitiveOp .And [ct.constraint, parentCall], md⟩ + else ct.constraint + | _ => ct.constraint + { name := mkId s!"{ct.name.text}$constraint" + inputs := [{ name := ct.valueName, type := { baseType with md := #[] } }] + outputs := [{ name := mkId "result", type := ⟨.TBool, #[]⟩ }] + body := .Transparent ⟨.Block [bodyExpr] none, #[]⟩ + isFunctional := true + determinism := .deterministic none + decreases := none + preconditions := [] + md := #[] } + +private def wrap (stmts : List StmtExprMd) (md : Imperative.MetaData Core.Expression) + : StmtExprMd := + match stmts with | [s] => s | ss => ⟨.Block ss none, md⟩ + +/-- Resolve constrained types in all type positions of an expression, + and inject constraint function calls into quantifier bodies -/ +def resolveExpr (ptMap : ConstrainedTypeMap) : StmtExprMd → StmtExprMd + | ⟨.LocalVariable n ty (some init), md⟩ => + ⟨.LocalVariable n (resolveType ptMap ty) (some (resolveExpr ptMap init)), md⟩ + | ⟨.LocalVariable n ty none, md⟩ => + ⟨.LocalVariable n (resolveType ptMap ty) none, md⟩ + | ⟨.Forall param body, md⟩ => + let body' := resolveExpr ptMap body + let param' := { param with type := resolveType ptMap param.type } + let injected := match constraintCallFor ptMap param.type.val param.name md with + | some c => ⟨.PrimitiveOp .Implies [c, body'], md⟩ + | none => body' + ⟨.Forall param' injected, md⟩ + | ⟨.Exists param body, md⟩ => + let body' := resolveExpr ptMap body + let param' := { param with type := resolveType ptMap param.type } + let injected := match constraintCallFor ptMap param.type.val param.name md with + | some c => ⟨.PrimitiveOp .And [c, body'], md⟩ + | none => body' + ⟨.Exists param' injected, md⟩ + | ⟨.AsType t ty, md⟩ => ⟨.AsType (resolveExpr ptMap t) (resolveType ptMap ty), md⟩ + | ⟨.IsType t ty, md⟩ => ⟨.IsType (resolveExpr ptMap t) (resolveType ptMap ty), md⟩ + | ⟨.PrimitiveOp op args, md⟩ => + ⟨.PrimitiveOp op (args.attach.map fun ⟨a, _⟩ => resolveExpr ptMap a), md⟩ + | ⟨.StaticCall c args, md⟩ => + ⟨.StaticCall c (args.attach.map fun ⟨a, _⟩ => resolveExpr ptMap a), md⟩ + | ⟨.Block ss sep, md⟩ => + ⟨.Block (ss.attach.map fun ⟨s, _⟩ => resolveExpr ptMap s) sep, md⟩ + | ⟨.IfThenElse c t (some el), md⟩ => + ⟨.IfThenElse (resolveExpr ptMap c) (resolveExpr ptMap t) (some (resolveExpr ptMap el)), md⟩ + | ⟨.IfThenElse c t none, md⟩ => + ⟨.IfThenElse (resolveExpr ptMap c) (resolveExpr ptMap t) none, md⟩ + | ⟨.While c inv dec body, md⟩ => + ⟨.While (resolveExpr ptMap c) (inv.attach.map fun ⟨i, _⟩ => resolveExpr ptMap i) + dec (resolveExpr ptMap body), md⟩ + | ⟨.Assign ts v, md⟩ => + ⟨.Assign (ts.attach.map fun ⟨t, _⟩ => resolveExpr ptMap t) (resolveExpr ptMap v), md⟩ + | ⟨.Return (some v), md⟩ => ⟨.Return (some (resolveExpr ptMap v)), md⟩ + | ⟨.Return none, md⟩ => ⟨.Return none, md⟩ + | ⟨.Assert c, md⟩ => ⟨.Assert (resolveExpr ptMap c), md⟩ + | ⟨.Assume c, md⟩ => ⟨.Assume (resolveExpr ptMap c), md⟩ + | e => e +termination_by e => sizeOf e +decreasing_by all_goals (have := WithMetadata.sizeOf_val_lt ‹_›; term_by_mem) + +abbrev ElimM := StateM PredVarMap + +private def inScope (action : ElimM α) : ElimM α := do + let saved ← get + let result ← action + set saved + return result + +def elimStmt (ptMap : ConstrainedTypeMap) + (stmt : StmtExprMd) : ElimM (List StmtExprMd) := do + let md := stmt.md + match _h : stmt.val with + | .LocalVariable name ty init => + let callOpt := constraintCallFor ptMap ty.val name md + if callOpt.isSome then modify fun pv => pv.insert name.text ty.val + let (init', check) : Option StmtExprMd × List StmtExprMd := match init with + | none => match callOpt with + | some c => (none, [⟨.Assume c, md⟩]) + | none => (none, []) + | some _ => (init, callOpt.toList.map fun c => ⟨.Assert c, md⟩) + pure ([⟨.LocalVariable name ty init', md⟩] ++ check) + + | .Assign [target] _ => match target.val with + | .Identifier name => do + match (← get).get? name.text with + | some ty => + let assert := (constraintCallFor ptMap ty name md).toList.map + fun c => ⟨.Assert c, md⟩ + pure ([stmt] ++ assert) + | none => pure [stmt] + | _ => pure [stmt] + + | .Block stmts sep => + let stmtss ← inScope (stmts.mapM (elimStmt ptMap)) + pure [⟨.Block stmtss.flatten sep, md⟩] + + | .IfThenElse cond thenBr (some elseBr) => + let thenSs ← inScope (elimStmt ptMap thenBr) + let elseSs ← inScope (elimStmt ptMap elseBr) + pure [⟨.IfThenElse cond (wrap thenSs md) (some (wrap elseSs md)), md⟩] + | .IfThenElse cond thenBr none => + let thenSs ← inScope (elimStmt ptMap thenBr) + pure [⟨.IfThenElse cond (wrap thenSs md) none, md⟩] + + | .While cond inv dec body => + let bodySs ← inScope (elimStmt ptMap body) + pure [⟨.While cond inv dec (wrap bodySs md), md⟩] + + | _ => pure [stmt] +termination_by sizeOf stmt +decreasing_by + all_goals simp_wf + all_goals (try have := WithMetadata.sizeOf_val_lt stmt) + all_goals (try term_by_mem) + all_goals omega + +def elimProc (ptMap : ConstrainedTypeMap) (proc : Procedure) : Procedure := + let inputRequires := proc.inputs.filterMap fun p => + constraintCallFor ptMap p.type.val p.name p.type.md + let outputEnsures := if proc.isFunctional then [] else proc.outputs.filterMap fun p => + (constraintCallFor ptMap p.type.val p.name p.type.md).map + fun c => ⟨c.val, p.type.md⟩ + let initVars : PredVarMap := proc.inputs.foldl (init := {}) fun s p => + if isConstrainedType ptMap p.type.val then s.insert p.name.text p.type.val else s + let body' := match proc.body with + | .Transparent bodyExpr => + let (stmts, _) := (elimStmt ptMap bodyExpr).run initVars + let body := wrap stmts bodyExpr.md + if outputEnsures.isEmpty then .Transparent body + else + let retBody := if proc.isFunctional then ⟨.Return (some body), bodyExpr.md⟩ else body + .Opaque outputEnsures (some retBody) [] + | .Opaque postconds impl modif => + let impl' := impl.map fun b => wrap ((elimStmt ptMap b).run initVars).1 b.md + .Opaque (postconds ++ outputEnsures) impl' modif + | .Abstract postconds => .Abstract (postconds ++ outputEnsures) + | .External => .External + let resolve := resolveExpr ptMap + let resolveBody : Body → Body := fun body => match body with + | .Transparent b => .Transparent (resolve b) + | .Opaque ps impl modif => .Opaque (ps.map resolve) (impl.map resolve) (modif.map resolve) + | .Abstract ps => .Abstract (ps.map resolve) + | .External => .External + { proc with + body := resolveBody body' + inputs := proc.inputs.map fun p => { p with type := resolveType ptMap p.type } + outputs := proc.outputs.map fun p => { p with type := resolveType ptMap p.type } + preconditions := (proc.preconditions ++ inputRequires).map resolve } + +private def mkWitnessProc (ptMap : ConstrainedTypeMap) (ct : ConstrainedType) : Procedure := + let md := ct.witness.md + let witnessId : Identifier := mkId "$witness" + let witnessInit : StmtExprMd := + ⟨.LocalVariable witnessId (resolveType ptMap ct.base) (some ct.witness), md⟩ + let assert : StmtExprMd := + ⟨.Assert (constraintCallFor ptMap (.UserDefined ct.name) witnessId md).get!, md⟩ + { name := mkId s!"$witness_{ct.name.text}" + inputs := [] + outputs := [] + body := .Transparent ⟨.Block [witnessInit, assert] none, md⟩ + preconditions := [] + isFunctional := false + determinism := .deterministic none + decreases := none + md := md } + +def constrainedTypeElim (_model : SemanticModel) (program : Program) : Program × Array DiagnosticModel := + let ptMap := buildConstrainedTypeMap program.types + if ptMap.isEmpty then (program, #[]) else + let constraintFuncs := program.types.filterMap fun + | .Constrained ct => some (mkConstraintFunc ptMap ct) | _ => none + let witnessProcedures := program.types.filterMap fun + | .Constrained ct => some (mkWitnessProc ptMap ct) | _ => none + let funcDiags := program.staticProcedures.foldl (init := #[]) fun acc proc => + if proc.isFunctional && proc.outputs.any (fun p => isConstrainedType ptMap p.type.val) then + acc.push (proc.md.toDiagnostic "constrained return types on functions are not yet supported") + else acc + ({ program with + staticProcedures := constraintFuncs ++ program.staticProcedures.map (elimProc ptMap) + ++ witnessProcedures + types := program.types.filter fun | .Constrained _ => false | _ => true }, + funcDiags) + +end Strata.Laurel diff --git a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean index 2ada05bab..75eaf50ca 100644 --- a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean +++ b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean @@ -495,6 +495,20 @@ def parseDatatype (arg : Arg) : TransM TypeDefinition := do | _, _ => TransM.error s!"parseDatatype expects datatype, got {repr op.name}" +def parseConstrainedType (arg : Arg) : TransM ConstrainedType := do + let .op op := arg + | TransM.error s!"parseConstrainedType expects operation" + match op.name, op.args with + | q`Laurel.constrainedType, #[nameArg, valueNameArg, baseArg, constraintArg, witnessArg] => + let name ← translateIdent nameArg + let valueName ← translateIdent valueNameArg + let base ← translateHighType baseArg + let constraint ← translateStmtExpr constraintArg + let witness ← translateStmtExpr witnessArg + return { name, base, valueName, constraint, witness } + | _, _ => + TransM.error s!"parseConstrainedType expects constrainedType, got {repr op.name}" + def parseTopLevel (arg : Arg) : TransM (Option Procedure × Option TypeDefinition) := do let .op op := arg | TransM.error s!"parseTopLevel expects operation" @@ -509,8 +523,11 @@ def parseTopLevel (arg : Arg) : TransM (Option Procedure × Option TypeDefinitio | q`Laurel.topLevelDatatype, #[datatypeArg] => let typeDef ← parseDatatype datatypeArg return (none, some typeDef) + | q`Laurel.topLevelConstrainedType, #[ctArg] => + let ct ← parseConstrainedType ctArg + return (none, some (.Constrained ct)) | _, _ => - TransM.error s!"parseTopLevel expects topLevelProcedure, topLevelComposite, or topLevelDatatype, got {repr op.name}" + TransM.error s!"parseTopLevel expects topLevelProcedure, topLevelComposite, topLevelDatatype, or topLevelConstrainedType, got {repr op.name}" /-- Translate concrete Laurel syntax into abstract Laurel syntax diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean index 282254daa..632e0a69b 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean @@ -7,7 +7,6 @@ -- Laurel dialect definition, loaded from LaurelGrammar.st -- NOTE: Changes to LaurelGrammar.st are not automatically tracked by the build system. -- Update this file (e.g. this comment) to trigger a recompile after modifying LaurelGrammar.st. --- Last grammar change: require semicolon after procedure/function definitions. import Strata.DDM.Integration.Lean namespace Strata.Laurel diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st index 720d64b86..406e704bb 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st @@ -166,4 +166,10 @@ op topLevelComposite(composite: Composite): TopLevel => composite; op topLevelProcedure(procedure: Procedure): TopLevel => procedure; op topLevelDatatype(datatype: Datatype): TopLevel => datatype; +category ConstrainedType; +op constrainedType (name: Ident, valueName: Ident, base: LaurelType, + constraint: StmtExpr, witness: StmtExpr): ConstrainedType + => "constrained " name " = " valueName ": " base " where " constraint:0 " witness " witness:0; +op topLevelConstrainedType(ct: ConstrainedType): TopLevel => ct; + op program (items: Seq TopLevel): Command => items; \ No newline at end of file diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean index 5f9babbe1..7308d70dc 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean @@ -21,6 +21,7 @@ import Strata.DL.Imperative.Stmt import Strata.DL.Imperative.MetaData import Strata.DL.Lambda.LExpr import Strata.Languages.Laurel.LaurelFormat +import Strata.Languages.Laurel.ConstrainedTypeElim import Strata.Util.Tactics open Core (VCResult VCResults VerifyOptions) @@ -619,6 +620,11 @@ def translate (program : Program): Except (Array DiagnosticModel) (Core.Program let (program, model) := (result.program, result.model) _resolutionDiags := _resolutionDiags ++ result.errors + let (program, constrainedTypeDiags) := constrainedTypeElim model program + let result := resolve program (some model) + let (program, model) := (result.program, result.model) + _resolutionDiags := _resolutionDiags ++ result.errors + -- Procedures marked isFunctional are translated to Core functions; all others become Core procedures. -- External procedures are completely ignored (not translated to Core). let nonExternal := program.staticProcedures.filter (fun p => !p.body.isExternal) @@ -667,7 +673,7 @@ def translate (program : Program): Except (Array DiagnosticModel) (Core.Program -- dbg_trace "=== Generated Strata Core Program ===" -- dbg_trace (toString (Std.Format.pretty (Strata.Core.formatProgram program) 100)) -- dbg_trace "=================================" - pure (program, diamondErrors ++ modifiesDiags) + pure (program, diamondErrors ++ modifiesDiags ++ constrainedTypeDiags.toList) /-- Verify a Laurel program using an SMT solver diff --git a/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean new file mode 100644 index 000000000..a3a686b4a --- /dev/null +++ b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean @@ -0,0 +1,130 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +/- +Tests that the constrained type elimination pass correctly transforms +Laurel programs by comparing the output against expected results. +-/ + +import Strata.DDM.Elab +import Strata.DDM.BuiltinDialects.Init +import Strata.Languages.Laurel.Grammar.LaurelGrammar +import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator +import Strata.Languages.Laurel.ConstrainedTypeElim +import Strata.Languages.Laurel.Resolution + +open Strata +open Strata.Elab (parseStrataProgramFromDialect) + +namespace Strata.Laurel + +def testProgram : String := r" +constrained nat = x: int where x >= 0 witness 0 +procedure test(n: nat) returns (r: nat) { + assert r >= 0; + var y: nat := n; + return y; +}; +" + +def parseLaurelAndElim (input : String) : IO Program := do + let inputCtx := Strata.Parser.stringInputContext "test" input + let dialects := Strata.Elab.LoadedDialects.ofDialects! #[initDialect, Laurel] + let strataProgram ← parseStrataProgramFromDialect dialects Laurel.name inputCtx + let uri := Strata.Uri.file "test" + match Laurel.TransM.run uri (Laurel.parseProgram strataProgram) with + | .error e => throw (IO.userError s!"Translation errors: {e}") + | .ok program => + let result := resolve program + let (program, model) := (result.program, result.model) + pure (constrainedTypeElim model program).1 + +/-- +info: function nat$constraint(x: int) returns ⏎ +(result: bool) +deterministic +{ x >= 0 } +procedure test(n: int) returns ⏎ +(r: int) +requires nat$constraint(n) +deterministic + ensures nat$constraint(r) := { assert r >= 0; var y: int := n; assert nat$constraint(y); return y } +procedure $witness_nat() returns ⏎ +() +deterministic +{ var $witness: int := 0; assert nat$constraint($witness) } +-/ +#guard_msgs in +#eval! do + let program ← parseLaurelAndElim testProgram + for proc in program.staticProcedures do + IO.println (toString (Std.Format.pretty (Std.ToFormat.format proc))) + +-- Scope management: constrained variable in if-branch must not leak into sibling block +def scopeProgram : String := r" +constrained pos = v: int where v > 0 witness 1 +procedure test(b: bool) { + if (b) { + var x: pos := 1; + } + { + var x: int := -5; + x := -10; + } +}; +" + +/-- +info: function pos$constraint(v: int) returns ⏎ +(result: bool) +deterministic +{ v > 0 } +procedure test(b: bool) returns ⏎ +() +deterministic +{ if b then { var x: int := 1; assert pos$constraint(x) }; { var x: int := -5; x := -10 } } +procedure $witness_pos() returns ⏎ +() +deterministic +{ var $witness: int := 1; assert pos$constraint($witness) } +-/ +#guard_msgs in +#eval! do + let program ← parseLaurelAndElim scopeProgram + for proc in program.staticProcedures do + IO.println (toString (Std.Format.pretty (Std.ToFormat.format proc))) + +-- Uninitialized constrained variable: havoc + assume constraint. +-- The variable has no known value, only the type constraint is assumed. +def uninitProgram : String := r" +constrained posint = x: int where x > 0 witness 1 +procedure f() { + var x: posint; + assert x == 1; +}; +" + +/-- +info: function posint$constraint(x: int) returns ⏎ +(result: bool) +deterministic +{ x > 0 } +procedure f() returns ⏎ +() +deterministic +{ var x: int; assume posint$constraint(x); assert x == 1 } +procedure $witness_posint() returns ⏎ +() +deterministic +{ var $witness: int := 1; assert posint$constraint($witness) } +-/ +#guard_msgs in +#eval! do + let program ← parseLaurelAndElim uninitProgram + for proc in program.staticProcedures do + IO.println (toString (Std.Format.pretty (Std.ToFormat.format proc))) + +end Laurel diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean index 33fcb29b0..2ae7e692e 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean @@ -8,23 +8,168 @@ import StrataTest.Util.TestDiagnostics import StrataTest.Languages.Laurel.TestExamples open StrataTest.Util -open Strata +namespace Strata namespace Laurel def program := r" constrained nat = x: int where x >= 0 witness 0 +constrained posnat = x: nat where x != 0 witness 1 -composite Option {} -composite Some extends Option { - value: int -} -composite None extends Option -constrained SealedOption = x: Option where x is Some || x is None witness None +// Input constraint becomes requires — body can rely on it +procedure inputAssumed(n: nat) { + assert n >= 0; +}; + +// Output constraint — valid return passes +procedure outputValid(): nat { + return 3; +}; + +// Output constraint — invalid return fails +procedure outputInvalid(): nat { +// ^^^ error: assertion does not hold + return -1; +}; + +// Return value of constrained type — caller gets ensures via call elimination +procedure opaqueNat(): nat; +procedure callerAssumes() returns (r: int) { + var x: int := opaqueNat(); + assert x >= 0; + return x; +}; + +// Assignment to constrained-typed variable — valid +procedure assignValid() { + var y: nat := 5; +}; + +// Assignment to constrained-typed variable — invalid +procedure assignInvalid() { + var y: nat := -1; +//^^^^^^^^^^^^^^^^^ error: assertion does not hold +}; -procedure foo() returns (r: nat) { +// Reassignment to constrained-typed variable — invalid +procedure reassignInvalid() { + var y: nat := 5; + y := -1; +//^^^^^^^^ error: assertion does not hold +}; + +// Argument to constrained-typed parameter — valid +procedure takesNat(n: nat) returns (r: int) { return n; }; +// ^^^ error: assertion does not hold +procedure argValid() returns (r: int) { + var x: int := takesNat(3); + return x; +}; + +// Argument to constrained-typed parameter — invalid (requires violation) +procedure argInvalid() returns (r: int) { + var x: int := takesNat(-1); + return x; +}; + +// Nested constrained type — independent constraints require transitive collection +constrained even = x: int where x % 2 == 0 witness 0 +constrained evenpos = x: even where x > 0 witness 2 +procedure nestedInput(x: evenpos) { + assert x > 0; + assert x % 2 == 0; +}; + +// Multiple constrained-typed parameters +procedure multiParam(a: nat, b: nat) { + assert a >= 0; + assert b >= 0; +}; + +// Two calls to same procedure — no temp var collision +procedure twoCalls() returns (r: int) { + var a: int := takesNat(1); + var b: int := takesNat(2); + return a + b; +}; + +// Constrained type in expression position must be resolved +procedure constrainedInExpr() { + var b: bool := forall(n: nat) => n + 1 > n; + assert b; +}; + +// Invalid witness — witness -1 does not satisfy x > 0 +constrained bad = x: int where x > 0 witness -1 +// ^^ error: assertion does not hold + +// Uninitialized constrained variable — havoc + assume constraint +procedure uninitNat() { + var y: nat; + assert y >= 0; +}; + +// Uninitialized nested constrained variable — havoc + assume constraint +procedure uninitPosnat() { + var y: posnat; + assert y != 0; + assert y >= 0; +}; + +// Uninitialized constrained variable — witness value is not provable +procedure uninitNotWitness() { + var y: posnat; + assert y == 1; +//^^^^^^^^^^^^^^ error: assertion does not hold +}; + +// Function with valid constrained return — constraint not checked (not yet supported) +function goodFunc(): nat { 3 }; +// ^^^^^^^^ error: constrained return types on functions are not yet supported + +// Function with invalid constrained return — constraint not checked (not yet supported) +function badFunc(): nat { -1 }; +// ^^^^^^^ error: constrained return types on functions are not yet supported + +// Caller of constrained function — body is inlined, caller sees actual value +procedure callerGood() { + var x: int := goodFunc(); + assert x >= 0; +}; + +// Quantifier constraint injection — forall +// n + 1 > 0 is only provable with n >= 0 injected; false for all int +procedure forallNat() { + var b: bool := forall(n: nat) => n + 1 > 0; + assert b; +}; + +// Quantifier constraint injection — exists +// n == -1 is satisfiable for int, but not when n >= 0 is required +// n == 42 works because 42 >= 0 +procedure existsNat() { + var b: bool := exists(n: nat) => n == 42; + assert b; +}; + +// Quantifier constraint injection — nested constrained type +// n - 1 >= 0 is only provable with n > 0 injected +procedure forallPosnat() { + var b: bool := forall(n: posnat) => n - 1 >= 0; + assert b; +}; + +// Capture avoidance — bound var y in constraint must not collide with parameter y +// Without capture avoidance, requires becomes exists(y) => y > y (false), making body vacuously true +constrained haslarger = x: int where (exists(y: int) => y > x) witness 0 +procedure captureTest(y: haslarger) { + assert false; +//^^^^^^^^^^^^^ error: assertion does not hold }; " --- Not working yet --- #eval! testInput "ConstrainedTypes" program processLaurelFile +#guard_msgs(drop info, error) in +#eval testInputWithOffset "ConstrainedTypes" program 14 processLaurelFile + +end Laurel +end Strata