From 85ddf71503414b02e249abe67eefcc0deb49e225 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 9 Jun 2026 13:32:32 +0000 Subject: [PATCH 01/42] Remove isFunctional property from Laurel.Procedure --- .../Languages/Laurel/ConstrainedTypeElim.lean | 13 +---- Strata/Languages/Laurel/ContractPass.lean | 56 ++++++++----------- Strata/Languages/Laurel/EliminateHoles.lean | 3 +- .../AbstractToConcreteTreeTranslator.lean | 12 +--- .../ConcreteToAbstractTreeTranslator.lean | 7 --- .../Languages/Laurel/Grammar/LaurelGrammar.st | 9 --- Strata/Languages/Laurel/LaurelAST.lean | 2 - .../Laurel/LaurelCompilationPipeline.lean | 8 ++- .../Laurel/LaurelToCoreTranslator.lean | 15 +++-- .../Laurel/LiftImperativeExpressions.lean | 21 ------- Strata/Languages/Laurel/Resolution.lean | 47 +--------------- Strata/Languages/Laurel/TransparencyPass.lean | 6 +- 12 files changed, 52 insertions(+), 147 deletions(-) diff --git a/Strata/Languages/Laurel/ConstrainedTypeElim.lean b/Strata/Languages/Laurel/ConstrainedTypeElim.lean index 49cca37de7..deedf3ee39 100644 --- a/Strata/Languages/Laurel/ConstrainedTypeElim.lean +++ b/Strata/Languages/Laurel/ConstrainedTypeElim.lean @@ -79,7 +79,6 @@ def mkConstraintFunc (ptMap : ConstrainedTypeMap) (ct : ConstrainedType) : Proce inputs := [{ name := ct.valueName, type := baseType }] outputs := [{ name := mkId "result", type := { val := .TBool, source := none } }] body := .Transparent { val := .Block [bodyExpr] none, source := none } - isFunctional := true decreases := none preconditions := [] } @@ -184,7 +183,7 @@ def elimProc (ptMap : ConstrainedTypeMap) (proc : Procedure) : Procedure := let inputRequires : List Condition := proc.inputs.filterMap fun p => (constraintCallFor ptMap p.type.val p.name (src := p.type.source)).map fun c => { condition := c } - let outputEnsures : List Condition := if proc.isFunctional then [] else proc.outputs.filterMap fun p => + let outputEnsures : List Condition := proc.outputs.filterMap fun p => (constraintCallFor ptMap p.type.val p.name (src := p.type.source)).map fun c => { condition := ⟨c.val, p.type.source⟩ } let initVars : PredVarMap := proc.inputs.foldl (init := {}) fun s p => @@ -195,8 +194,7 @@ def elimProc (ptMap : ConstrainedTypeMap) (proc : Procedure) : Procedure := let body := wrap stmts bodyExpr.source if outputEnsures.isEmpty then .Transparent body else - let retBody := if proc.isFunctional then ⟨.Return (some body), bodyExpr.source⟩ else body - .Opaque outputEnsures (some retBody) [] + .Opaque outputEnsures (some body) [] | .Opaque postconds impl modif => let impl' := impl.map fun b => wrap ((elimStmt ptMap b).run initVars).1 b.source .Opaque (postconds ++ outputEnsures) impl' modif @@ -227,7 +225,6 @@ private def mkWitnessProc (ptMap : ConstrainedTypeMap) (ct : ConstrainedType) : outputs := [] body := .Opaque [] (some ⟨.Block [witnessInit, assert] none, src⟩) [] preconditions := [] - isFunctional := false decreases := none } public def constrainedTypeElim (_model : SemanticModel) (program : Program) @@ -238,14 +235,10 @@ public def constrainedTypeElim (_model : SemanticModel) (program : Program) | .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.cons (diagnosticFromSource proc.name.source "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/ContractPass.lean b/Strata/Languages/Laurel/ContractPass.lean index 0ae17de08e..61fb49a810 100644 --- a/Strata/Languages/Laurel/ContractPass.lean +++ b/Strata/Languages/Laurel/ContractPass.lean @@ -63,7 +63,6 @@ private def mkConditionProc (name : String) (params : List Parameter) outputs := [⟨mkId "$result", { val := .TBool, source := none }⟩] preconditions := [] decreases := none - isFunctional := true body := .Transparent condition.condition } /-- Build a postcondition function for a single condition that takes all inputs @@ -77,7 +76,6 @@ private def mkPostConditionProc (name : String) outputs := [⟨mkId "$result", { val := .TBool, source := none }⟩] preconditions := [] decreases := none - isFunctional := true body := .Transparent condition.condition } /-- Information about a procedure's contracts. -/ @@ -96,7 +94,7 @@ private def collectContractInfo (procs : List Procedure) : Std.HashMap String Co let postconds := getPostconditions proc.body let hasPre := !proc.preconditions.isEmpty let hasPost := !postconds.isEmpty - if !proc.isFunctional && (hasPre || hasPost) then + if hasPre || hasPost then let preNames := proc.preconditions.zipIdx.map fun (c, i) => (preCondProcName proc.name.text i, c.summary) let postNames := postconds.zipIdx.map fun (c, i) => @@ -150,15 +148,12 @@ private def mkTempAssignments (args : List StmtExprMd) (calleeName : String) (decls, refs) /-- Generate precondition checks (one per precondition) for a call site. -/ -private def mkPreChecks (info : ContractInfo) (isFunctional : Bool) +private def mkPreChecks (info : ContractInfo) (tempRefs : List StmtExprMd) (src : Option FileRange) : List StmtExprMd := if !info.hasPreCondition then [] else info.preNames.map fun (name, summary) => let call := mkCall name tempRefs - if isFunctional then - ⟨.Assume call, src⟩ - else - ⟨.Assert { condition := call, summary := some (summary.getD "precondition") }, src⟩ + ⟨.Assert { condition := call, summary := some (summary.getD "precondition") }, src⟩ /-- Generate postcondition assumes (one per postcondition) for a call site. -/ private def mkPostAssumes (info : ContractInfo) @@ -169,12 +164,12 @@ private def mkPostAssumes (info : ContractInfo) /-- Rewrite call sites in a statement/expression tree. -/ private def rewriteCallSites (contractInfoMap : Std.HashMap String ContractInfo) - (isFunctional : Bool) (expr : StmtExprMd) : StmtExprMd := + (expr : StmtExprMd) : StmtExprMd := let rewriteStaticCall (counter : Nat) (callee : Identifier) (args : List StmtExprMd) (info : ContractInfo) (src : Option FileRange) : List StmtExprMd × Nat := let (tempDecls, tempRefs) := mkTempAssignments args callee.text info.inputParams counter src - let preCheck := mkPreChecks info isFunctional tempRefs src + let preCheck := mkPreChecks info tempRefs src let (callStmt, postAssume, returnValue) := if info.hasPostCondition && !info.outputParams.isEmpty then let outputTempDecls := info.outputParams.zipIdx.map fun (p, i) => @@ -217,7 +212,7 @@ private def rewriteCallSites (contractInfoMap : Std.HashMap String ContractInfo) | _ => return e')) let (tempDecls, tempRefs) := mkTempAssignments args' callee.text info.inputParams counter src let callWithTemps : StmtExprMd := ⟨.Assign targets ⟨.StaticCall callee tempRefs, callSrc⟩, src⟩ - let preCheck := mkPreChecks info isFunctional tempRefs src + let preCheck := mkPreChecks info tempRefs src let outputArgs := targets.filterMap fun t => match t.val with | .Local name => some (mkMd (.Var (.Local name))) @@ -245,7 +240,7 @@ private def rewriteCallSites (contractInfoMap : Std.HashMap String ContractInfo) /-- Rewrite call sites in all bodies of a procedure. -/ private def rewriteCallSitesInProc (contractInfoMap : Std.HashMap String ContractInfo) (proc : Procedure) : Procedure := - let rw := rewriteCallSites contractInfoMap proc.isFunctional + let rw := rewriteCallSites contractInfoMap match proc.body with | .Transparent body => { proc with body := .Transparent (rw body) } @@ -275,7 +270,7 @@ def contractPass (program : Program) : Program := let contractInfoMap := collectContractInfo program.staticProcedures -- Generate helper procedures for all procedures with contracts - let helperProcs := (program.staticProcedures.filter (fun proc => !proc.isFunctional)).flatMap fun proc => + let helperProcs := program.staticProcedures.flatMap fun proc => let postconds := getPostconditions proc.body let preProcs := proc.preconditions.zipIdx.map fun (c, i) => mkConditionProc (preCondProcName proc.name.text i) proc.inputs c @@ -286,25 +281,22 @@ def contractPass (program : Program) : Program := -- Transform procedures: strip contracts, add assume/assert, rewrite call sites let transformedProcs := program.staticProcedures.map fun proc => - if proc.isFunctional then - proc - else - let proc := match proc.invokeOn with - | some trigger => - let postconds := getPostconditions proc.body - if postconds.isEmpty then { proc with invokeOn := none } - else { proc with - axioms := [mkInvokeOnAxiom proc.inputs trigger postconds] - invokeOn := none } - | none => proc - let proc := match contractInfoMap.get? proc.name.text with - | some info => - { proc with - preconditions := [] - body := transformProcBody proc info } - | none => proc - -- Rewrite call sites in the procedure body - rewriteCallSitesInProc contractInfoMap proc + let proc := match proc.invokeOn with + | some trigger => + let postconds := getPostconditions proc.body + if postconds.isEmpty then { proc with invokeOn := none } + else { proc with + axioms := [mkInvokeOnAxiom proc.inputs trigger postconds] + invokeOn := none } + | none => proc + let proc := match contractInfoMap.get? proc.name.text with + | some info => + { proc with + preconditions := [] + body := transformProcBody proc info } + | none => proc + -- Rewrite call sites in the procedure body + rewriteCallSitesInProc contractInfoMap proc { program with staticProcedures := helperProcs ++ transformedProcs } diff --git a/Strata/Languages/Laurel/EliminateHoles.lean b/Strata/Languages/Laurel/EliminateHoles.lean index 2d06622e47..ab356024db 100644 --- a/Strata/Languages/Laurel/EliminateHoles.lean +++ b/Strata/Languages/Laurel/EliminateHoles.lean @@ -35,7 +35,7 @@ structure ElimHoleState where private abbrev ElimHoleM := StateM ElimHoleState -/-- Generate a fresh uninterpreted function for a typed hole and return a call to it. -/ +/-- Generate a fresh procedure for a typed hole and return a call to it. -/ private def mkHoleCall (holeType : HighTypeMd) : ElimHoleM StmtExprMd := do let s ← get let n := s.counter @@ -48,7 +48,6 @@ private def mkHoleCall (holeType : HighTypeMd) : ElimHoleM StmtExprMd := do outputs := [{ name := "$result", type := holeType }] preconditions := [] decreases := none - isFunctional := true body := .Opaque [] none [] } modify fun s => { s with generatedFunctions := s.generatedFunctions ++ [holeProc] } diff --git a/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean b/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean index cd394d4ccf..36cb7d4514 100644 --- a/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean +++ b/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean @@ -218,7 +218,6 @@ private def modifiesClausesToArgs (modifies : List StmtExprMd) : Array Arg := wildcardArgs ++ specificArgs private def procedureToOp (proc : Procedure) : StrataDDM.Operation := - let opName := if proc.isFunctional then "function" else "procedure" let params := proc.inputs.map parameterToArg |>.toArray let returnTypeArg : Arg := match proc.outputs with @@ -241,14 +240,7 @@ private def procedureToOp (proc : Procedure) : StrataDDM.Operation := laurelOp "invokeOnClause" #[stmtExprToArg e]) let (opaqueSpecArg, bodyArg) := match proc.body with | .Transparent body => - -- For functions, the body is implicitly wrapped in a Return by ConcreteToAbstract; - -- unwrap it here so the concrete output doesn't show an explicit `return`. - let emitBody := if proc.isFunctional then - match body.val with - | .Return (some inner) => inner - | _ => body - else body - (optionArg none, optionArg (some (laurelOp "body" #[stmtExprToArg emitBody]))) + (optionArg none, optionArg (some (laurelOp "body" #[stmtExprToArg body]))) | .Opaque postconds impl modifies => let ens := postconds.map ensuresClauseToArg |>.toArray let mods := if modifies.isEmpty then #[] else modifiesClausesToArgs modifies @@ -260,7 +252,7 @@ private def procedureToOp (proc : Procedure) : StrataDDM.Operation := | .External => (optionArg none, optionArg (some (laurelOp "externalBody"))) { ann := sr - name := { dialect := "Laurel", name := opName } + name := { dialect := "Laurel", name := "procedure" } args := #[ ident proc.name.text, commaSep params, diff --git a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean index 67f9fa4a87..a6c8da613a 100644 --- a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean +++ b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean @@ -150,7 +150,6 @@ instance : Inhabited Procedure where outputs := [] preconditions := [] decreases := none - isFunctional := false invokeOn := none body := .Transparent { val := .LiteralBool true, source := none } } @@ -518,11 +517,6 @@ def parseProcedure (arg : Arg) : TransM Procedure := do | _, _ => TransM.error s!"Expected body or externalBody operation, got {repr bodyOp.name}" | .option _ none => pure none | _ => TransM.error s!"Expected body, got {repr bodyArg}" - -- For functions, wrap the body in a Return so the last expression - -- is treated as the return value by downstream passes. - let body := if op.name == q`Laurel.function then - body.map fun b => ⟨.Return (some b), b.source⟩ - else body -- Determine procedure body kind let procBody := if isExternal then Body.External @@ -536,7 +530,6 @@ def parseProcedure (arg : Arg) : TransM Procedure := do outputs := returnParameters preconditions := preconditions decreases := none - isFunctional := op.name == q`Laurel.function invokeOn := invokeOn body := procBody } diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st index b6166444ad..c58ca0f4d8 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st @@ -190,15 +190,6 @@ op procedure (name : Ident, parameters: CommaSepBy Parameter, body : Option Body) : Procedure => "procedure " name "(" parameters ")" returnType returnParameters requires invokeOn opaqueSpec body ";"; -op function (name : Ident, parameters: CommaSepBy Parameter, - returnType: Option ReturnType, - returnParameters: Option ReturnParameters, - requires: Seq RequiresClause, - invokeOn: Option InvokeOnClause, - opaqueSpec: Option OpaqueSpec, - body : Option Body) : Procedure => - "function " name "(" parameters ")" returnType returnParameters requires invokeOn opaqueSpec body ";"; - op composite (name: Ident, extending: Option Extends, fields: Seq Field, procedures: Seq Procedure): Composite => "composite " name extending " {" fields procedures " }"; category ConstrainedType; diff --git a/Strata/Languages/Laurel/LaurelAST.lean b/Strata/Languages/Laurel/LaurelAST.lean index 33f45b99fc..23716357e7 100644 --- a/Strata/Languages/Laurel/LaurelAST.lean +++ b/Strata/Languages/Laurel/LaurelAST.lean @@ -196,8 +196,6 @@ structure Procedure : Type where -- TODO: add back determinism together with an implementation /-- Optional termination measure for recursive procedures. -/ decreases : Option (AstNode StmtExpr) -- optionally prove termination - /-- If true, the body may only have functional constructs, so no destructive assignments or loops. -/ - isFunctional : Bool /-- The procedure body: transparent, opaque, or abstract. -/ body : Body /-- Optional trigger for auto-invocation. When present, the translator also emits an axiom diff --git a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean index 97266ab1d7..b7eb04e5a5 100644 --- a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean +++ b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean @@ -302,7 +302,13 @@ def translateWithLaurel (options : LaurelTranslateOptions) (program : Program) return (none, passDiags, program, stats) else emit "CoreWithLaurelTypes" "core.st" coreWithLaurelTypes - let initState : TranslateState := { model := fnModel, overflowChecks := options.overflowChecks } + let initState : TranslateState := { + model := fnModel, + overflowChecks := options.overflowChecks, + procedureNames := coreWithLaurelTypes.decls.foldl (fun r d => match d with + | .procedure p => r.insert p.name.text + | _ => r ) (Std.HashSet.emptyWithCapacity 0) + } let (coreProgramOption, translateState) := runTranslateM initState (translateLaurelToCore options coreWithLaurelTypes) -- Because of the duplication between functions and procedures, this translation is liable to create duplicate diagnostics diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean index a0814f9555..fe7b15e986 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean @@ -52,10 +52,15 @@ structure TranslateState where why the program was deemed invalid so that if no other diagnostics explain the suppression, these can be surfaced to the user. -/ coreDiagnostics : List DiagnosticModel := [] + procedureNames: Std.HashSet String /-- The translation monad: state over Except, allowing both accumulated diagnostics and hard failures -/ @[expose] abbrev TranslateM := OptionT (StateM TranslateState) +/-- Emit a diagnostic into the translation state (soft warning, does not abort) -/ +def containsProcedure (name : Identifier) : TranslateM Bool := do + return (← get).procedureNames.contains name.text + /-- Emit a diagnostic into the translation state (soft warning, does not abort) -/ def emitDiagnostic (d : DiagnosticModel) : TranslateM Unit := modify fun s => { s with diagnostics := s.diagnostics ++ [d] } @@ -220,7 +225,7 @@ def translateExpr (expr : StmtExprMd) return .ite () bcond bthen belse | .StaticCall callee args => -- In a pure context, only Core functions (not procedures) are allowed - if isPureContext && !model.isFunction callee then + if isPureContext && (← containsProcedure callee) then disallowed expr.source s!"calls to procedures are not supported in functions or contracts" else let fnOp : Core.Expression.Expr := .op () ⟨callee.text, ()⟩ none @@ -419,7 +424,9 @@ def translateStmt (stmt : StmtExprMd) -- Match on the value to decide how to translate match _hv : value.val with | .StaticCall callee args => - if model.isFunction callee then + if (← containsProcedure callee) then + translateCallTargets callee.text args + else -- Function call: translate as a normal expression assignment let coreExpr ← translateExpr value match targets with @@ -430,8 +437,6 @@ def translateStmt (stmt : StmtExprMd) return result | _ => throwStmtDiagnostic $ md.toDiagnostic "function call without a single target" DiagnosticType.StrataBug - else - translateCallTargets callee.text args | .InstanceCall _target callee args => translateCallTargets callee.text args | .Hole _ _ => @@ -457,7 +462,7 @@ def translateStmt (stmt : StmtExprMd) return [Imperative.Stmt.ite (.det bcond) bthen belse md] | .StaticCall callee args => -- Check if this is a function or procedure - if model.isFunction callee then + if !(← containsProcedure callee) then -- Function call in statement position: preserve as unused init exprAsUnusedInit stmt md else diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index c18b667639..8e3499c98f 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -208,27 +208,6 @@ def containsBareAssignment (expr : StmtExprMd) : Bool := decreasing_by all_goals ((try cases x); simp_all; try term_by_mem) -/-- Check if an expression contains any non-functional procedure calls (recursively). -/ -def containsImperativeCall (model : SemanticModel) (expr : StmtExprMd) : Bool := - match expr with - | AstNode.mk val _ => - match val with - | .StaticCall name args => - (match model.get name with - | .staticProcedure proc => !proc.isFunctional - | _ => false) || - args.attach.any (fun x => containsImperativeCall model x.val) - | .PrimitiveOp _ args _ => args.attach.any (fun x => containsImperativeCall model x.val) - | .Block stmts _ => stmts.attach.any (fun x => containsImperativeCall model x.val) - | .IfThenElse cond th el => - containsImperativeCall model cond || - containsImperativeCall model th || - match el with | some e => containsImperativeCall model e | none => false - | _ => false - termination_by expr - decreasing_by - all_goals ((try cases x); simp_all; try term_by_mem) - /-- Shared logic for lifting an assignment in expression position: prepends the assignment, creates before-snapshots for all targets, diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 4e68f144c6..5ed86c0033 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -180,18 +180,6 @@ def SemanticModel.get? (model: SemanticModel) (iden: Identifier): Option Resolve def SemanticModel.get (model: SemanticModel) (iden: Identifier): ResolvedNode := (model.get? iden).getD default -def SemanticModel.isFunction (model: SemanticModel) (id: Identifier): Bool := - match model.get id with - | .staticProcedure proc => proc.isFunctional - | .parameter _ => true - | .datatypeConstructor _ _ => true - | .datatypeDestructor _ _ => true - | .constant _ => true - | .unresolved _ => true -- functions calls are more permissive, so true avoids possibly incorrect errors - | node => - dbg_trace s!"Sound but incomplete BUG! id: {repr id}, is not a procedure, but a node {repr node}" - false - /-- The output of the resolution pass. -/ structure ResolutionResult where /-- The program with unique IDs on all definition and reference nodes. -/ @@ -591,7 +579,6 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr let axioms' ← proc.axioms.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', - isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', invokeOn := invokeOn', axioms := axioms', @@ -623,7 +610,6 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := savedInstType } let axioms' ← proc.axioms.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', - isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', invokeOn := invokeOn', axioms := axioms', @@ -720,7 +706,6 @@ private def mkTesterProcedure (dt : DatatypeDefinition) (ctor : DatatypeConstruc outputs := [outputParam] preconditions := [] decreases := none - isFunctional := true body := .External } /-- Insert a definition into the refToDef map using the ID already on the identifier. -/ @@ -977,32 +962,6 @@ def resolve (program : Program) (existingModel: Option SemanticModel := none) : /-! ## Resolution for UnorderedCoreWithLaurelTypes -/ -/-- -Convert an `UnorderedCoreWithLaurelTypes` to a flat `Program` suitable for -resolution. Additional type definitions (e.g. composite types from the original -Laurel program) can be supplied so that `UserDefined` type references resolve -correctly. --/ -private def unorderedCoreToProgram (uc : UnorderedCoreWithLaurelTypes) - (additionalTypes : List TypeDefinition := []) : Program := - { staticProcedures := uc.functions ++ uc.coreProcedures, - staticFields := [], - types := uc.datatypes.map TypeDefinition.Datatype ++ additionalTypes, - constants := uc.constants } - -/-- -Reconstruct an `UnorderedCoreWithLaurelTypes` from a resolved `Program`. --/ -private def fromResolvedProgram (resolvedProgram : Program) - : UnorderedCoreWithLaurelTypes := - let resolvedProcs := resolvedProgram.staticProcedures - let resolvedDatatypes := resolvedProgram.types.filterMap fun td => - match td with | .Datatype dt => some dt | _ => none - { functions := resolvedProcs.filter (·.isFunctional) - coreProcedures := resolvedProcs.filter (!·.isFunctional) - datatypes := resolvedDatatypes - constants := resolvedProgram.constants } - /-- Resolve an `UnorderedCoreWithLaurelTypes` by converting to a flat `Program`, running the resolution pass, and reconstructing the result. Returns the @@ -1016,9 +975,7 @@ but they are because certain type references have incorrectly not been updated. def resolveUnorderedCore (uc : UnorderedCoreWithLaurelTypes) (existingModel : Option SemanticModel := none) (additionalTypes : List TypeDefinition := []) - : UnorderedCoreWithLaurelTypes × SemanticModel × Array DiagnosticModel := - let fnProgram := unorderedCoreToProgram uc additionalTypes - let fnResolveResult := resolve fnProgram existingModel - (fromResolvedProgram fnResolveResult.program, fnResolveResult.model, fnResolveResult.errors) + : UnorderedCoreWithLaurelTypes × SemanticModel × Array DiagnosticModel := sorry + -- should not try to convert to a Laurel.Program and back. Probably needs to have its own version of resolve end diff --git a/Strata/Languages/Laurel/TransparencyPass.lean b/Strata/Languages/Laurel/TransparencyPass.lean index 8a6163769d..0da7a37b51 100644 --- a/Strata/Languages/Laurel/TransparencyPass.lean +++ b/Strata/Languages/Laurel/TransparencyPass.lean @@ -137,7 +137,7 @@ private def mkFunctionCopy (asFunctionNames : Std.HashSet String) (proc : Proced | .Transparent b => .Transparent (rewriteCallsToFunctional asFunctionNames (if hasProcedureTwin then stripAssertAssume b else b)) | .Opaque _ _ _ => if hasProcedureTwin then .Opaque [] none [] else proc.body | x => x - { proc with name := funcName, isFunctional := true, body := body } + { proc with name := funcName, body := body } /-- Append a free postcondition to a procedure's body postconditions. For Opaque and Abstract bodies, the free condition is appended to the @@ -167,13 +167,13 @@ For each procedure: - If the function has a body, add a free postcondition equating the procedure output to the function -/ def transparencyPass (program : Program) : UnorderedCoreWithLaurelTypes := - let (toUpdate, _) := program.staticProcedures.partition (fun p => !p.body.isExternal && !p.isFunctional) + let (toUpdate, _) := program.staticProcedures.partition (fun p => !p.body.isExternal) let toUpdateNames : Std.HashSet String := toUpdate.foldl (fun s p => s.insert p.name.text) {} -- $asFunction copies for non-external procedures let functions := program.staticProcedures.map (mkFunctionCopy toUpdateNames) let coreProcedures := toUpdate.map fun proc => let freePostcondition := mkFreePostcondition proc - let proc := { proc with isFunctional := false, axioms := proc.axioms.map (rewriteCallsToFunctional toUpdateNames) } + let proc := { proc with axioms := proc.axioms.map (rewriteCallsToFunctional toUpdateNames) } let proc := rewriteQuantifierBodiesInProc toUpdateNames proc addFreePostcondition proc freePostcondition let datatypes := program.types.filterMap fun td => match td with From c3b10e9c5b66da9cde473a4cff65447c7202ebca Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 9 Jun 2026 13:44:42 +0000 Subject: [PATCH 02/42] Remove 'function' from Laurel tests --- .../Laurel/LaurelToCoreTranslator.lean | 4 +- .../T10_ConstrainedTypesError.lean | 42 ------------------- .../Fundamentals/T14_Quantifiers.lean | 4 +- .../Fundamentals/T15_ShortCircuit.lean | 25 ----------- .../Examples/Fundamentals/T19_InvokeOn.lean | 20 ++++----- .../Fundamentals/T22_ArityMismatch.lean | 2 +- .../T2_ImpureExpressionsError.lean | 9 ++-- .../Examples/Fundamentals/T3_ControlFlow.lean | 22 +++++++++- .../Fundamentals/T3_ControlFlowError.lean | 27 ++---------- .../Fundamentals/T5_ProcedureCalls.lean | 12 ------ .../Fundamentals/T6_Preconditions.lean | 29 ------------- .../Fundamentals/T8_PostconditionsErrors.lean | 42 ------------------- .../Laurel/Examples/Objects/T6_Datatypes.lean | 7 ++-- 13 files changed, 46 insertions(+), 199 deletions(-) delete mode 100644 StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypesError.lean delete mode 100644 StrataTest/Languages/Laurel/Examples/Fundamentals/T8_PostconditionsErrors.lean diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean index fe7b15e986..7c81dd3d1a 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean @@ -256,7 +256,7 @@ def translateExpr (expr : StmtExprMd) | .Assign _ _ => disallowed expr.source "destructive assignments are not supported in transparent bodies or contracts" | .While _ _ _ _ => - disallowed expr.source "loops are not supported in functions or contracts" + disallowed expr.source "loops are not supported in transparent bodies or contracts" | .Exit _ => disallowed expr.source "exit is not supported in expression position" | .Block (⟨ .Assert _, innerSrc⟩ :: rest) label => do @@ -271,7 +271,7 @@ def translateExpr (expr : StmtExprMd) let coreMonoType ← translateType ty return .app () (.abs () name.text (some coreMonoType) bodyExpr) valueExpr | .Block (⟨ .Var (.Declare _), innerSrc⟩ :: rest) label => do - _ ← disallowed innerSrc "local variables in functions must have initializers" + _ ← disallowed innerSrc "local variables must have initializers in transparent bodies or contracts " translateExpr { val := StmtExpr.Block rest label, source := innerSrc } boundVars isPureContext | .Block (⟨ .IfThenElse cond thenBranch (some elseBranch), innerSrc⟩ :: rest) label => disallowed innerSrc "if-then-else only supported as the last statement in a block" diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypesError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypesError.lean deleted file mode 100644 index 4c3d20f45f..0000000000 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypesError.lean +++ /dev/null @@ -1,42 +0,0 @@ -/- - Copyright Strata Contributors - - SPDX-License-Identifier: Apache-2.0 OR MIT --/ -module - -meta import all StrataTest.Util.TestDiagnostics -meta import all StrataTest.Languages.Laurel.TestExamples - -meta section - -open StrataTest.Util - -namespace Strata -namespace Laurel - -def program := r" -constrained nat = x: int where x >= 0 witness 0 - -// 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() - opaque -{ - var x: int := goodFunc(); - assert x >= 0 -}; -" - -#guard_msgs(drop info, error) in -#eval testInputWithOffset "ConstrainedTypes" program 14 processLaurelFile - -end Laurel -end Strata diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean index ad6f671ed8..47961cea32 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean @@ -35,8 +35,8 @@ procedure testQuantifierInContract(n: int) { }; -function P(x: int): int; -function Q(): int; +procedure P(x: int): int; +procedure Q(): int; procedure triggers() opaque { diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean index 034e2df5d6..73376ae98e 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean @@ -16,9 +16,6 @@ namespace Strata namespace Laurel def shortCircuitProgram := r" -function mustNotCallFunc(x: int): int - requires false -{ x }; procedure mustNotCallProc(): int requires false @@ -27,28 +24,6 @@ procedure mustNotCallProc(): int return 0 }; -// Pure path: function with requires false -procedure testAndThenFunc() - opaque -{ - var b: bool := false && mustNotCallFunc(0) > 0; - assert !b -}; - -procedure testOrElseFunc() - opaque -{ - var b: bool := true || mustNotCallFunc(0) > 0; - assert b -}; - -procedure testImpliesFunc() - opaque -{ - var b: bool := false ==> mustNotCallFunc(0) > 0; - assert b -}; - // Pure path: division by zero procedure testAndThenDivByZero() diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean index 3f60fddc12..05087568f0 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean @@ -16,12 +16,12 @@ open Strata namespace Strata.Laurel def program := r#" -function P(x: int): bool; -function Q(x: int): bool; +procedure P(x: int): bool; +procedure Q(x: int): bool; -function assertP(x: int): int requires P(x); -function needsPAndQsInvoke1(): int { - assertP(3) +procedure assertP(x: int): int requires P(x); +procedure needsPAndQsInvoke1(): int { + return assertP(3) }; procedure PAndQ(x: int) @@ -29,8 +29,8 @@ procedure PAndQ(x: int) opaque ensures P(x) && Q(x); -function needsPAndQsInvoke2(): int { - assertP(3) +procedure needsPAndQsInvoke2(): int { + return assertP(3) }; // The axiom fires because P(x) appears in the goal. @@ -47,8 +47,8 @@ procedure axiomDoesNotFireBecauseOfPattern(x: int) //^^^^^^^^^^^ error: assertion could not be proved }; -function A(x: int, y: real): bool; -function B(x: real): bool; +procedure A(x: int, y: real): bool; +procedure B(x: real): bool; procedure AAndB(x: int, y: real) invokeOn A(x, y) opaque @@ -67,7 +67,7 @@ procedure invokeB(x: int, y :real) //^^^^^^^^^^^ error: assertion could not be proved }; -function R(x: int): bool; +procedure R(x: int): bool; procedure badPostcondition(x: int) invokeOn R(x) opaque diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean index fce8013d30..1498b36681 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean @@ -16,7 +16,7 @@ namespace Strata namespace Laurel def arityMismatchProgram := r" -function f(x: int): int { x }; +procedure f(x: int): int { x }; procedure caller() opaque diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean index a126db37a3..eeda775251 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean @@ -24,18 +24,19 @@ procedure hasMutatingAssignment(): int x }; -function functionWithMutatingAssignment(x: int): int +procedure transparentWithMutatingAssignment(x: int): int { x := x + 1 //^^^^^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts }; -function functionWithWhile(x: int): int +procedure transparentWithWhile(x: int): int { while(false) {} -//^^^^^^^^^^^^^^^ error: loops are not supported in functions or contracts +//^^^^^^^^^^^^^^^ error: loops are not supported in transparent bodies or contracts }; -function functionCallingHasMutationAssignment(x: int): int + +procedure callsHasMutatingAssignment(x: int): int { hasMutatingAssignment() }; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean index 64bb0ce9ac..7785d677ac 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean @@ -16,6 +16,24 @@ open Strata namespace Strata.Laurel def program := r" +procedure assertAndAssumeInTransparent(a: int) returns (r: int) +{ + assert 2 == 3; + assume true; + return a +}; + +procedure letExpressionsInTransparent() returns (r: int) { + var x: int := 0; + var y: int := x + 1; + var z: int := y + 1; + z +}; + +procedure callLetExpressionsInTransparent() opaque { + var x: int := letExpressionsInTransparent(); + assert x == 2 +}; procedure returnAtEnd(x: int) returns (r: int) { @@ -30,9 +48,9 @@ procedure returnAtEnd(x: int) returns (r: int) } }; -function elseWithCall(): int +procedure elseWithCall(): int { - if true then 3 else returnAtEnd(3) + return if true then 3 else returnAtEnd(3) }; procedure guardInFunction(x: int) returns (r: int) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean index a166066b12..a2e97e1d7c 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean @@ -16,31 +16,10 @@ open Strata namespace Strata.Laurel def program := r" -function assertAndAssumeInFunctions(a: int) returns (r: int) -{ - assert 2 == 3; -//^^^^^^^^^^^^^ error: asserts are not YET supported in functions or contracts - assume true; -//^^^^^^^^^^^ error: assumes are not YET supported in functions or contracts - a -}; - -function letsInFunction() returns (r: int) { - var x: int := 0; - var y: int := x + 1; - var z: int := y + 1; - z -}; - -procedure callLetsInFunction() opaque { - var x: int := letsInFunction(); - assert x == 2 -}; - -function localVariableWithoutInitializer(): int { +procedure localVariableWithoutInitializer(): int { var x: int; -//^^^^^^^^^^ error: local variables in functions must have initializers - 3 +//^^^^^^^^^^ error: local variables must have initializers in transparent bodies or contracts + return 3 }; " diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean index bebe421579..449ef768f6 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean @@ -43,18 +43,6 @@ procedure fooProof() // because we don't yet support making fooReassign transparent // assert x == y; }; - -function aFunction(x: int): int -{ - x -}; - -procedure aFunctionCaller() - opaque -{ - var x: int := aFunction(3); - assert x == 3 -}; " #guard_msgs (drop info, error) in diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean index 08c9b0ecfb..56c3abfe41 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean @@ -37,20 +37,6 @@ procedure caller() var y: int := hasRequires(3) }; -function aFunctionWithPrecondition(x: int): int - requires x == 10 -{ - x -}; - -procedure aFunctionWithPreconditionCaller() - opaque -{ - var x: int := aFunctionWithPrecondition(0) -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion does not hold -// Error ranges are too wide because Core does not use expression locations -}; - procedure multipleRequires(x: int, y: int) returns (r: int) requires x > 0 requires y > 0 @@ -66,21 +52,6 @@ procedure multipleRequiresCaller() var b: int := multipleRequires(-1, 2) //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: precondition could not be proved }; - -function funcMultipleRequires(x: int, y: int): int - requires x > 0 - requires y > 0 -{ - x + y -}; - -procedure funcMultipleRequiresCaller() - opaque -{ - var a: int := funcMultipleRequires(1, 2); - var b: int := funcMultipleRequires(1, -1) -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion does not hold -}; " #guard_msgs (drop info, error) in diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_PostconditionsErrors.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_PostconditionsErrors.lean deleted file mode 100644 index 88f8bec609..0000000000 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_PostconditionsErrors.lean +++ /dev/null @@ -1,42 +0,0 @@ -/- - Copyright Strata Contributors - - SPDX-License-Identifier: Apache-2.0 OR MIT --/ -module - -meta import all StrataTest.Util.TestDiagnostics -meta import all StrataTest.Languages.Laurel.TestExamples - -meta section - -open StrataTest.Util -open Strata - -namespace Strata.Laurel - -def program := r" - -function opaqueFunction(x: int) returns (r: int) -// ^^^^^^^^^^^^^^ error: functions with postconditions are not yet supported -// The above limitation is because Core does not yet support functions with postconditions - requires x > 0 - opaque - ensures r > 0 -// The above limitation is because functions in Core do not support postconditions -{ - x -}; - -procedure callerOfOpaqueFunction() - opaque -{ - var x: int := opaqueFunction(3); - assert x > 0; -// The following assertion should fail but does not - assert x == 3 -}; -" - -#guard_msgs (drop info, error) in -#eval testInputWithOffset "Postconditions" program 14 processLaurelFile diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean b/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean index 4743c1f7bb..dbbc929fd9 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean @@ -55,14 +55,13 @@ procedure unsafeDestructor() opaque { //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion does not hold }; -// Datatype in function -function listHead(xs: IntList): int +procedure listHead(xs: IntList): int requires IntList..isCons(xs) { - IntList..head(xs) + return IntList..head(xs) }; -procedure testFunction() opaque { +procedure testListHead() opaque { var xs: IntList := Cons(10, Nil()); var h: int := listHead(xs); assert h == 10 From 179951fd3fc695ae307707ffe1b03d3fc8f8592d Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 9 Jun 2026 13:46:17 +0000 Subject: [PATCH 03/42] update library files --- .../Languages/Laurel/CoreDefinitionsForLaurel.lean | 6 +++--- .../Laurel/HeapParameterizationConstants.lean | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean b/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean index f4e4924c8e..c7a8099cb5 100644 --- a/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean +++ b/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean @@ -35,13 +35,13 @@ datatype LaurelUnit { MkLaurelUnit() } // And remove the hacky filter in HeapParameterization datatype Box { MkBox() } -function select(map: int, key: int) : Box +procedure select(map: int, key: int) : Box external; -function update(map: int, key: int, value: int) : Box +procedure update(map: int, key: int, value: int) : Box external; -function const(value: int) : Box +procedure const(value: int) : Box external; #end diff --git a/Strata/Languages/Laurel/HeapParameterizationConstants.lean b/Strata/Languages/Laurel/HeapParameterizationConstants.lean index 7258f29d6a..0cb616f12f 100644 --- a/Strata/Languages/Laurel/HeapParameterizationConstants.lean +++ b/Strata/Languages/Laurel/HeapParameterizationConstants.lean @@ -53,21 +53,21 @@ datatype Heap { } // Read a field from the heap: readField(heap, obj, field) = Heap..data!(heap)[obj][field] -function readField(heap: Heap, obj: Composite, field: Field): Box { - select(select(Heap..data!(heap), obj), field) +procedure readField(heap: Heap, obj: Composite, field: Field): Box { + return select(select(Heap..data!(heap), obj), field) }; // Update a field in the heap -function updateField(heap: Heap, obj: Composite, field: Field, val: Box): Heap { - MkHeap( +procedure updateField(heap: Heap, obj: Composite, field: Field, val: Box): Heap { + return MkHeap( update(Heap..data!(heap), obj, update(select(Heap..data!(heap), obj), field, val)), Heap..nextReference!(heap)) }; // Increment the heap allocation nextReference, returning a new heap -function increment(heap: Heap): Heap { - MkHeap(Heap..data!(heap), Heap..nextReference!(heap) + 1) +procedure increment(heap: Heap): Heap { + return MkHeap(Heap..data!(heap), Heap..nextReference!(heap) + 1) }; #end From 6e945c7cdfd5124cf999e75ba08dadaf01ded150 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 9 Jun 2026 14:00:42 +0000 Subject: [PATCH 04/42] add missing implementation for resolveUnorderedCore --- .../Laurel/Grammar/LaurelGrammar.lean | 2 +- Strata/Languages/Laurel/Resolution.lean | 104 +++++++++++++++++- .../AbstractToConcreteTreeTranslatorTest.lean | 4 +- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean index c1d24aa7ee..f8cd602b88 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean @@ -9,7 +9,7 @@ module -- 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: block format uses indent(2) with leading spaces for vertical layout. +-- Last grammar change: remove functions public import StrataDDM.AST import StrataDDM.BuiltinDialects.Init import StrataDDM.Integration.Lean.HashCommands diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 5ed86c0033..6994b3b889 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -975,7 +975,107 @@ but they are because certain type references have incorrectly not been updated. def resolveUnorderedCore (uc : UnorderedCoreWithLaurelTypes) (existingModel : Option SemanticModel := none) (additionalTypes : List TypeDefinition := []) - : UnorderedCoreWithLaurelTypes × SemanticModel × Array DiagnosticModel := sorry - -- should not try to convert to a Laurel.Program and back. Probably needs to have its own version of resolve + : UnorderedCoreWithLaurelTypes × SemanticModel × Array DiagnosticModel := + -- Phase 1: pre-register all top-level names, then resolve references + let phase1 : ResolveM UnorderedCoreWithLaurelTypes := do + -- Pre-register additional types (e.g. composite types from the original Laurel program) + for td in additionalTypes do + match td with + | .Composite ct => + let _ ← defineNameCheckDup ct.name (.compositeType ct) + for field in ct.fields do + let qualifiedName := ct.name.text ++ "." ++ field.name.text + let _ ← defineNameCheckDup field.name (.field ct.name field) (some qualifiedName) + for proc in ct.instanceProcedures do + let _ ← defineNameCheckDup proc.name (.instanceProcedure ct.name proc) + | .Constrained ct => + let _ ← defineNameCheckDup ct.name (.constrainedType ct) + | .Datatype dt => + let _ ← defineNameCheckDup dt.name (.datatypeDefinition dt) + for ctor in dt.constructors do + let _ ← defineNameCheckDup ctor.name (.datatypeConstructor dt.name ctor) + let testerProc := mkTesterProcedure dt ctor + let _ ← defineNameCheckDup (mkId (dt.testerName ctor)) + (.staticProcedure testerProc) (some (dt.testerName ctor)) + for p in ctor.args do + let pName ← defineNameCheckDup p.name (.datatypeDestructor dt.name p) (some (dt.destructorName p)) + let _ ← defineNameCheckDup pName (.datatypeDestructor dt.name p) (some (dt.unsafeDestructorName p)) + | .Alias ta => + let _ ← defineNameCheckDup ta.name (.typeAlias ta) + + -- Pre-register datatypes from the unordered core + for dt in uc.datatypes do + let _ ← defineNameCheckDup dt.name (.datatypeDefinition dt) + for ctor in dt.constructors do + let _ ← defineNameCheckDup ctor.name (.datatypeConstructor dt.name ctor) + let testerProc := mkTesterProcedure dt ctor + let _ ← defineNameCheckDup (mkId (dt.testerName ctor)) + (.staticProcedure testerProc) (some (dt.testerName ctor)) + for p in ctor.args do + let pName ← defineNameCheckDup p.name (.datatypeDestructor dt.name p) (some (dt.destructorName p)) + let _ ← defineNameCheckDup pName (.datatypeDestructor dt.name p) (some (dt.unsafeDestructorName p)) + + -- Pre-register constants + for c in uc.constants do + let _ ← defineNameCheckDup c.name (.constant c) + + -- Pre-register functions and core procedures + for proc in uc.functions do + let _ ← defineNameCheckDup proc.name (.staticProcedure proc) + for proc in uc.coreProcedures do + let _ ← defineNameCheckDup proc.name (.staticProcedure proc) + + -- Build type scopes for additional composite types (for field resolution) + for td in additionalTypes do + if let .Composite ct := td then + let s ← get + let mut typeScope : Scope := {} + for parent in ct.extending do + match s.typeScopes.get? parent.text with + | some parentScope => + for (k, v) in parentScope do + typeScope := typeScope.insert k v + | none => pure () + for field in ct.fields do + let qualifiedKey := ct.name.text ++ "." ++ field.name.text + match s.scope.get? qualifiedKey with + | some entry => typeScope := typeScope.insert field.name.text entry + | none => pure () + modify fun s => { s with typeScopes := s.typeScopes.insert ct.name.text typeScope } + + -- Resolve datatypes + let datatypes' ← uc.datatypes.mapM fun dt => do + match ← resolveTypeDefinition (.Datatype dt) with + | .Datatype dt' => pure dt' + | _ => pure dt -- unreachable + + -- Resolve constants + let constants' ← uc.constants.mapM resolveConstant + + -- Resolve functions and core procedures + let functions' ← uc.functions.mapM resolveProcedure + let coreProcedures' ← uc.coreProcedures.mapM resolveProcedure + + return { functions := functions', coreProcedures := coreProcedures', + datatypes := datatypes', constants := constants' } + + let nextId := existingModel.elim 1 (fun m => m.nextId) + let (uc', finalState) := phase1.run { nextId := nextId } + + -- Phase 2: build refToDef from the resolved unordered core + let program' : Program := { + staticProcedures := uc'.functions ++ uc'.coreProcedures, + staticFields := [], + types := uc'.datatypes.map .Datatype ++ additionalTypes, + constants := uc'.constants + } + let refToDef := buildRefToDef program' + + let model : SemanticModel := { + compositeCount := additionalTypes.length, + refToDef := refToDef, + nextId := finalState.nextId + } + (uc', model, finalState.errors) end diff --git a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean index 668220db1d..26e63ea494 100644 --- a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean +++ b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean @@ -85,13 +85,13 @@ info: procedure add(x: int, y: int): int { x + y };") /-- -info: function aFunction(x: int): int +info: procedure aFunction(x: int): int { x }; -/ #guard_msgs in -#eval do IO.println (← roundtrip r"function aFunction(x: int): int +#eval do IO.println (← roundtrip r"procedure aFunction(x: int): int { x };") /-- From 23e07ccf4a79b3ba5faebb24e005410d8fb5b353 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 9 Jun 2026 14:23:12 +0000 Subject: [PATCH 05/42] Fixes. AssertFalse passes --- .../Languages/Laurel/CoreGroupingAndOrdering.lean | 8 +++++++- .../Laurel/EliminateReturnStatements.lean | 2 -- .../Laurel/EliminateValuesInReturns.lean | 2 +- .../Laurel/HeapParameterizationConstants.lean | 15 ++++++--------- Strata/Languages/Laurel/TransparencyPass.lean | 9 ++++++++- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean b/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean index 6e45d23941..9b7b17dea9 100644 --- a/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean +++ b/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean @@ -201,8 +201,14 @@ open Std (Format ToFormat) public section +/-- Format a procedure as a function, replacing the leading "procedure" keyword with "function". -/ +private def formatAsFunction (proc : Procedure) : Format := + let s := (ToFormat.format proc).pretty 100 + let s := if s.startsWith "procedure" then "function" ++ s.drop "procedure".length else s + Format.text s + def formatOrderedDecl : OrderedDecl → Format - | .funcs funcs _ => Format.joinSep (funcs.map ToFormat.format) "\n\n" + | .funcs funcs _ => Format.joinSep (funcs.map formatAsFunction) "\n\n" | .procedure proc => ToFormat.format proc | .datatypes dts => Format.joinSep (dts.map ToFormat.format) "\n\n" | .constant c => ToFormat.format c diff --git a/Strata/Languages/Laurel/EliminateReturnStatements.lean b/Strata/Languages/Laurel/EliminateReturnStatements.lean index 7846ec8263..c2fbc279c3 100644 --- a/Strata/Languages/Laurel/EliminateReturnStatements.lean +++ b/Strata/Languages/Laurel/EliminateReturnStatements.lean @@ -26,8 +26,6 @@ public section private def returnLabel : String := "$return" - - /-- Transform a single procedure: wrap body in a labelled block and replace returns. -/ private def eliminateReturnStmts (proc : Procedure) : Procedure := match proc.body with diff --git a/Strata/Languages/Laurel/EliminateValuesInReturns.lean b/Strata/Languages/Laurel/EliminateValuesInReturns.lean index cd82f80c79..0c6aa7506c 100644 --- a/Strata/Languages/Laurel/EliminateValuesInReturns.lean +++ b/Strata/Languages/Laurel/EliminateValuesInReturns.lean @@ -54,7 +54,7 @@ def hasValuedReturn (stmt : StmtExprMd) : Bool := all_goals omega /-- Apply value-return elimination to a single procedure. Only applies to - non-functional procedures with exactly one output parameter. + procedures with exactly one output parameter. Emits an error if a valued return is used with multiple output parameters. -/ def eliminateValuesInReturnsInProc (proc : Procedure) : Procedure × Array DiagnosticModel := match proc.outputs with diff --git a/Strata/Languages/Laurel/HeapParameterizationConstants.lean b/Strata/Languages/Laurel/HeapParameterizationConstants.lean index 0cb616f12f..5b1eb605d6 100644 --- a/Strata/Languages/Laurel/HeapParameterizationConstants.lean +++ b/Strata/Languages/Laurel/HeapParameterizationConstants.lean @@ -53,22 +53,19 @@ datatype Heap { } // Read a field from the heap: readField(heap, obj, field) = Heap..data!(heap)[obj][field] -procedure readField(heap: Heap, obj: Composite, field: Field): Box { - return select(select(Heap..data!(heap), obj), field) -}; +procedure readField(heap: Heap, obj: Composite, field: Field): Box + return select(select(Heap..data!(heap), obj), field); // Update a field in the heap -procedure updateField(heap: Heap, obj: Composite, field: Field, val: Box): Heap { +procedure updateField(heap: Heap, obj: Composite, field: Field, val: Box): Heap return MkHeap( update(Heap..data!(heap), obj, update(select(Heap..data!(heap), obj), field, val)), - Heap..nextReference!(heap)) -}; + Heap..nextReference!(heap)); // Increment the heap allocation nextReference, returning a new heap -procedure increment(heap: Heap): Heap { - return MkHeap(Heap..data!(heap), Heap..nextReference!(heap) + 1) -}; +procedure increment(heap: Heap): Heap + return MkHeap(Heap..data!(heap), Heap..nextReference!(heap) + 1); #end diff --git a/Strata/Languages/Laurel/TransparencyPass.lean b/Strata/Languages/Laurel/TransparencyPass.lean index 0da7a37b51..c4e121caec 100644 --- a/Strata/Languages/Laurel/TransparencyPass.lean +++ b/Strata/Languages/Laurel/TransparencyPass.lean @@ -188,7 +188,14 @@ def formatUnorderedCoreWithLaurelTypes (p : UnorderedCoreWithLaurelTypes) : Form let constantFmts := p.constants.map ToFormat.format let functionFmts := p.functions.map ToFormat.format let procFmts := p.coreProcedures.map ToFormat.format - Format.joinSep (datatypeFmts ++ constantFmts ++ functionFmts ++ procFmts) "\n\n" + let preamble := datatypeFmts ++ constantFmts + let functionSection := + if functionFmts.isEmpty then [] + else [Format.text "// FUNCTIONS"] ++ functionFmts + let procSection := + if procFmts.isEmpty then [] + else [Format.text "// PROCEDURES"] ++ procFmts + Format.joinSep (preamble ++ functionSection ++ procSection) "\n\n" instance : ToFormat UnorderedCoreWithLaurelTypes where format := formatUnorderedCoreWithLaurelTypes From 4158558069b6dcea078899a74db0e21e3ae9bf5e Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 9 Jun 2026 14:30:03 +0000 Subject: [PATCH 06/42] Test fixes --- .../Laurel/DivisionByZeroCheckTest.lean | 6 +- .../Fundamentals/T10_ConstrainedTypes.lean | 2 +- .../Fundamentals/T2_ImpureExpressions.lean | 4 +- .../T2_ImpureExpressionsError.lean | 7 +- .../Examples/Fundamentals/T3_ControlFlow.lean | 12 ---- .../Fundamentals/T3_ControlFlowError.lean | 2 +- .../Fundamentals/T3_ControlFlowError2.lean | 1 - .../Laurel/LiftExpressionAssignmentsTest.lean | 3 +- .../Languages/Laurel/LiftHolesTest.lean | 66 +++++++++---------- .../Languages/Laurel/TypeAliasElimTest.lean | 3 +- 10 files changed, 47 insertions(+), 59 deletions(-) diff --git a/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean b/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean index b6d91bdf9c..3e6fa52f58 100644 --- a/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean +++ b/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean @@ -40,10 +40,10 @@ procedure unsafeDivision(x: int) // Error ranges are too wide because Core does not use expression locations }; -function pureDiv(x: int, y: int): int +procedure pureDiv(x: int, y: int): int requires y != 0 { - x / y + return x / y }; procedure callPureDivSafe() @@ -57,7 +57,7 @@ procedure callPureDivUnsafe(x: int) opaque { var z: int := pureDiv(10, x) -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion does not hold +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: precondition could not be proved // Error ranges are too wide because Core does not use expression locations }; " diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean index 28d4dff12e..a234fd665c 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean @@ -196,7 +196,7 @@ procedure captureTest(y: haslarger) " #guard_msgs(drop info, error) in -#eval testInputWithOffset "ConstrainedTypes" program 14 processLaurelFile +#eval testInputWithOffset "ConstrainedTypes" program 17 processLaurelFile end Laurel end Strata diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index 28584de204..5b78852702 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -115,9 +115,9 @@ procedure imperativeCallInConditionalExpression(b: bool) } }; -function add(x: int, y: int): int +procedure add(x: int, y: int): int { - x + y + return x + y }; procedure repeatedBlockExpressions() diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean index eeda775251..fbb32a2c6a 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean @@ -26,17 +26,20 @@ procedure hasMutatingAssignment(): int procedure transparentWithMutatingAssignment(x: int): int { - x := x + 1 + x := x + 1; //^^^^^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts + return 3 }; procedure transparentWithWhile(x: int): int { - while(false) {} + while(false) {}; //^^^^^^^^^^^^^^^ error: loops are not supported in transparent bodies or contracts + return 3 }; procedure callsHasMutatingAssignment(x: int): int + opaque { hasMutatingAssignment() }; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean index 7785d677ac..53033a683b 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean @@ -23,18 +23,6 @@ procedure assertAndAssumeInTransparent(a: int) returns (r: int) return a }; -procedure letExpressionsInTransparent() returns (r: int) { - var x: int := 0; - var y: int := x + 1; - var z: int := y + 1; - z -}; - -procedure callLetExpressionsInTransparent() opaque { - var x: int := letExpressionsInTransparent(); - assert x == 2 -}; - procedure returnAtEnd(x: int) returns (r: int) { if x > 0 then { diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean index a2e97e1d7c..61f3d51bfc 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean @@ -24,4 +24,4 @@ procedure localVariableWithoutInitializer(): int { " #guard_msgs (error, drop all) in -#eval! testInputWithOffset "ControlFlowError" program 14 processLaurelFile +#eval! testInputWithOffset "ControlFlowError" program 17 processLaurelFile diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean index cb77d6dc3f..4b65c601d3 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean @@ -16,7 +16,6 @@ open Strata namespace Strata.Laurel def program := r" - procedure deadCodeAfterIfElse(x: int) returns (r: int) { if x > 0 then { return 1 } else { return 2 }; //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: in a transparent body, if-then-else is only supported as the last statement in a block diff --git a/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean b/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean index f093b4bb8b..379098ce28 100644 --- a/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean +++ b/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean @@ -45,8 +45,7 @@ def parseLaurelAndLift (input : String) : IO Program := do | .ok program => let result := resolve program let (program, model) := (result.program, result.model) - let imperativeCallees := program.staticProcedures.filter (fun p => !p.isFunctional) - |>.map (fun p => p.name.text) + let imperativeCallees := program.staticProcedures |>.map (fun p => p.name.text) pure (liftExpressionAssignments program model imperativeCallees) /-- diff --git a/StrataTest/Languages/Laurel/LiftHolesTest.lean b/StrataTest/Languages/Laurel/LiftHolesTest.lean index 8f08ffc15e..45a4d368f6 100644 --- a/StrataTest/Languages/Laurel/LiftHolesTest.lean +++ b/StrataTest/Languages/Laurel/LiftHolesTest.lean @@ -7,7 +7,7 @@ module /- Tests that the eliminateHoles pass correctly replaces `.Hole` nodes with calls -to freshly generated uninterpreted functions, with types inferred from context. +to freshly generated uninterpreted procedures, with types inferred from context. -/ meta import StrataDDM.Elab @@ -46,7 +46,7 @@ private def parseElimAndPrint (input : String) : IO Unit := do -- Hole in Add arg inside typed local variable → int. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; procedure test() @@ -64,7 +64,7 @@ procedure test() -- Bare Hole as Assign Declare initializer → replaced with call (no longer preserved as havoc). /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; procedure test() @@ -82,7 +82,7 @@ procedure test() -- Hole in comparison arg inside assert → int (inferred from sibling literal). /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; procedure test() @@ -100,7 +100,7 @@ procedure test() -- Hole directly as assert condition → bool. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: bool) opaque; procedure test() @@ -118,7 +118,7 @@ procedure test() -- Hole directly as assume condition → bool. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: bool) opaque; procedure test() @@ -136,7 +136,7 @@ procedure test() -- Hole as if-then-else condition → bool. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: bool) opaque; procedure test() @@ -157,7 +157,7 @@ procedure test() -- Hole in then-branch of if-then-else inside typed local variable → int. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; procedure test() @@ -177,7 +177,7 @@ procedure test() -- Hole as while-loop condition → bool. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: bool) opaque; procedure test() @@ -197,7 +197,7 @@ procedure test() -- Hole as while-loop invariant → bool. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: bool) opaque; procedure test() @@ -220,7 +220,7 @@ procedure test() -- Hole in And arg inside assert → bool. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: bool) opaque; procedure test() @@ -238,7 +238,7 @@ procedure test() -- Hole in Neg inside typed local variable → int. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; procedure test() @@ -256,7 +256,7 @@ procedure test() -- Hole in StrConcat inside typed local variable → string. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: string) opaque; procedure test() @@ -270,12 +270,12 @@ procedure test() /-! ## Multiple holes -/ --- Two holes in Add → both int, separate functions. +-- Two holes in Add → both int, separate procedures. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; -function $hole_1() +procedure $hole_1() returns ($result: int) opaque; procedure test() @@ -293,10 +293,10 @@ procedure test() -- Holes across statements: Mul arg (int) then assert condition (bool). /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; -function $hole_1() +procedure $hole_1() returns ($result: bool) opaque; procedure test() @@ -317,7 +317,7 @@ procedure test() -- Hole in Add inside Gt inside if condition → int (inferred from sibling literal 0). /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; procedure test() @@ -338,7 +338,7 @@ procedure test() -- Hole in Implies inside while invariant → bool. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: bool) opaque; procedure test() @@ -360,7 +360,7 @@ procedure test() -- Hole in Mul inside typed local variable with real type → real. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: real) opaque; procedure test() @@ -378,9 +378,9 @@ procedure test() /-! ## Call argument and return type inference -/ --- Hole in comparison with variable sibling → hole function takes the procedure's params. +-- Hole in comparison with variable sibling → hole procedure takes the procedure's params. /-- -info: function $hole_0(n: int) +info: procedure $hole_0(n: int) returns ($result: int) opaque; procedure test(n: int) @@ -396,21 +396,21 @@ procedure test(n: int) { assert n > }; " -/-! ## Holes in functions -/ +/-! ## Holes in procedures -/ --- Hole in function body → same treatment as procedures. +-- Hole in procedure body → same treatment as procedures. /-- -info: function $hole_0(x: int) +info: procedure $hole_0(x: int) returns ($result: int) opaque; -function test(x: int): int +procedure test(x: int): int { $hole_0(x) }; -/ #guard_msgs in #eval! parseElimAndPrint r" -function test(x: int): int +procedure test(x: int): int { }; " @@ -433,7 +433,7 @@ procedure test() -- Mixed: det hole eliminated, nondet hole preserved. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: int) opaque; procedure test() @@ -450,7 +450,7 @@ procedure test() { var x: int := ; assert }; " --- Nondet hole in function → should be rejected (not tested here since +-- Nondet hole in procedure → should be rejected (not tested here since -- the error occurs at Core translation time, which requires the full pipeline). /-! ## Holes inside datatype destructor / tester arguments -/ @@ -461,7 +461,7 @@ procedure test() -- parent datatype's resolved Identifier (with `uniqueId`), so this works -- without textual decoding of the override name. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: IntList) opaque; procedure test() @@ -477,7 +477,7 @@ procedure test() { var x: int := IntList..head() }; -- Hole as argument to an unsafe `!` destructor → same datatype recovery. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: IntList) opaque; procedure test() @@ -493,7 +493,7 @@ procedure test() { var x: int := IntList..head!() }; -- Hole as argument to a tester → typed as the parent datatype. /-- -info: function $hole_0() +info: procedure $hole_0() returns ($result: IntList) opaque; procedure test() diff --git a/StrataTest/Languages/Laurel/TypeAliasElimTest.lean b/StrataTest/Languages/Laurel/TypeAliasElimTest.lean index 6295539cab..2a613012ca 100644 --- a/StrataTest/Languages/Laurel/TypeAliasElimTest.lean +++ b/StrataTest/Languages/Laurel/TypeAliasElimTest.lean @@ -32,8 +32,7 @@ private def mkTy (ty : HighType) : HighTypeMd := { val := ty, source := none } /-- Helper: construct a minimal procedure. -/ private def mkProc (name : String) (inputs : List Parameter) (outputs : List Parameter) (body : Body := .Transparent ⟨.Block [] none, none⟩) : Procedure := - { name := mkId name, inputs, outputs, preconditions := [], decreases := none, - isFunctional := false, body } + { name := mkId name, inputs, outputs, preconditions := [], decreases := none, body } /-- Helper: run resolve + typeAliasElim on a program. -/ private def resolveAndElim (program : Program) : Program := From f6e53f0af1df48b03ab700cc8e9af8077bc7d47d Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 10 Jun 2026 08:21:38 +0000 Subject: [PATCH 07/42] Update tests --- .../Examples/Fundamentals/T1_AssertFalse.lean | 2 +- .../Fundamentals/T3_ControlFlowError3.lean | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean index 08595dd29b..8cc54c07f1 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean @@ -35,4 +35,4 @@ procedure bar() " #guard_msgs(drop info, error) in -#eval testInputWithOffset "AssertFalse" program 14 processLaurelFile +#eval testInputWithOffset "AssertFalse" program 17 processLaurelFile diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean new file mode 100644 index 0000000000..5e1a2dc9e0 --- /dev/null +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean @@ -0,0 +1,33 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +meta import all StrataTest.Util.TestDiagnostics +meta import all StrataTest.Languages.Laurel.TestExamples + +meta section + +open StrataTest.Util +open Strata + +namespace Strata.Laurel + +def program := r" +procedure letExpressionsInTransparent() returns (r: int) { + var x: int := 0; + var y: int := x + 1; + var z: int := y + 1; + return z +}; + +procedure callLetExpressionsInTransparent() opaque { + var x: int := letExpressionsInTransparent(); + assert x == 2 +}; +" + +#guard_msgs (error, drop all) in +#eval! testInputWithOffset "ControlFlowError" program 14 processLaurelFile From fc214a39516465a6e6778bf115ae3daa81d08428 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Thu, 11 Jun 2026 14:48:58 +0000 Subject: [PATCH 08/42] Test fixes --- .../Laurel/EliminateReturnsInExpression.lean | 2 ++ .../Laurel/EliminateValuesInReturns.lean | 7 ----- .../Laurel/Grammar/LaurelGrammar.lean | 4 --- .../Laurel/LaurelCompilationPipeline.lean | 30 +++++------------- .../Laurel/LiftImperativeExpressions.lean | 21 ------------- .../T2_ImpureExpressionsError.lean | 9 +----- .../T2_ImpureExpressionsError2.lean | 31 +++++++++++++++++++ .../Examples/Fundamentals/T3_ControlFlow.lean | 3 +- .../Fundamentals/T6_Preconditions.lean | 6 ++-- 9 files changed, 46 insertions(+), 67 deletions(-) create mode 100644 StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean diff --git a/Strata/Languages/Laurel/EliminateReturnsInExpression.lean b/Strata/Languages/Laurel/EliminateReturnsInExpression.lean index d316a8d2f5..889f2de7b1 100644 --- a/Strata/Languages/Laurel/EliminateReturnsInExpression.lean +++ b/Strata/Languages/Laurel/EliminateReturnsInExpression.lean @@ -41,6 +41,8 @@ def removeReturns (stmt : StmtExprMd) : Except DiagnosticModel StmtExprMd := | .Assert _ => .ok passThrough | .Block _ _ => .ok passThrough | .IfThenElse _ _ (some _) => .error (diagnosticFromSource head.source "in a transparent body, if-then-else is only supported as the last statement in a block") + | .While _ _ _ _ => .error $ diagnosticFromSource head.source $ "loops are not supported in transparent bodies or contracts" + | .Var (.Declare _) => .error $ diagnosticFromSource head.source $ "local variables must have initializers in transparent bodies or contracts" | _ => .error (diagnosticFromSource head.source s!"unsupported statement {head.val.constructorName} in block head") | .IfThenElse cond thenBr (some elseBr) => do diff --git a/Strata/Languages/Laurel/EliminateValuesInReturns.lean b/Strata/Languages/Laurel/EliminateValuesInReturns.lean index 5112d6aa85..0c6aa7506c 100644 --- a/Strata/Languages/Laurel/EliminateValuesInReturns.lean +++ b/Strata/Languages/Laurel/EliminateValuesInReturns.lean @@ -53,16 +53,9 @@ def hasValuedReturn (stmt : StmtExprMd) : Bool := all_goals (try term_by_mem) all_goals omega -<<<<<<< HEAD /-- Apply value-return elimination to a single procedure. Only applies to procedures with exactly one output parameter. Emits an error if a valued return is used with multiple output parameters. -/ -======= -/-- Apply value-return elimination to a single procedure. Rewrites `return expr` - into `outParam := expr; return` for any procedure with exactly one output - parameter (functional and non-functional alike). - Emits an error if a valued return is used with zero or multiple output parameters. -/ ->>>>>>> issue-924-contract-and-proof-pass def eliminateValuesInReturnsInProc (proc : Procedure) : Procedure × Array DiagnosticModel := match proc.outputs with | [outParam] => diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean index e961cf298c..f8cd602b88 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean @@ -9,11 +9,7 @@ module -- 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. -<<<<<<< HEAD -- Last grammar change: remove functions -======= --- Last grammar change: `return` value is now Option StmtExpr (supports a valueless return). ->>>>>>> issue-924-contract-and-proof-pass public import StrataDDM.AST import StrataDDM.BuiltinDialects.Init import StrataDDM.Integration.Lean.HashCommands diff --git a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean index 3792652704..03a8a3af68 100644 --- a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean +++ b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean @@ -293,31 +293,15 @@ def translateWithLaurel (options : LaurelTranslateOptions) (program : Program) emit pass.name "unorderedCoreWithLaurelTypes.st" unorderedCore let coreWithLaurelTypes := orderFunctionsAndProcedures unorderedCore -<<<<<<< HEAD - -- This early return is a simple way to protect against duplicative errors. Without this return, - -- resolution errors reported by Laurel would also be reported by Core. - -- There might be better solution that allows getting some resolution errors from Laurel and some verification errors from Core, - -- but that would need more consideration. - if ! passDiags.isEmpty then - return (none, passDiags, program, stats) - else - emit "CoreWithLaurelTypes" "core.st" coreWithLaurelTypes - let initState : TranslateState := { - model := fnModel, - overflowChecks := options.overflowChecks, - procedureNames := coreWithLaurelTypes.decls.foldl (fun r d => match d with - | .procedure p => r.insert p.name.text - | _ => r ) (Std.HashSet.emptyWithCapacity 0) - } - let (coreProgramOption, translateState) := - runTranslateM initState (translateLaurelToCore options coreWithLaurelTypes) - -- Because of the duplication between functions and procedures, this translation is liable to create duplicate diagnostics - let mut allDiagnostics: List DiagnosticModel := passDiags ++ translateState.diagnostics.eraseDups; -======= ->>>>>>> issue-924-contract-and-proof-pass emit "CoreWithLaurelTypes" "core.st" coreWithLaurelTypes - let initState : TranslateState := { model := fnModel, overflowChecks := options.overflowChecks } + let initState : TranslateState := { + model := fnModel, + overflowChecks := options.overflowChecks, + procedureNames := coreWithLaurelTypes.decls.foldl (fun r d => match d with + | .procedure p => r.insert p.name.text + | _ => r ) (Std.HashSet.emptyWithCapacity 0) + } let (coreProgramOption, translateState) := runTranslateM initState (translateLaurelToCore options coreWithLaurelTypes) -- Because of the duplication between functions and procedures, this translation is liable to create duplicate diagnostics diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index cd47e58393..80cdab5987 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -190,27 +190,6 @@ def containsAssignmentOrImperativeCall (imperativeCallees : List String) (expr : all_goals (try term_by_mem) all_goals omega -<<<<<<< HEAD -/-- Like containsAssignment but does NOT recurse into Blocks (treats them as opaque). - Used by assert/assume handlers to allow generated Block wrappers through. -/ -def containsBareAssignment (expr : StmtExprMd) : Bool := - match expr with - | AstNode.mk val _ => - match val with - | .Assign .. => true - | .StaticCall _ args => args.attach.any (fun x => containsBareAssignment x.val) - | .PrimitiveOp _ args _ => args.attach.any (fun x => containsBareAssignment x.val) - | .Block _ _ => false - | .IfThenElse cond th el => - containsBareAssignment cond || containsBareAssignment th || - match el with | some e => containsBareAssignment e | none => false - | _ => false - termination_by expr - decreasing_by - all_goals ((try cases x); simp_all; try term_by_mem) - -======= ->>>>>>> issue-924-contract-and-proof-pass /-- Shared logic for lifting an assignment in expression position: prepends the assignment, creates before-snapshots for all targets, diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean index fbb32a2c6a..06d734b8f9 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean @@ -31,13 +31,6 @@ procedure transparentWithMutatingAssignment(x: int): int return 3 }; -procedure transparentWithWhile(x: int): int -{ - while(false) {}; -//^^^^^^^^^^^^^^^ error: loops are not supported in transparent bodies or contracts - return 3 -}; - procedure callsHasMutatingAssignment(x: int): int opaque { @@ -61,7 +54,7 @@ procedure impureContractIsNotLegal2(x: int) " #guard_msgs (error, drop all) in -#eval! testInputWithOffset "NestedImpureStatements" program 14 processLaurelFile +#eval! testInputWithOffset "NestedImpureStatements" program 17 processLaurelFile end Laurel diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean new file mode 100644 index 0000000000..8483cb8fca --- /dev/null +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean @@ -0,0 +1,31 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +meta import all StrataTest.Util.TestDiagnostics +meta import all StrataTest.Languages.Laurel.TestExamples + +meta section + +open StrataTest.Util +open Strata + +namespace Strata.Laurel + +def program: String := r" +procedure transparentWithWhile(x: int): int +{ + while(false) {}; +//^^^^^^^^^^^^^^^ error: loops are not supported in transparent bodies or contracts + return 3 +}; +" + +#guard_msgs (error, drop all) in +#eval! testInputWithOffset "NestedImpureStatements" program 17 processLaurelFile + + +end Laurel diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean index 5ef3613ee3..c6f56a3eea 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean @@ -19,6 +19,7 @@ def program := r" procedure assertAndAssumeInTransparent(a: int) returns (r: int) { assert 2 == 3; +//^^^^^^^^^^^^^ error: assertion does not hold assume true; return a }; @@ -111,4 +112,4 @@ procedure valuelessEarlyReturn(b: bool) " #guard_msgs (error, drop all) in -#eval! testInputWithOffset "ControlFlow" program 14 processLaurelFile +#eval! testInputWithOffset "ControlFlow" program 17 processLaurelFile diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean index 56c3abfe41..7c62f9c928 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean @@ -25,7 +25,7 @@ procedure hasRequires(x: int) returns (r: int) { assert x > 0; assert x > 3; -//^^^^^^^^^^^^ error: assertion does not hold +//^^^^^^^^^^^^ error: assertion could not be proved x + 1 }; @@ -33,7 +33,7 @@ procedure caller() opaque { var x: int := hasRequires(1); -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: precondition does not hold +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: precondition could not be proved var y: int := hasRequires(3) }; @@ -55,4 +55,4 @@ procedure multipleRequiresCaller() " #guard_msgs (drop info, error) in -#eval testInputWithOffset "Preconditions" program 14 processLaurelFile +#eval testInputWithOffset "Preconditions" program 17 processLaurelFile From bfbf99a71524c2e38943efd8918dc51cbac76649 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Thu, 11 Jun 2026 16:13:47 +0000 Subject: [PATCH 09/42] Fix ControlFlowError test --- Strata/Languages/Laurel/LaurelToCoreTranslator.lean | 4 +++- .../Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean index f9175e550d..dfbab4d402 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean @@ -269,7 +269,9 @@ def translateExpr (expr : StmtExprMd) let valueExpr ← translateExpr initializer boundVars isPureContext let bodyExpr ← translateExpr { val := StmtExpr.Block rest label, source := innerSrc } (name :: boundVars) isPureContext let coreMonoType ← translateType ty - return .app () (.abs () name.text (some coreMonoType) bodyExpr) valueExpr + disallowed innerSrc "local variables in functions are not YET supported" + -- This doesn't work because of a limitation in Core. + -- return .app () (.abs () (some coreMonoType) bodyExpr) valueExpr | .Block (⟨ .Var (.Declare _), innerSrc⟩ :: rest) label => do _ ← disallowed innerSrc "local variables must have initializers in transparent bodies or contracts " translateExpr { val := StmtExpr.Block rest label, source := innerSrc } boundVars isPureContext diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean index 5e1a2dc9e0..da8a5255ad 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean @@ -18,8 +18,11 @@ namespace Strata.Laurel def program := r" procedure letExpressionsInTransparent() returns (r: int) { var x: int := 0; +//^^^^^^^^^^^^^^^ error: local variables in functions are not YET supported var y: int := x + 1; +//^^^^^^^^^^^^^^^^^^^ error: local variables in functions are not YET supported var z: int := y + 1; +//^^^^^^^^^^^^^^^^^^^ error: local variables in functions are not YET supported return z }; @@ -30,4 +33,4 @@ procedure callLetExpressionsInTransparent() opaque { " #guard_msgs (error, drop all) in -#eval! testInputWithOffset "ControlFlowError" program 14 processLaurelFile +#eval! testInputWithOffset "ControlFlowError" program 17 processLaurelFile From cbb1b07e648c5b8c610e012d269162b073f1a141 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Thu, 11 Jun 2026 16:30:50 +0000 Subject: [PATCH 10/42] Improve error message --- .../Laurel/LaurelToCoreTranslator.lean | 2 +- .../AbstractToConcreteTreeTranslatorTest.lean | 3 +- .../Examples/Fundamentals/T3_ControlFlow.lean | 36 +++++++++++++------ .../Fundamentals/T3_ControlFlowError3.lean | 6 ++-- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean index dfbab4d402..981e39cc03 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreTranslator.lean @@ -269,7 +269,7 @@ def translateExpr (expr : StmtExprMd) let valueExpr ← translateExpr initializer boundVars isPureContext let bodyExpr ← translateExpr { val := StmtExpr.Block rest label, source := innerSrc } (name :: boundVars) isPureContext let coreMonoType ← translateType ty - disallowed innerSrc "local variables in functions are not YET supported" + disallowed innerSrc "local variables in transparent bodies are not YET supported" -- This doesn't work because of a limitation in Core. -- return .app () (.abs () (some coreMonoType) bodyExpr) valueExpr | .Block (⟨ .Var (.Declare _), innerSrc⟩ :: rest) label => do diff --git a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean index c408dca363..b9de3974e6 100644 --- a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean +++ b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean @@ -290,7 +290,8 @@ info: procedure test(): int info: procedure earlyExit(b: bool) opaque { - if b then { + if b + then { return }; assert true diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean index c6f56a3eea..29b52584e8 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean @@ -69,22 +69,36 @@ procedure testFunctions() procedure guards(a: int) returns (r: int) { - var b: int := a + 2; - if b > 2 then { - var c: int := b + 3; - if c > 3 then { - return c + 4 + if a > 2 then { + if a > 3 then { + return 4 }; - var d: int := c + 5; - return d + 6 + return 6 }; - var e: int := b + 1; - assert e <= 3; - assert e < 3; + assert a <= 2; + assert a < 2; //^^^^^^^^^^^^ error: assertion does not hold - return e + return 5 }; +// +// procedure guards(a: int) returns (r: int) +// { +// var b: int := a + 2; +// if b > 2 then { +// var c: int := b + 3; +// if c > 3 then { +// return c + 4 +// }; +// //var d: int := c + 5; +// return d + 6 +// }; +// //var e: int := b + 1; +// assert e <= 3; +// assert e < 3; +// return e +// }; + procedure dag(a: int) returns (r: int) opaque { diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean index da8a5255ad..733b855dd6 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean @@ -18,11 +18,11 @@ namespace Strata.Laurel def program := r" procedure letExpressionsInTransparent() returns (r: int) { var x: int := 0; -//^^^^^^^^^^^^^^^ error: local variables in functions are not YET supported +//^^^^^^^^^^^^^^^ error: local variables in transparent bodies are not YET supported var y: int := x + 1; -//^^^^^^^^^^^^^^^^^^^ error: local variables in functions are not YET supported +//^^^^^^^^^^^^^^^^^^^ error: local variables in transparent bodies are not YET supported var z: int := y + 1; -//^^^^^^^^^^^^^^^^^^^ error: local variables in functions are not YET supported +//^^^^^^^^^^^^^^^^^^^ error: local variables in transparent bodies are not YET supported return z }; From d2b7b23d691536ec71161af51e38fd7ddf3ea5a0 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Thu, 11 Jun 2026 16:51:58 +0000 Subject: [PATCH 11/42] Update if-then-else formatting --- .../Languages/Laurel/Grammar/LaurelGrammar.lean | 2 +- Strata/Languages/Laurel/Grammar/LaurelGrammar.st | 2 +- .../AbstractToConcreteTreeTranslatorTest.lean | 10 +++++----- StrataTest/Languages/Laurel/LiftHolesTest.lean | 16 ++++++++-------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean index f8cd602b88..d55e1b86c5 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean @@ -9,7 +9,7 @@ module -- 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: remove functions +-- Last grammar change: indent then/else branches in ifThenElse public import StrataDDM.AST import StrataDDM.BuiltinDialects.Init import StrataDDM.Integration.Lean.HashCommands diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st index 238bc1392f..e0669542b8 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st @@ -101,7 +101,7 @@ category ElseBranch; op elseBranch(stmts : StmtExpr) : ElseBranch => @[prec(0)] "\nelse " stmts; op ifThenElse (cond: StmtExpr, thenBranch: StmtExpr, elseBranch: Option ElseBranch): StmtExpr => - @[prec(20)] "if " cond "\nthen " thenBranch:0 elseBranch:0; + @[prec(20)] "if " cond indent(2, "\nthen " thenBranch:0 elseBranch:0); op assert (cond : StmtExpr, errorMessage: Option ErrorSummary) : StmtExpr => @[prec(0)] "assert " cond:0 errorMessage:0; op assume (cond : StmtExpr) : StmtExpr => @[prec(0)] "assume " cond:0; diff --git a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean index b9de3974e6..3e6fa1bd80 100644 --- a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean +++ b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean @@ -110,8 +110,8 @@ info: procedure test(x: int): int opaque { if x > 0 - then x - else 0 - x + then x + else 0 - x }; -/ #guard_msgs in @@ -291,9 +291,9 @@ info: procedure earlyExit(b: bool) opaque { if b - then { - return - }; + then { + return + }; assert true }; -/ diff --git a/StrataTest/Languages/Laurel/LiftHolesTest.lean b/StrataTest/Languages/Laurel/LiftHolesTest.lean index 45a4d368f6..e4493af565 100644 --- a/StrataTest/Languages/Laurel/LiftHolesTest.lean +++ b/StrataTest/Languages/Laurel/LiftHolesTest.lean @@ -143,9 +143,9 @@ procedure test() opaque { if $hole_0() - then { - assert true - } + then { + assert true + } }; -/ #guard_msgs in @@ -164,8 +164,8 @@ procedure test() opaque { var x: int := if true - then $hole_0() - else 0 + then $hole_0() + else 0 }; -/ #guard_msgs in @@ -324,9 +324,9 @@ procedure test() opaque { if 1 + $hole_0() > 0 - then { - assert true - } + then { + assert true + } }; -/ #guard_msgs in From a8bfb8094b7dadcd11b1f49b11189187de4ac44f Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Thu, 11 Jun 2026 17:03:43 +0000 Subject: [PATCH 12/42] Fix bug related to assert/assume lifting when they are in conditions --- .../Laurel/LiftImperativeExpressions.lean | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index 80cdab5987..25fcf943ed 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -294,11 +294,28 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do return ⟨.Var (.Local callResultVar), source⟩ | .IfThenElse cond thenBranch elseBranch => - let imperativeCallees := (← get).imperativeCallees - let thenHasAssign := containsAssignmentOrImperativeCall imperativeCallees thenBranch - let elseHasAssign := match elseBranch with - | some e => containsAssignmentOrImperativeCall imperativeCallees e - | none => false + -- Decide whether the whole if-then-else must be lifted into statement + -- position. This is needed whenever processing a branch lifts *anything* + -- (assignments, imperative calls, asserts, assumes, nondeterministic + -- holes, ...) — not just assignments. If we left such lifted statements + -- in the surrounding prepend stack, they would escape the branch's guard + -- and run unconditionally. + -- + -- Rather than statically enumerating every liftable construct, we + -- trial-run `transformExpr` on each branch and observe whether it pushed + -- anything onto the prepend stack. The full state (including fresh-name + -- counters and substitutions) is saved and restored, so the trial has no + -- observable effect on the real transformation below. + let savedState ← get + modify fun s => { s with prependedStmts := [], subst := [] } + let _ ← transformExpr thenBranch + let thenHasAssign := !(← get).prependedStmts.isEmpty + modify fun s => { s with prependedStmts := [], subst := [] } + let _ ← match elseBranch with + | some e => do let _ ← transformExpr e; pure () + | none => pure () + let elseHasAssign := !(← get).prependedStmts.isEmpty + set savedState if thenHasAssign || elseHasAssign then -- Infer type from the ORIGINAL then-branch (not the transformed one), From 7ae018f368a9736e1ac18c4fa22ec6c2ffd23d1e Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Thu, 11 Jun 2026 17:11:06 +0000 Subject: [PATCH 13/42] Add missing test to T2 --- .../Examples/Fundamentals/T2_ImpureExpressions.lean | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index 5b78852702..426133620e 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -149,10 +149,20 @@ procedure addProcCaller(): int // var z: int := addProc({x := 1; x}, {x := x + 10; x}) + (x := 3); // assert z == 15 }; + +procedure assertInsideConditionalExpression(a: int): int + return if a > 2 + then 4 + else { + assert a <= 2; + assert a < 2; +// ^^^^^^^^^^^^ error: assertion does not hold + 5 + }; " #guard_msgs (error, drop all) in -#eval! testInputWithOffset "NestedImpureStatements" program 14 processLaurelFile +#eval! testInputWithOffset "NestedImpureStatements" program 17 processLaurelFile end Laurel From 7cb5fee34a2e39036b31d917873c0eb4d1bcf652 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Fri, 12 Jun 2026 09:39:02 +0000 Subject: [PATCH 14/42] Refactoring lifting pass --- .../Laurel/LiftImperativeExpressions.lean | 92 +++++++++---------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index 25fcf943ed..676485d5d4 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -142,45 +142,50 @@ private def computeType (expr : StmtExprMd) : LiftM HighTypeMd := do let s ← get return computeExprType s.model expr -/-- Check if an expression contains any assignments or imperative calls (recursively). -/ -def containsAssignmentOrImperativeCall (imperativeCallees : List String) (expr : StmtExprMd) : Bool := +/-- Check if an expression contains any assignments or imperative calls +(recursively). When `liftsAssertsAssumes` is set, asserts and assumes also +count — these are lifted into statement position by `transformExpr`, so an +if-then-else whose branch contains one must itself be lifted to keep the +statement guarded by the condition. -/ +def containsAssignmentOrImperativeCall (imperativeCallees : List String) (expr : StmtExprMd) + (liftsAssertsAssumes : Bool := false) : Bool := match expr with | AstNode.mk val _ => match val with | .Assign .. => true | .StaticCall name args1 => imperativeCallees.contains name.text || - args1.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val) - | .PrimitiveOp _ args2 _ => args2.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val) - | .Block stmts _ => stmts.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val) + args1.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) + | .PrimitiveOp _ args2 _ => args2.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) + | .Block stmts _ => stmts.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) | .IfThenElse cond th el => - containsAssignmentOrImperativeCall imperativeCallees cond || - containsAssignmentOrImperativeCall imperativeCallees th || - match el with | some e => containsAssignmentOrImperativeCall imperativeCallees e | none => false - | .Assume cond => containsAssignmentOrImperativeCall imperativeCallees cond - | .Assert cond => containsAssignmentOrImperativeCall imperativeCallees cond.condition + containsAssignmentOrImperativeCall imperativeCallees cond liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees th liftsAssertsAssumes || + match el with | some e => containsAssignmentOrImperativeCall imperativeCallees e liftsAssertsAssumes | none => false + | .Assume cond => liftsAssertsAssumes || containsAssignmentOrImperativeCall imperativeCallees cond liftsAssertsAssumes + | .Assert cond => liftsAssertsAssumes || containsAssignmentOrImperativeCall imperativeCallees cond.condition liftsAssertsAssumes | .InstanceCall target _ args => - containsAssignmentOrImperativeCall imperativeCallees target || - args.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val) + containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes || + args.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) | .Quantifier _ _ trigger body => - containsAssignmentOrImperativeCall imperativeCallees body || - match trigger with | some t => containsAssignmentOrImperativeCall imperativeCallees t | none => false - | .Old value => containsAssignmentOrImperativeCall imperativeCallees value - | .Fresh value => containsAssignmentOrImperativeCall imperativeCallees value + containsAssignmentOrImperativeCall imperativeCallees body liftsAssertsAssumes || + match trigger with | some t => containsAssignmentOrImperativeCall imperativeCallees t liftsAssertsAssumes | none => false + | .Old value => containsAssignmentOrImperativeCall imperativeCallees value liftsAssertsAssumes + | .Fresh value => containsAssignmentOrImperativeCall imperativeCallees value liftsAssertsAssumes | .ProveBy value proof => - containsAssignmentOrImperativeCall imperativeCallees value || - containsAssignmentOrImperativeCall imperativeCallees proof + containsAssignmentOrImperativeCall imperativeCallees value liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees proof liftsAssertsAssumes | .ReferenceEquals lhs rhs => - containsAssignmentOrImperativeCall imperativeCallees lhs || - containsAssignmentOrImperativeCall imperativeCallees rhs + containsAssignmentOrImperativeCall imperativeCallees lhs liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees rhs liftsAssertsAssumes | .PureFieldUpdate target _ newValue => - containsAssignmentOrImperativeCall imperativeCallees target || - containsAssignmentOrImperativeCall imperativeCallees newValue - | .AsType target _ => containsAssignmentOrImperativeCall imperativeCallees target - | .IsType target _ => containsAssignmentOrImperativeCall imperativeCallees target - | .Assigned name => containsAssignmentOrImperativeCall imperativeCallees name - | .ContractOf _ func => containsAssignmentOrImperativeCall imperativeCallees func - | .Return (some v) => containsAssignmentOrImperativeCall imperativeCallees v + containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees newValue liftsAssertsAssumes + | .AsType target _ => containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes + | .IsType target _ => containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes + | .Assigned name => containsAssignmentOrImperativeCall imperativeCallees name liftsAssertsAssumes + | .ContractOf _ func => containsAssignmentOrImperativeCall imperativeCallees func liftsAssertsAssumes + | .Return (some v) => containsAssignmentOrImperativeCall imperativeCallees v liftsAssertsAssumes | _ => false termination_by expr decreasing_by @@ -294,28 +299,15 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do return ⟨.Var (.Local callResultVar), source⟩ | .IfThenElse cond thenBranch elseBranch => - -- Decide whether the whole if-then-else must be lifted into statement - -- position. This is needed whenever processing a branch lifts *anything* - -- (assignments, imperative calls, asserts, assumes, nondeterministic - -- holes, ...) — not just assignments. If we left such lifted statements - -- in the surrounding prepend stack, they would escape the branch's guard - -- and run unconditionally. - -- - -- Rather than statically enumerating every liftable construct, we - -- trial-run `transformExpr` on each branch and observe whether it pushed - -- anything onto the prepend stack. The full state (including fresh-name - -- counters and substitutions) is saved and restored, so the trial has no - -- observable effect on the real transformation below. - let savedState ← get - modify fun s => { s with prependedStmts := [], subst := [] } - let _ ← transformExpr thenBranch - let thenHasAssign := !(← get).prependedStmts.isEmpty - modify fun s => { s with prependedStmts := [], subst := [] } - let _ ← match elseBranch with - | some e => do let _ ← transformExpr e; pure () - | none => pure () - let elseHasAssign := !(← get).prependedStmts.isEmpty - set savedState + let imperativeCallees := (← get).imperativeCallees + -- A branch must be lifted if it contains anything `transformExpr` would + -- hoist: assignments, imperative calls, asserts, or assumes. (Asserts and + -- assumes matter because hoisting them out of the branch would drop the + -- condition's guard — see `liftsAssertsAssumes`.) + let thenHasAssign := containsAssignmentOrImperativeCall imperativeCallees thenBranch (liftsAssertsAssumes := true) + let elseHasAssign := match elseBranch with + | some e => containsAssignmentOrImperativeCall imperativeCallees e (liftsAssertsAssumes := true) + | none => false if thenHasAssign || elseHasAssign then -- Infer type from the ORIGINAL then-branch (not the transformed one), @@ -360,7 +352,7 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do modify fun s => { s with prependedStmts := condPrepends ++ s.prependedStmts } return default else - -- No assignments in branches — recurse normally + -- No liftable statements in branches — recurse normally. let seqCond ← transformExpr cond let seqThen ← transformExpr thenBranch let seqElse ← match elseBranch with From efafe4359ca00b48fd108109c3b09aeb6330ebac Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 16 Jun 2026 11:41:29 +0000 Subject: [PATCH 15/42] Fixes --- .../T2_ImpureExpressionsError2.lean | 17 +++++------------ .../Fundamentals/T3_ControlFlowError3.lean | 14 +++++--------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean index 8483cb8fca..3eb77656bd 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean @@ -5,27 +5,20 @@ -/ module -meta import all StrataTest.Util.TestDiagnostics -meta import all StrataTest.Languages.Laurel.TestExamples - -meta section +import StrataTest.Util.TestLaurel open StrataTest.Util open Strata -namespace Strata.Laurel +#eval testLaurel <| +#strata +program Laurel; -def program: String := r" procedure transparentWithWhile(x: int): int { while(false) {}; //^^^^^^^^^^^^^^^ error: loops are not supported in transparent bodies or contracts return 3 }; -" - -#guard_msgs (error, drop all) in -#eval! testInputWithOffset "NestedImpureStatements" program 17 processLaurelFile - -end Laurel +#end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean index 733b855dd6..6c0094d92b 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean @@ -5,17 +5,15 @@ -/ module -meta import all StrataTest.Util.TestDiagnostics -meta import all StrataTest.Languages.Laurel.TestExamples - -meta section +import StrataTest.Util.TestLaurel open StrataTest.Util open Strata -namespace Strata.Laurel +#eval testLaurel <| +#strata +program Laurel; -def program := r" procedure letExpressionsInTransparent() returns (r: int) { var x: int := 0; //^^^^^^^^^^^^^^^ error: local variables in transparent bodies are not YET supported @@ -30,7 +28,5 @@ procedure callLetExpressionsInTransparent() opaque { var x: int := letExpressionsInTransparent(); assert x == 2 }; -" -#guard_msgs (error, drop all) in -#eval! testInputWithOffset "ControlFlowError" program 17 processLaurelFile +#end From 16f57e62ab79761df42c745c9b7a64e7b8ccec71 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 16 Jun 2026 15:46:20 +0000 Subject: [PATCH 16/42] Add comment --- Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean index 0e0d5b723d..833cbc773a 100644 --- a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean +++ b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean @@ -604,7 +604,9 @@ instance : Inhabited LaurelVerifyOptions where default := {} /-- Unwrap the pattern produced by EliminateValuesInReturns + EliminateReturnStatements: - `{ result := ; exit "$return" } $return` → `` -/ + `{ result := ; exit "$return" } $return` → `` + Support for transparent multi-out procedures is not yet available. +-/ private def unwrapReturnBlock (b : StmtExprMd) : StmtExprMd := match b.val with | .Block [⟨.Assign [⟨.Local _, _⟩] value, _⟩, ⟨.Exit "$return", _⟩] (some "$return") => value From ced5837228f3bad80465b2b1bcda01f654a3c0ab Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 11:25:56 +0000 Subject: [PATCH 17/42] Fixes --- .../Laurel/LiftImperativeExpressions.lean | 33 ++++++++++++++----- .../Fundamentals/T2_ImpureExpressions.lean | 6 ++++ .../T2_ImpureExpressionsError2.lean | 2 +- .../Fundamentals/T3_ControlFlowError.lean | 4 +-- StrataTest/Util/TestLaurel.lean | 14 ++++---- 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index 0d9fdec982..57c26408a3 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -102,6 +102,9 @@ private def freshCondVar : LiftM Identifier := do private def prepend (stmt : StmtExprMd) : LiftM Unit := modify fun s => { s with prependedStmts := stmt :: s.prependedStmts } +private def prependList (stmts : List StmtExprMd) : LiftM Unit := + modify fun s => { s with prependedStmts := stmts ++ s.prependedStmts } + private def onlyKeepSideEffectStmtsAndLast (stmts : List StmtExprMd) : LiftM (List StmtExprMd) := do match stmts with | [] => return [] @@ -215,6 +218,22 @@ private def liftAssignExpr (targets : List VariableMd) (seqValue : StmtExprMd) | _ => pure () mutual + + +/-- +Process an expression in expression context, traversing arguments right to left. +Assignments are lifted to prependedStmts and replaced with snapshot variable references. +-/ +def transformLiftedExpr (expr : StmtExprMd) : LiftM (List StmtExprMd × StmtExprMd) := do + let savedSubst := (← get).subst + let savedPrepends ← takePrepends + modify fun s => { s with prependedStmts := [], subst := []} + let result ← transformExpr expr + let newPrepends ← takePrepends + modify fun s => { s with prependedStmts := savedPrepends, subst := savedSubst } + return (newPrepends, result) + termination_by (sizeOf expr, 1) + /-- Process an expression in expression context, traversing arguments right to left. Assignments are lifted to prependedStmts and replaced with snapshot variable references. @@ -399,17 +418,15 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do return expr | .Assume cond => - let prepends ← takePrepends - _ ← transformExpr cond - let argPrepends ← takePrepends - modify fun s => { s with prependedStmts := argPrepends ++ [expr] ++ prepends } + let (argPrepends, newCond) ← transformLiftedExpr cond + prepend ⟨ .Assume newCond, source⟩ + prependList argPrepends default | .Assert cond => - let prepends ← takePrepends - _ ← transformExpr cond.condition - let argPrepends ← takePrepends - modify fun s => { s with prependedStmts := argPrepends ++ [expr] ++ prepends } + let (argPrepends, newCond) ← transformLiftedExpr cond.condition + prepend ⟨ .Assert {cond with condition := newCond}, source⟩ + prependList argPrepends default | .Return (some retExpr) => diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index 391bbf4b4a..ed15973dc4 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -156,4 +156,10 @@ procedure assertInsideConditionalExpression(a: int): int 5 }; +procedure assertInBlockExpr() +opaque { + var x: int := 0; + var y: int := { assert x == 0; x := 1; x }; + assert y == 1 +}; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean index 3eb77656bd..13df2b4fe4 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean @@ -5,10 +5,10 @@ -/ module +public import StrataDDM.AST import StrataTest.Util.TestLaurel open StrataTest.Util -open Strata #eval testLaurel <| #strata diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean index 246b95b30d..969f65a1ed 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean @@ -13,13 +13,13 @@ open Strata #strata program Laurel; -function localVariableWithoutInitializer(): int { +procedure localVariableWithoutInitializer(): int { var x: int; //^^^^^^^^^^ error: local variables must have initializers in transparent bodies or contracts return 3 }; -function deadCodeAfterIfElse(x: int) returns (r: int) { +procedure deadCodeAfterIfElse(x: int) returns (r: int) { if x > 0 then { return 1 } else { return 2 }; //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: if-then-else only supported as the last statement in a block return 3 diff --git a/StrataTest/Util/TestLaurel.lean b/StrataTest/Util/TestLaurel.lean index 933f320c11..800086dd3b 100644 --- a/StrataTest/Util/TestLaurel.lean +++ b/StrataTest/Util/TestLaurel.lean @@ -3,14 +3,16 @@ SPDX-License-Identifier: Apache-2.0 OR MIT -/ +module +public import Strata.Languages.Laurel.Grammar.LaurelGrammar +public import Strata.Languages.Laurel.LaurelCompilationPipeline +public import StrataDDM.Integration.Lean.HashCommands import StrataDDM.Integration.Lean.HashCommands import StrataDDM.Elab import StrataDDM.BuiltinDialects.Init -import Strata.Languages.Laurel.Grammar.LaurelGrammar import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator import Strata.Languages.Laurel.Resolution -import Strata.Languages.Laurel.LaurelCompilationPipeline import Strata.Languages.Laurel open Strata @@ -258,8 +260,8 @@ private def runAndCheck (block : SourcedProgram) `options` defaults to `defaultLaurelTestOptions` (quiet verifier, default solver). Pass an explicit value to override the solver, timeout, etc. — for example, `(options := { verifyOptions := { .quiet with solver := "z3" } })`. -/ -def testLaurel (block : SourcedProgram) - (options : LaurelVerifyOptions := defaultLaurelTestOptions) : IO Unit := +public def testLaurel (block : SourcedProgram) + (options : LaurelVerifyOptions := default) : IO Unit := runAndCheck block (runLaurelPipelineRaw · options) /-- Path to the directory for intermediate files, inside the build directory. @@ -268,7 +270,7 @@ def buildDir : IO String := do let cwd ← IO.currentDir return s!"{cwd}/.lake/build/intermediatePrograms/" -def testLaurelKeepIntermediates (block : SourcedProgram) : IO Unit := do +public def testLaurelKeepIntermediates (block : SourcedProgram) : IO Unit := do let dir ← buildDir runAndCheck block (runLaurelPipelineRaw · { translateOptions := { keepAllFilesPrefix := dir}}) @@ -276,7 +278,7 @@ def testLaurelKeepIntermediates (block : SourcedProgram) : IO Unit := do Use when the test only cares about resolution, not the verifier — e.g. "shadowing in nested blocks is OK", or asserting a specific resolution error without the verifier surfacing unrelated noise. -/ -def testLaurelResolution (block : SourcedProgram) : IO Unit := +public def testLaurelResolution (block : SourcedProgram) : IO Unit := runAndCheck block runLaurelResolutionRaw end StrataTest.Util From 952a8e6db7b3b668f8c4ff4b6f6fec7787e168c4 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 11:29:26 +0000 Subject: [PATCH 18/42] Fixes --- .../Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean | 2 +- StrataTest/Languages/Laurel/MapStmtExprTest.lean | 1 + StrataTest/Util/TestLaurel.lean | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean index 12871b4353..431cb8d041 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean @@ -132,7 +132,7 @@ procedure forLoopStep() // --- More complex scenarios ------------------------------------------------- -function double(n: int): int { 2 * n }; +procedure double(n: int): int { return 2 * n }; procedure incrementAsFunctionArgument() opaque diff --git a/StrataTest/Languages/Laurel/MapStmtExprTest.lean b/StrataTest/Languages/Laurel/MapStmtExprTest.lean index eee5f05fa0..aac62f379d 100644 --- a/StrataTest/Languages/Laurel/MapStmtExprTest.lean +++ b/StrataTest/Languages/Laurel/MapStmtExprTest.lean @@ -8,6 +8,7 @@ Tests for the generic `mapStmtExprM` traversal. Verifies that `mapStmtExpr id` is the identity: applying it to a parsed program produces identical output. -/ +module import StrataTest.Util.TestLaurel import Strata.Languages.Laurel.MapStmtExpr diff --git a/StrataTest/Util/TestLaurel.lean b/StrataTest/Util/TestLaurel.lean index 800086dd3b..c190634390 100644 --- a/StrataTest/Util/TestLaurel.lean +++ b/StrataTest/Util/TestLaurel.lean @@ -24,7 +24,7 @@ namespace StrataTest.Util /-- Translate a `StrataDDM.Program` (typically produced by `#strata`) to a Laurel `Program`. Used by tests that need to plug in a custom post-translation pipeline stage; throws if translation fails. -/ -def translateLaurel (program : StrataDDM.Program) : IO Laurel.Program := do +public def translateLaurel (program : StrataDDM.Program) : IO Laurel.Program := do match Laurel.TransM.run (Strata.Uri.file "<#strata>") (Laurel.parseProgram program) with | .error e => throw (IO.userError s!"Translation errors: {e}") | .ok laurelProgram => pure laurelProgram From ceeb0509f87e3487abf80923b15e6be37ed5d47e Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 11:52:29 +0000 Subject: [PATCH 19/42] Fix to contract pass and test expectations --- Strata/Languages/Laurel/ConstrainedTypeElim.lean | 8 ++++---- Strata/Languages/Laurel/ContractPass.lean | 14 +++++++------- .../Fundamentals/T10_ConstrainedTypes.lean | 14 +++++++------- .../Examples/Fundamentals/T22_ArityMismatch.lean | 5 ++--- .../Objects/T1b_HeapMutatingValueReturn.lean | 2 +- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Strata/Languages/Laurel/ConstrainedTypeElim.lean b/Strata/Languages/Laurel/ConstrainedTypeElim.lean index 488a1f9b5d..210459d859 100644 --- a/Strata/Languages/Laurel/ConstrainedTypeElim.lean +++ b/Strata/Languages/Laurel/ConstrainedTypeElim.lean @@ -61,9 +61,9 @@ def constraintCallFor (ptMap : ConstrainedTypeMap) (ty : HighType) 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 := +/-- Generate a constraint procedure for a constrained type. + For nested types, the procedure calls the parent's constraint function. -/ +def mkConstraintProc (ptMap : ConstrainedTypeMap) (ct : ConstrainedType) : Procedure := let baseType := resolveType ptMap ct.base let bodyExpr := match ct.base.val with | .UserDefined parent => @@ -233,7 +233,7 @@ public def constrainedTypeElim (_model : SemanticModel) (program : Program) let ptMap := buildConstrainedTypeMap program.types if ptMap.isEmpty then (program, []) else let constraintFuncs := program.types.filterMap fun - | .Constrained ct => some (mkConstraintFunc ptMap ct) | _ => none + | .Constrained ct => some (mkConstraintProc ptMap ct) | _ => none let witnessProcedures := program.types.filterMap fun | .Constrained ct => some (mkWitnessProc ptMap ct) | _ => none ({ program with diff --git a/Strata/Languages/Laurel/ContractPass.lean b/Strata/Languages/Laurel/ContractPass.lean index 11bb47f309..04503b19cc 100644 --- a/Strata/Languages/Laurel/ContractPass.lean +++ b/Strata/Languages/Laurel/ContractPass.lean @@ -105,19 +105,19 @@ private def transformProcBody (proc : Procedure) (info : ContractInfo) : Body := let preAssumes : List StmtExprMd := proc.preconditions.zip info.preNames |>.map fun (pc, name, _) => ⟨.Assume (mkCall name inputArgs), pc.condition.source⟩ + let postAsserts : List StmtExprMd := + postconds.zip info.postNames |>.map fun (pc, _name, _summary) => + let summary := pc.summary.getD "postcondition" + ⟨.Assert { condition := pc.condition, summary := some summary }, pc.condition.source⟩ match proc.body with | .Transparent body => - let postAsserts : List StmtExprMd := - postconds.zip info.postNames |>.map fun (pc, _name, _summary) => - let summary := pc.summary.getD "postcondition" - ⟨.Assert { condition := pc.condition, summary := some summary }, pc.condition.source⟩ .Transparent ⟨.Block (preAssumes ++ [body] ++ postAsserts) none, body.source⟩ | .Opaque _ (some impl) _ => - .Opaque postconds (some ⟨.Block (preAssumes ++ [impl]) none, impl.source⟩) [] + .Opaque [] (some ⟨.Block (preAssumes ++ [impl] ++ postAsserts) none, impl.source⟩) [] | .Opaque _ none mods => - .Opaque postconds none mods + .Opaque [] none mods | .Abstract _ => - .Abstract postconds + .Abstract [] | b => b /-- Monad used by the contract-pass rewriter; carries a global counter for diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean index 1b46b06c32..c33b168bb0 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean @@ -31,7 +31,7 @@ procedure outputValid(): nat // Output constraint — invalid return fails procedure outputInvalid(): nat -// ^^^ error: postcondition does not hold +// ^^^ error: postcondition could not be proved opaque { return -1 @@ -59,7 +59,7 @@ procedure assignInvalid() opaque { var y: nat := -1 -//^^^^^^^^^^^^^^^^ error: assertion does not hold +//^^^^^^^^^^^^^^^^ error: assertion could not be proved }; // Reassignment to constrained-typed variable — invalid @@ -68,7 +68,7 @@ procedure reassignInvalid() { var y: nat := 5; y := -1 -//^^^^^^^ error: assertion does not hold +//^^^^^^^ error: assertion could not be proved }; // Argument to constrained-typed parameter — valid @@ -87,7 +87,7 @@ procedure argInvalid() returns (r: int) opaque { var x: int := takesNat(-1); -//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: precondition does not hold +//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: precondition could not be proved return x }; @@ -128,7 +128,7 @@ procedure constrainedInExpr() // Invalid witness — witness -1 does not satisfy x > 0 constrained bad = x: int where x > 0 witness -1 -// ^^ error: assertion does not hold +// ^^ error: assertion could not be proved // Uninitialized constrained variable — havoc + assume constraint procedure uninitNat() @@ -153,7 +153,7 @@ procedure uninitNotWitness() { var y: posnat; assert y == 1 -//^^^^^^^^^^^^^ error: assertion does not hold +//^^^^^^^^^^^^^ error: assertion could not be proved }; // Quantifier constraint injection — forall @@ -187,6 +187,6 @@ procedure captureTest(y: haslarger) opaque { assert false -//^^^^^^^^^^^^ error: assertion does not hold +//^^^^^^^^^^^^ error: assertion could not be proved }; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean index e5ecfe57dd..f5a3571f8d 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean @@ -12,14 +12,13 @@ open Strata /-! ## Function called with too many arguments -/ /-- -error: <#strata>(436-457) ❌ Type checking error. -Impossible to unify int with (arrow int $__ty35). +error: input length and args length mismatch -/ #guard_msgs in #eval testLaurel <| #strata program Laurel; -procedure f(x: int): int { x }; +procedure f(x: int): int { return x }; procedure caller() opaque diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean b/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean index 1a18972604..f699562724 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean @@ -40,7 +40,7 @@ composite Container { procedure setAndReturnBuggy(c: Container, x: int) returns (r: int) opaque ensures r == x + 1 -// ^^^^^^^^^^ error: postcondition does not hold +// ^^^^^^^^^^ error: postcondition could not be proved modifies c { c#value := x; From f7eef03a8bc7a5a39b677732400a92d6b5e9a87d Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 12:10:03 +0000 Subject: [PATCH 20/42] Fix in lifting pass --- .../Laurel/LiftImperativeExpressions.lean | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index 57c26408a3..3ecb095021 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -86,15 +86,13 @@ structure LiftState where @[expose] abbrev LiftM := StateM LiftState -private def emptyMd : Option String := none - private def freshTempFor (varName : Identifier) : LiftM Identifier := do let counters := (← get).varCounters let counter := counters.find? (·.1 == varName) |>.map (·.2) |>.getD 0 modify fun s => { s with varCounters := (varName, counter + 1) :: s.varCounters.filter (·.1 != varName) } return s!"${varName.text}_{counter}" -private def freshCondVar : LiftM Identifier := do +private def freshTempVar : LiftM Identifier := do let n := (← get).condCounter modify fun s => { s with condCounter := n + 1 } return s!"$cndtn_{n}" @@ -249,7 +247,7 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do | .Hole false (some holeType) => -- Nondeterministic typed hole: lift to a fresh variable with no initializer (havoc) - let holeVar ← freshCondVar + let holeVar ← freshTempVar prepend ⟨ .Var (.Declare ⟨holeVar, holeType⟩), source⟩ return ⟨ .Var (.Local holeVar), source ⟩ @@ -292,10 +290,9 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do let seqCall := ⟨.StaticCall callee seqArgs.reverse, source⟩ return seqCall else - let startingPrepend ← takePrepends - let seqArgs ← args.reverse.mapM transformExpr - let argsPrepends ← takePrepends - let seqCall := ⟨.StaticCall callee seqArgs.reverse, source⟩ + let seqArgsAndPrepends ← args.reverse.mapM transformLiftedExpr + let seqArgs := seqArgsAndPrepends.map (fun t => t.2) + let seqCall: StmtExprMd := ⟨.StaticCall callee seqArgs.reverse, source⟩ -- Imperative call in expression position: lift to an assignment. -- Only valid for single-output procedures (or unresolved ones where we -- fall back to a single target). Multi-output procedures in expression @@ -305,15 +302,16 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do | .staticProcedure proc => proc.outputs | .instanceProcedure _ proc => proc.outputs | _ => [] - let callResultVar ← freshCondVar + let callResultVar ← freshTempVar let callResultType ← match outputs with | [single] => pure single.type | _ => computeType expr - let liftedCall := [ + let liftedCall: List StmtExprMd := [ ⟨ (.Var (.Declare ⟨callResultVar, callResultType⟩)), source ⟩, ⟨.Assign [⟨ .Local callResultVar, source⟩] seqCall, source⟩ ] - modify fun s => { s with prependedStmts := argsPrepends ++ liftedCall ++ startingPrepend} + prependList liftedCall + prependList (seqArgsAndPrepends.map (fun t => t.1)).flatten return ⟨.Var (.Local callResultVar), source⟩ | .IfThenElse cond thenBranch elseBranch => @@ -335,7 +333,7 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do let needsCondVar := condType.val != .TVoid -- Lift the entire if-then-else. Introduce a fresh variable for the result. - let condVar ← freshCondVar + let condVar ← freshTempVar -- Save outer state let savedSubst := (← get).subst let savedPrepends ← takePrepends From 6767211343a396c572ac3c88c5f00b43e35d909c Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 12:21:17 +0000 Subject: [PATCH 21/42] Fix tests --- .../Languages/Laurel/ConstrainedTypeElimTest.lean | 14 +++++++------- .../Languages/Laurel/DivisionByZeroCheckTest.lean | 6 +++--- .../Examples/Fundamentals/T16_PropertySummary.lean | 4 ++-- .../Examples/Fundamentals/T3_ControlFlowError.lean | 2 +- .../Examples/Fundamentals/T5_ProcedureCalls.lean | 1 + 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean index 844b2b600b..ad53d1c65b 100644 --- a/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean +++ b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean @@ -29,7 +29,7 @@ private def printElim (program : StrataDDM.Program) : IO Unit := do IO.println (toString (Std.Format.pretty (Std.ToFormat.format proc))) /-- -info: function nat$constraint(x: int): bool +info: procedure nat$constraint(x: int): bool { x >= 0 }; @@ -65,7 +65,7 @@ procedure test(n: nat) returns (r: nat) opaque { -- Scope management: constrained variable in if-branch must not leak into sibling block /-- -info: function pos$constraint(v: int): bool +info: procedure pos$constraint(v: int): bool { v > 0 }; @@ -73,10 +73,10 @@ procedure test(b: bool) opaque { if b - then { - var x: int := 1; - assert pos$constraint(x) - }; + then { + var x: int := 1; + assert pos$constraint(x) + }; { var x: int := -5; x := -10 @@ -108,7 +108,7 @@ procedure test(b: bool) opaque { -- Uninitialized constrained variable: havoc + assume constraint. -- The variable has no known value, only the type constraint is assumed. /-- -info: function posint$constraint(x: int): bool +info: procedure posint$constraint(x: int): bool { x > 0 }; diff --git a/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean b/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean index 8bacf4934d..ec01280352 100644 --- a/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean +++ b/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean @@ -30,7 +30,7 @@ procedure safeDivision() assert z == 5 }; -function pureDiv(x: int, y: int): int +procedure pureDiv(x: int, y: int): int requires y != 0 { return x / y @@ -63,10 +63,10 @@ procedure unsafeDivision(x: int) #eval testLaurel <| #strata program Laurel; -function pureDiv(x: int, y: int): int +procedure pureDiv(x: int, y: int): int requires y != 0 { - x / y + return x / y }; procedure callPureDivUnsafe(x: int) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean index 9e063f036d..3ded6c682b 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean @@ -18,7 +18,7 @@ procedure divide(x: int, y: int) returns (result: int) opaque { assert y == 0 summary "divisor is zero"; -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: divisor is zero does not hold +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: divisor is zero could not be proved return x }; @@ -26,6 +26,6 @@ procedure checkPositive(n: int) returns (ok: bool) opaque { var x: int := divide(3, 0) -//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: divisor is non-zero does not hold +//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: divisor is non-zero could not be proved }; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean index 969f65a1ed..8ee15c8915 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean @@ -21,7 +21,7 @@ procedure localVariableWithoutInitializer(): int { procedure deadCodeAfterIfElse(x: int) returns (r: int) { if x > 0 then { return 1 } else { return 2 }; -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: if-then-else only supported as the last statement in a block +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: in a transparent body, if-then-else is only supported as the last statement in a block return 3 }; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean index eee589ca30..c771af3c29 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean @@ -23,6 +23,7 @@ procedure fooReassign(): int }; procedure fooSingleAssign(): int + opaque // required because we don't yet support let variables in transparent bodies { var x: int := 0; var x2: int := x + 1; From 2df41be2348eb5d773d348521d1ba258780b96d3 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 12:26:07 +0000 Subject: [PATCH 22/42] Add extra test to T2_ImpureExpressions --- .../Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index ed15973dc4..8bcd260d28 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -162,4 +162,11 @@ opaque { var y: int := { assert x == 0; x := 1; x }; assert y == 1 }; + +procedure assignmentInExpressionAfterImperativeProcCall() +opaque { + var x: int := 0; + var y: int := imperativeProc(x) + (x := 2); + assert y == 3 +}; #end From 3459035150edc3d891428115b6f6790df943b86f Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 12:34:04 +0000 Subject: [PATCH 23/42] update test --- .../Examples/Fundamentals/T2_ImpureExpressions.lean | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index 8bcd260d28..1c9cecfdb2 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -163,10 +163,15 @@ opaque { assert y == 1 }; -procedure assignmentInExpressionAfterImperativeProcCall() +procedure transparentProc(x: int) returns (r: int) +{ + return x + 1 +}; + +procedure assignmentInExpressionAfterProcCall() opaque { var x: int := 0; - var y: int := imperativeProc(x) + (x := 2); + var y: int := transparentProc(x) + (x := 2); assert y == 3 }; #end From 5c87a7f11d74641cc6560dff7ca6d55ec9dc4962 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 12:42:55 +0000 Subject: [PATCH 24/42] Drop info output in tests --- StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean | 3 +++ .../Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T12_Operators.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T13_WhileLoops.lean | 1 + .../Laurel/Examples/Fundamentals/T13_WhileLoopsError.lean | 3 +++ .../Laurel/Examples/Fundamentals/T14_Quantifiers.lean | 1 + .../Laurel/Examples/Fundamentals/T15_ShortCircuit.lean | 1 + .../Laurel/Examples/Fundamentals/T16_PropertySummary.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T17_ForLoop.lean | 1 + .../Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean | 1 + .../Laurel/Examples/Fundamentals/T19_BitvectorTypes.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean | 2 ++ .../Laurel/Examples/Fundamentals/T20_TransparentBody.lean | 1 + .../Laurel/Examples/Fundamentals/T21_ExitMultiPathAssert.lean | 1 + .../Laurel/Examples/Fundamentals/T22_MultipleReturns.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean | 1 + .../Laurel/Examples/Fundamentals/T23b_IncrDecrField.lean | 1 + .../Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean | 1 + .../Languages/Laurel/Examples/Fundamentals/T4b_Exit.lean | 1 + .../Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean | 1 + .../Laurel/Examples/Fundamentals/T6_Preconditions.lean | 1 + .../Laurel/Examples/Fundamentals/T8_Postconditions.lean | 1 + .../Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean | 2 ++ .../Laurel/Examples/Fundamentals/T8c_BodilessInlining.lean | 1 + .../Laurel/Examples/Objects/T10_CompositeBvField.lean | 1 + .../Languages/Laurel/Examples/Objects/T1_MutableFields.lean | 1 + .../Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean | 2 ++ .../Languages/Laurel/Examples/Objects/T2_ModifiesClauses.lean | 1 + .../Languages/Laurel/Examples/Objects/T5_inheritance.lean | 1 + StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean | 1 + .../Laurel/Examples/Objects/T7_InstanceProcedures.lean | 1 + .../Laurel/Examples/Objects/T8_NonCompositeModifies.lean | 1 + .../Languages/Laurel/Examples/PrimitiveTypes/T1_Decimals.lean | 1 + .../Languages/Laurel/Examples/PrimitiveTypes/T2_String.lean | 1 + .../Laurel/Examples/PrimitiveTypes/T2_StringConcatLifting.lean | 1 + StrataTest/Languages/Laurel/IncrDecrTypeRejectionTest.lean | 2 ++ StrataTest/Languages/Laurel/StrataExpectSmokeTest.lean | 2 ++ 39 files changed, 48 insertions(+) diff --git a/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean b/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean index ec01280352..aa224bf569 100644 --- a/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean +++ b/StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean @@ -18,6 +18,7 @@ generates verification conditions for these preconditions. /-! ### Safe paths verify cleanly -/ +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; @@ -47,6 +48,7 @@ procedure callPureDivSafe() /-! ### Unsafe division: divisor not constrained, fails verification -/ -- Error ranges are too wide because Core does not use expression locations. +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -60,6 +62,7 @@ procedure unsafeDivision(x: int) /-! ### Unsafe call to function with `requires y != 0` -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean index c33b168bb0..82d2c77c60 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T12_Operators.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T12_Operators.lean index bdfb797d68..de110a7973 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T12_Operators.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T12_Operators.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoops.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoops.lean index 0d7b193c9f..be8c930189 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoops.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoops.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoopsError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoopsError.lean index e55297f647..d660bebe8e 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoopsError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T13_WhileLoopsError.lean @@ -15,6 +15,7 @@ These negative tests pin each failing loop invariant's diagnostic to that invariant's own source range (per-invariant source ranges threaded through loop elimination), rather than the whole loop. -/ +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; @@ -31,6 +32,7 @@ procedure badInitialInvariant() }; #end +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; @@ -50,6 +52,7 @@ procedure secondInvariantFails() }; #end +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean index c1a8d9cf6e..cfa2df1935 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T14_Quantifiers.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean index 2f70fc19c8..0b43e4f267 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean index 3ded6c682b..05b49587de 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T16_PropertySummary.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T17_ForLoop.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T17_ForLoop.lean index bb4358e997..0685370325 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T17_ForLoop.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T17_ForLoop.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean index 9a97632738..5c845fc31e 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean @@ -14,6 +14,7 @@ A recursive function over a recursive datatype. The `isRecursive` flag should be inferred automatically from the self-call. -/ +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_BitvectorTypes.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_BitvectorTypes.lean index 11b55b9dcf..73c6017c87 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_BitvectorTypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_BitvectorTypes.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean index 4792514b07..e3f9ad577c 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel (options := { verifyOptions := { Core.VerifyOptions.quiet with solver := "z3" } }) #strata diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean index c509426b73..18fa27d0a6 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T1_AssertFalse.lean @@ -11,6 +11,7 @@ open Strata /-! ## Failing asserts -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -27,6 +28,7 @@ procedure foo() /-! ## Assume false makes assert false trivially provable -/ +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean index 1b3ae3aa75..1a8db064e8 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T21_ExitMultiPathAssert.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T21_ExitMultiPathAssert.lean index 855f23eb43..a4041dbec4 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T21_ExitMultiPathAssert.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T21_ExitMultiPathAssert.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_MultipleReturns.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_MultipleReturns.lean index 3897cae872..5fac9f2338 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_MultipleReturns.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_MultipleReturns.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean index 431cb8d041..e77ba754de 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean @@ -32,6 +32,7 @@ parameterization which interacts poorly with counterexample search for the failing tests in this file). -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T23b_IncrDecrField.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T23b_IncrDecrField.lean index eaf689dd22..2ebe48d0ec 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T23b_IncrDecrField.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T23b_IncrDecrField.lean @@ -36,6 +36,7 @@ The parentheses around `(c#n)` are needed in the surface syntax because (90); `c#n++` parses ambiguously without them. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index 1c9cecfdb2..56798f2b5d 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean index e077bb7260..b6a5adb9f3 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T4b_Exit.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T4b_Exit.lean index 50d7e25c96..ef6d5f8e29 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T4b_Exit.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T4b_Exit.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean index c771af3c29..f58e031fb5 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean index f210707aa3..99b8bc086e 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T6_Preconditions.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean index 08e32bff17..877dc94c9f 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean index 66a71def1c..7e2ab91e17 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean @@ -11,6 +11,7 @@ open Strata /-! ## Correct early return -/ +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; @@ -27,6 +28,7 @@ procedure earlyReturnCorrect(x: int) returns (r: int) /-! ## Buggy early return: postcondition fails -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8c_BodilessInlining.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8c_BodilessInlining.lean index d3fba57772..93ed0ca88a 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8c_BodilessInlining.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8c_BodilessInlining.lean @@ -17,6 +17,7 @@ body, so inlining would make everything after the call trivially provable. Now the body assumes the postconditions instead, so `assert false` after the inlined call is correctly rejected. -/ +#guard_msgs (drop info) in #eval testLaurel (options := { translateOptions := { inlineFunctionsWhenPossible := true } }) <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean b/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean index d60053efef..c0e0d40289 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean @@ -14,6 +14,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean b/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean index 2df0e6c486..a2b1333a16 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean b/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean index f699562724..0bfcac23cd 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean @@ -11,6 +11,7 @@ open Strata /-! ## Correct heap mutating value return -/ +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; @@ -30,6 +31,7 @@ procedure setAndReturn(c: Container, x: int) returns (r: int) /-! ## Buggy: postcondition r == x + 1 cannot hold when r := x -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T2_ModifiesClauses.lean b/StrataTest/Languages/Laurel/Examples/Objects/T2_ModifiesClauses.lean index ed9bfc9301..fd7f32dd0b 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T2_ModifiesClauses.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T2_ModifiesClauses.lean @@ -19,6 +19,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean index 3899f9e3a1..80b1d97207 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean b/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean index 99191d08c0..a0bc1e12e0 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean b/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean index a605006ec5..80c45a0b12 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T8_NonCompositeModifies.lean b/StrataTest/Languages/Laurel/Examples/Objects/T8_NonCompositeModifies.lean index 87a6e2424d..3c491e1c73 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T8_NonCompositeModifies.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T8_NonCompositeModifies.lean @@ -16,6 +16,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T1_Decimals.lean b/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T1_Decimals.lean index 0a1280653c..b574c79b31 100644 --- a/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T1_Decimals.lean +++ b/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T1_Decimals.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_String.lean b/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_String.lean index 0528672ee8..0a718b7b92 100644 --- a/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_String.lean +++ b/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_String.lean @@ -10,6 +10,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_StringConcatLifting.lean b/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_StringConcatLifting.lean index cc3756bed8..c223b3e3bd 100644 --- a/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_StringConcatLifting.lean +++ b/StrataTest/Languages/Laurel/Examples/PrimitiveTypes/T2_StringConcatLifting.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/IncrDecrTypeRejectionTest.lean b/StrataTest/Languages/Laurel/IncrDecrTypeRejectionTest.lean index 33c166b4c9..ad6cda63b6 100644 --- a/StrataTest/Languages/Laurel/IncrDecrTypeRejectionTest.lean +++ b/StrataTest/Languages/Laurel/IncrDecrTypeRejectionTest.lean @@ -21,6 +21,7 @@ open Strata /-! ## Rejected: `++`/`--` on unsupported element types -/ +#guard_msgs (drop info) in #eval testLaurelResolution <| #strata program Laurel; @@ -43,6 +44,7 @@ procedure incrFloat(g: float64) opaque { /-! ## Accepted: `++`/`--` on an int-based constrained type (e.g. `nat`) -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; diff --git a/StrataTest/Languages/Laurel/StrataExpectSmokeTest.lean b/StrataTest/Languages/Laurel/StrataExpectSmokeTest.lean index ea6ba94d08..e12c9b603b 100644 --- a/StrataTest/Languages/Laurel/StrataExpectSmokeTest.lean +++ b/StrataTest/Languages/Laurel/StrataExpectSmokeTest.lean @@ -16,6 +16,7 @@ open Strata /-! ## Positive smoke test -/ +#guard_msgs (drop info) in #eval testLaurel #strata program Laurel; @@ -37,6 +38,7 @@ procedure foo() opaque { /-! ## Negative smoke test: a verifier-level diagnostic. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; From 6c78979ea39d99f50841c250773806070d04c28f Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 12:53:16 +0000 Subject: [PATCH 25/42] Fixes --- .../Fundamentals/T2_ImpureExpressionsError2.lean | 7 +++++-- .../Examples/Fundamentals/T3_ControlFlowError3.lean | 6 +++++- StrataTest/Languages/Laurel/MapStmtExprTest.lean | 12 +++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean index 13df2b4fe4..8e4cda0262 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean @@ -5,11 +5,12 @@ -/ module -public import StrataDDM.AST -import StrataTest.Util.TestLaurel +meta import StrataTest.Util.TestLaurel open StrataTest.Util +meta section + #eval testLaurel <| #strata program Laurel; @@ -22,3 +23,5 @@ procedure transparentWithWhile(x: int): int }; #end + +end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean index 6c0094d92b..f2f49ab046 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean @@ -5,11 +5,13 @@ -/ module -import StrataTest.Util.TestLaurel +meta import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +meta section + #eval testLaurel <| #strata program Laurel; @@ -30,3 +32,5 @@ procedure callLetExpressionsInTransparent() opaque { }; #end + +end diff --git a/StrataTest/Languages/Laurel/MapStmtExprTest.lean b/StrataTest/Languages/Laurel/MapStmtExprTest.lean index aac62f379d..32656be094 100644 --- a/StrataTest/Languages/Laurel/MapStmtExprTest.lean +++ b/StrataTest/Languages/Laurel/MapStmtExprTest.lean @@ -10,15 +10,19 @@ is the identity: applying it to a parsed program produces identical output. -/ module -import StrataTest.Util.TestLaurel -import Strata.Languages.Laurel.MapStmtExpr -import Strata.Languages.Laurel.Resolution +meta import StrataTest.Util.TestLaurel +meta import Strata.Languages.Laurel.MapStmtExpr +meta import Strata.Languages.Laurel.Resolution +meta import Strata.Languages.Laurel.Grammar +meta import StrataDDM.Integration.Lean.HashCommands open Strata open StrataTest.Util namespace Strata.Laurel +meta section + private def parseAndResolve (program : StrataDDM.Program) : IO Program := do let laurelProgram ← translateLaurel program pure (resolve laurelProgram).program @@ -76,4 +80,6 @@ procedure test(x: int, b: bool) returns (r: int) }; #end +end + end Strata.Laurel From e98598f77edea1c0047bf9150efcecfba3fd6083 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 13:23:35 +0000 Subject: [PATCH 26/42] Add test-case to T2 --- .../Examples/Fundamentals/T2_ImpureExpressions.lean | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index 56798f2b5d..ff87c164ab 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -175,4 +175,12 @@ opaque { var y: int := transparentProc(x) + (x := 2); assert y == 3 }; + +procedure liftInsideAssignmentInExpression() +opaque { + var x: int := 0; + var y: int := (x := 1 + transparentProc(x)); + assert y == 3 +}; + #end From 1c78fe16b2e2d86fb2d4748eb619d42dc2c30ec0 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 13:27:56 +0000 Subject: [PATCH 27/42] Fix to lift pass --- .../Laurel/LiftImperativeExpressions.lean | 39 ++++++++----------- .../Fundamentals/T2_ImpureExpressions.lean | 2 +- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index 3ecb095021..69845bc198 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -195,26 +195,6 @@ def containsAssignmentOrImperativeCall (imperativeCallees : List String) (expr : all_goals (try term_by_mem) all_goals omega -/-- -Shared logic for lifting an assignment in expression position: -prepends the assignment, creates before-snapshots for all targets, -and updates substitutions. The value should already be transformed by the caller. --/ -private def liftAssignExpr (targets : List VariableMd) (seqValue : StmtExprMd) - (source : Option FileRange) : LiftM Unit := do - -- Prepend the assignment itself - prepend (⟨.Assign targets seqValue, source⟩) - -- Create a before-snapshot for each target and update substitutions - for target in targets do - match target.val with - | .Local varName => - let snapshotName ← freshTempFor varName - let varType ← computeType ⟨ .Var (.Local varName), source ⟩ - -- Snapshot goes before the assignment (cons pushes to front) - prepend (⟨.Assign [⟨.Declare ⟨snapshotName, varType⟩, source⟩] (⟨.Var (.Local varName), source⟩), source⟩) - setSubst varName snapshotName - | _ => pure () - mutual @@ -271,9 +251,22 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do dbg_trace "Strata bug: non-identifier targets should have been removed before the lift expression phase"; return expr - -- Use the original value (not seqValue) for the prepended assignment, - -- because prepended statements execute in program order and don't need substitutions. - liftAssignExpr targets value source + let (valuePrepends, newValue) ← transformLiftedExpr value + + prepend (⟨.Assign targets newValue, source⟩) + + -- Create a before-snapshot for each target and update substitutions + for target in targets do + match target.val with + | .Local varName => + let snapshotName ← freshTempFor varName + let varType ← computeType ⟨ .Var (.Local varName), source ⟩ + -- Snapshot goes before the assignment (cons pushes to front) + prepend (⟨.Assign [⟨.Declare ⟨snapshotName, varType⟩, source⟩] (⟨.Var (.Local varName), source⟩), source⟩) + setSubst varName snapshotName + | _ => pure () + + prependList valuePrepends return resultExpr diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index ff87c164ab..30b98d7bec 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -179,7 +179,7 @@ opaque { procedure liftInsideAssignmentInExpression() opaque { var x: int := 0; - var y: int := (x := 1 + transparentProc(x)); + var y: int := ((x := 1) + transparentProc(x)); assert y == 3 }; From bcdef57f7eaaed0a7ffcd3b1851c0e488d040c3d Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 13:40:51 +0000 Subject: [PATCH 28/42] Add local variable inlining pass --- .../Laurel/InlineLocalVariables.lean | 228 ++++++++++++++++++ .../Laurel/LaurelCompilationPipeline.lean | 19 +- .../Examples/Fundamentals/T3_ControlFlow.lean | 12 + .../Fundamentals/T3_ControlFlowError3.lean | 36 --- .../Languages/Laurel/TmpInlineCheck.lean | 29 +++ 5 files changed, 284 insertions(+), 40 deletions(-) create mode 100644 Strata/Languages/Laurel/InlineLocalVariables.lean delete mode 100644 StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean create mode 100644 StrataTest/Languages/Laurel/TmpInlineCheck.lean diff --git a/Strata/Languages/Laurel/InlineLocalVariables.lean b/Strata/Languages/Laurel/InlineLocalVariables.lean new file mode 100644 index 0000000000..68b2e855e0 --- /dev/null +++ b/Strata/Languages/Laurel/InlineLocalVariables.lean @@ -0,0 +1,228 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +public import Strata.Languages.Laurel.LaurelPass +public import Strata.Languages.Laurel.UnorderedCore +import Strata.Languages.Laurel.LaurelAST +import Strata.Languages.Laurel.TransparencyPass + +/-! +## Inline Local Variables Pass + +Runs after the transparency pass and operates **only on functions** (the +`$asFunction` copies produced by the transparency pass). Function bodies are +pure expressions and cannot contain local variable declarations once they reach +the Core schema translation. This pass eliminates them by inlining. + +For each local variable of the form `var := `, every reference to +`` that occurs after the declaration is replaced with ``, and the +declaration itself is dropped. The inlined expression is itself inlined first, +so chains like + +``` +var a := 1; +var b := a + 1; +``` + +become `b ↦ 1 + 1`. + +Because functions are pure, an inlined variable is never reassigned. Any +assignment to such a `` is therefore a user error and emits a diagnostic. +-/ + +namespace Strata.Laurel + +/-- Substitution from an inlined local variable name to the expression it was + initialized with. Newer bindings are pushed to the front so `List.lookup` + finds the most recent declaration first. -/ +private abbrev InlineSubst := List (Identifier × StmtExprMd) + +/-- Diagnostics are accumulated in the state. -/ +private abbrev InlineM := StateM (Array DiagnosticModel) + +private def emitDiag (d : DiagnosticModel) : InlineM Unit := + modify (·.push d) + +public section + +mutual + +/-- +Inline local variables within an expression, given the substitution in scope. + +Only `Block` nodes introduce new (sequential) scope, so the substitution is not +threaded back out of `inlineExpr`; child expressions simply inherit `subst`. +-/ +private partial def inlineExpr (subst : InlineSubst) (expr : StmtExprMd) : InlineM StmtExprMd := do + let source := expr.source + match expr.val with + | .Var (.Local name) => + match subst.lookup name with + | some replacement => return replacement + | none => return expr + | .Var (.Field target fieldName) => + return ⟨.Var (.Field (← inlineExpr subst target) fieldName), source⟩ + | .Var (.Declare _) => return expr + + | .Block stmts label => + let stmts' ← inlineBlockStmts subst stmts + return ⟨.Block stmts' label, source⟩ + + | .Assign targets value => + -- An assignment to an inlined local is contradictory: report it. + for t in targets do + match t.val with + | .Local name => + if (subst.lookup name).isSome then + emitDiag (diagnosticFromSource (t.source.orElse fun _ => source) + s!"cannot assign to '{name.text}': it is an inlined local variable") + | _ => pure () + let targets' ← targets.mapM (inlineVariable subst) + let value' ← inlineExpr subst value + return ⟨.Assign targets' value', source⟩ + + | .IfThenElse cond th el => + let cond' ← inlineExpr subst cond + let th' ← inlineExpr subst th + let el' ← el.mapM (inlineExpr subst) + return ⟨.IfThenElse cond' th' el', source⟩ + + | .While cond invs dec body => + let cond' ← inlineExpr subst cond + let invs' ← invs.mapM (inlineExpr subst) + let dec' ← dec.mapM (inlineExpr subst) + let body' ← inlineExpr subst body + return ⟨.While cond' invs' dec' body', source⟩ + + | .Return value => + return ⟨.Return (← value.mapM (inlineExpr subst)), source⟩ + + | .PrimitiveOp op args skipProof => + return ⟨.PrimitiveOp op (← args.mapM (inlineExpr subst)) skipProof, source⟩ + + | .StaticCall callee args => + return ⟨.StaticCall callee (← args.mapM (inlineExpr subst)), source⟩ + + | .InstanceCall target callee args => + let target' ← inlineExpr subst target + let args' ← args.mapM (inlineExpr subst) + return ⟨.InstanceCall target' callee args', source⟩ + + | .PureFieldUpdate target fieldName newValue => + let target' ← inlineExpr subst target + let newValue' ← inlineExpr subst newValue + return ⟨.PureFieldUpdate target' fieldName newValue', source⟩ + + | .ReferenceEquals lhs rhs => + return ⟨.ReferenceEquals (← inlineExpr subst lhs) (← inlineExpr subst rhs), source⟩ + + | .AsType target ty => + return ⟨.AsType (← inlineExpr subst target) ty, source⟩ + + | .IsType target ty => + return ⟨.IsType (← inlineExpr subst target) ty, source⟩ + + | .Quantifier mode param trigger body => + -- The bound variable shadows any inlined local of the same name. + let innerSubst := subst.filter (·.1 != param.name) + let trigger' ← trigger.mapM (inlineExpr innerSubst) + let body' ← inlineExpr innerSubst body + return ⟨.Quantifier mode param trigger' body', source⟩ + + | .Assigned name => + return ⟨.Assigned (← inlineExpr subst name), source⟩ + + | .Old value => + return ⟨.Old (← inlineExpr subst value), source⟩ + + | .Fresh value => + return ⟨.Fresh (← inlineExpr subst value), source⟩ + + | .Assert cond => + return ⟨.Assert { cond with condition := ← inlineExpr subst cond.condition }, source⟩ + + | .Assume cond => + return ⟨.Assume (← inlineExpr subst cond), source⟩ + + | .ProveBy value proof => + return ⟨.ProveBy (← inlineExpr subst value) (← inlineExpr subst proof), source⟩ + + | .ContractOf ty func => + return ⟨.ContractOf ty (← inlineExpr subst func), source⟩ + + | .IncrDecr mode op target => + return ⟨.IncrDecr mode op (← inlineVariable subst target), source⟩ + + -- Leaves: nothing to inline. + | .Exit _ | .LiteralInt _ | .LiteralBool _ | .LiteralString _ | .LiteralDecimal _ + | .LiteralBv _ _ | .New _ | .This | .Abstract | .All | .Hole .. => return expr + +/-- Inline within the target expression of a field variable; other variable + forms have no sub-expressions to rewrite. -/ +private partial def inlineVariable (subst : InlineSubst) (v : VariableMd) : InlineM VariableMd := do + match v.val with + | .Field target fieldName => + return ⟨.Field (← inlineExpr subst target) fieldName, v.source⟩ + | _ => return v + +/-- +Process the statements of a block sequentially, threading the substitution. + +A `var := ` declaration (`Assign` with a single `Declare` target) +is removed, and `` is bound to the inlined `` for the remaining +statements. All other statements are inlined under the current substitution. +-/ +private partial def inlineBlockStmts (subst : InlineSubst) (stmts : List StmtExprMd) : InlineM (List StmtExprMd) := do + match stmts with + | [] => return [] + | stmt :: rest => + match stmt.val with + | .Assign [⟨.Declare param, _⟩] value => + let value' ← inlineExpr subst value + inlineBlockStmts ((param.name, value') :: subst) rest + | _ => + let stmt' ← inlineExpr subst stmt + let rest' ← inlineBlockStmts subst rest + return stmt' :: rest' + +end + +/-- Inline local variables in a single function, returning the rewritten + procedure and any diagnostics. -/ +def inlineLocalVariablesInFunction (proc : Procedure) : Procedure × Array DiagnosticModel := + match proc.body with + | .Transparent body => + let (body', diags) := (inlineExpr [] body).run #[] + ({ proc with body := .Transparent body' }, diags) + | .Opaque postconds impl modif => + match impl with + | some i => + let (i', diags) := (inlineExpr [] i).run #[] + ({ proc with body := .Opaque postconds (some i') modif }, diags) + | none => (proc, #[]) + | _ => (proc, #[]) + +/-- Inline local variables in every function of an `UnorderedCoreWithLaurelTypes`. + Only `functions` are transformed; `coreProcedures` are left unchanged. -/ +def inlineLocalVariablesInFunctions (uc : UnorderedCoreWithLaurelTypes) + : UnorderedCoreWithLaurelTypes × List DiagnosticModel := + let results := uc.functions.map inlineLocalVariablesInFunction + let functions' := results.map (·.1) + let diags := results.flatMap (·.2.toList) + ({ uc with functions := functions' }, diags) + +public def inlineLocalVariablesPass : LaurelPass UnorderedCoreWithLaurelTypes UnorderedCoreWithLaurelTypes where + name := "InlineLocalVariablesPass" + documentation := "Inlines local variable declarations of the form `var := ` in function bodies. References to the variable after its declaration are replaced with the initializer expression, and the declaration is removed. Assignments to an inlined variable emit a diagnostic. Operates only on functions, which are pure and cannot carry local variable declarations into Core." + comesAfter := [⟨ transparencyPass.meta, "Inlining of local variables in functions only makes sense after the transparency pass has created the functions"⟩] + run := fun p _ _ => + let (uc, diags) := inlineLocalVariablesInFunctions p + (uc, diags, {}) + +end -- public section + +end Strata.Laurel diff --git a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean index fb65ec7d03..c8d651c411 100644 --- a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean +++ b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean @@ -20,6 +20,7 @@ import Strata.Languages.Laurel.CoreDefinitionsForLaurel import Strata.Languages.Laurel.CoreGroupingAndOrdering import Strata.Languages.Laurel.TransparencyPass import Strata.Languages.Laurel.LiftImperativeExpressions +import Strata.Languages.Laurel.InlineLocalVariables import Strata.Languages.Laurel.ConstrainedTypeElim import Strata.Languages.Laurel.ContractPass import Strata.Languages.Laurel.TypeAliasElim @@ -166,7 +167,8 @@ private def runLaurelPasses /-- The ordered sequence of passes on the unordered Core representation. -/ private def unorderedCorePipeline : Array (LaurelPass UnorderedCoreWithLaurelTypes UnorderedCoreWithLaurelTypes) := #[ - liftImperativeExpressionsPass + liftImperativeExpressionsPass, + inlineLocalVariablesPass ] /-- All pipeline passes, projected to their parameter-free metadata. Combines @@ -200,9 +202,12 @@ def translateWithLaurel (options : LaurelTranslateOptions) (program : Program) emit "transparencyPass" "core.st" unorderedCore let mut unorderedCore := unorderedCore let mut fnModel := model + let mut ucDiags : List DiagnosticModel := [] for pass in unorderedCorePipeline do - unorderedCore := (pass.run unorderedCore fnModel options).1 + let (uc, passPassDiags, _) := pass.run unorderedCore fnModel options + unorderedCore := uc + ucDiags := ucDiags ++ passPassDiags if pass.needsResolves then let compositeTypes := program.types.filter (fun t => match t with | .Composite _ => true | _ => false) let (uc', m', errors) := resolveUnorderedCore unorderedCore (some fnModel) compositeTypes @@ -211,16 +216,22 @@ def translateWithLaurel (options : LaurelTranslateOptions) (program : Program) { d with message := s!"Internal error: resolution after '{pass.name}' introduced this diagnostic: {d.message}" } emit pass.name "unorderedCoreWithLaurelTypes.st" unorderedCore - return (none, passDiags ++ newDiags, program, stats) + return (none, passDiags ++ ucDiags ++ newDiags, program, stats) unorderedCore := uc' fnModel := m' emit pass.name "unorderedCoreWithLaurelTypes.st" unorderedCore + -- An error introduced by an unordered-core pass (e.g. an assignment to an + -- inlined local) prevents producing a Core program, just like Laurel pass + -- errors above. + if ucDiags.any (·.type != .Warning) then + return (none, passDiags ++ ucDiags, program, stats) + let coreWithLaurelTypes := (orderingPass.run unorderedCore model options).1 emit "CoreWithLaurelTypes" "core.st" coreWithLaurelTypes let (coreProgram, coreDiagnostics, _) := laurelToCoreSchemaPass.run coreWithLaurelTypes fnModel options - let mut allDiagnostics: List DiagnosticModel := passDiags ++ coreDiagnostics; + let mut allDiagnostics: List DiagnosticModel := passDiags ++ ucDiags ++ coreDiagnostics; emit "Core" "core.st" coreProgram let coreProgramOption := diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean index b6a5adb9f3..7e4390fecc 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean @@ -14,6 +14,18 @@ open Strata #strata program Laurel; +procedure letExpressionsInTransparent() returns (r: int) { + var x: int := 0; + var y: int := x + 1; + var z: int := y + 1; + return z +}; + +procedure callLetExpressionsInTransparent() opaque { + var x: int := letExpressionsInTransparent(); + assert x == 2 +}; + procedure assertAndAssumeInTransparent(a: int) returns (r: int) { assert 2 == 3; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean deleted file mode 100644 index f2f49ab046..0000000000 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError3.lean +++ /dev/null @@ -1,36 +0,0 @@ -/- - Copyright Strata Contributors - - SPDX-License-Identifier: Apache-2.0 OR MIT --/ -module - -meta import StrataTest.Util.TestLaurel - -open StrataTest.Util -open Strata - -meta section - -#eval testLaurel <| -#strata -program Laurel; - -procedure letExpressionsInTransparent() returns (r: int) { - var x: int := 0; -//^^^^^^^^^^^^^^^ error: local variables in transparent bodies are not YET supported - var y: int := x + 1; -//^^^^^^^^^^^^^^^^^^^ error: local variables in transparent bodies are not YET supported - var z: int := y + 1; -//^^^^^^^^^^^^^^^^^^^ error: local variables in transparent bodies are not YET supported - return z -}; - -procedure callLetExpressionsInTransparent() opaque { - var x: int := letExpressionsInTransparent(); - assert x == 2 -}; - -#end - -end diff --git a/StrataTest/Languages/Laurel/TmpInlineCheck.lean b/StrataTest/Languages/Laurel/TmpInlineCheck.lean new file mode 100644 index 0000000000..9f021820d3 --- /dev/null +++ b/StrataTest/Languages/Laurel/TmpInlineCheck.lean @@ -0,0 +1,29 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +import StrataTest.Util.TestLaurel + +open StrataTest.Util +open Strata + +-- A transparent function with a local variable. Without the inlining pass, +-- the `var x := 3` would reach the Core schema translation and be rejected. +#guard_msgs (drop info) in +#eval testLaurel <| +#strata +program Laurel; +procedure foo(): int +{ + var x: int := 3; + var y: int := x + 1; + return y + x +}; + +procedure caller() opaque { + var r: int := foo(); + assert r == 7 +}; +#end From 6f7b93df831f54d0e6ab1da1a0c298a64710d8b4 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 13:42:09 +0000 Subject: [PATCH 29/42] Remove tmp file --- .../Languages/Laurel/TmpInlineCheck.lean | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 StrataTest/Languages/Laurel/TmpInlineCheck.lean diff --git a/StrataTest/Languages/Laurel/TmpInlineCheck.lean b/StrataTest/Languages/Laurel/TmpInlineCheck.lean deleted file mode 100644 index 9f021820d3..0000000000 --- a/StrataTest/Languages/Laurel/TmpInlineCheck.lean +++ /dev/null @@ -1,29 +0,0 @@ -/- - Copyright Strata Contributors - - SPDX-License-Identifier: Apache-2.0 OR MIT --/ - -import StrataTest.Util.TestLaurel - -open StrataTest.Util -open Strata - --- A transparent function with a local variable. Without the inlining pass, --- the `var x := 3` would reach the Core schema translation and be rejected. -#guard_msgs (drop info) in -#eval testLaurel <| -#strata -program Laurel; -procedure foo(): int -{ - var x: int := 3; - var y: int := x + 1; - return y + x -}; - -procedure caller() opaque { - var r: int := foo(); - assert r == 7 -}; -#end From f82910a849a3229f42300326219590f7cd05e4e4 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 14:08:15 +0000 Subject: [PATCH 30/42] Remove more info output --- .../Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean | 1 - .../Laurel/Examples/Objects/T7_InstanceProcedures.lean | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean index f58e031fb5..be801af5cb 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean @@ -24,7 +24,6 @@ procedure fooReassign(): int }; procedure fooSingleAssign(): int - opaque // required because we don't yet support let variables in transparent bodies { var x: int := 0; var x2: int := x + 1; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean b/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean index 49bebead73..beec222d33 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T7_InstanceProcedures.lean @@ -50,6 +50,7 @@ procedure useCounter() Without per-composite scoping, `tick` would collide in the global scope during pre-registration. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -94,6 +95,7 @@ procedure runClock() /-! ## 3. Method with multiple parameters: `c#setTo(v)` -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -120,6 +122,7 @@ procedure useCell(x: int) /-! ## 4. Boolean-typed field updated through an instance method, and read back via field access in the caller's `assert`. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -148,6 +151,7 @@ procedure useWidget() only `a`; the unused `b` parameter is included to confirm method dispatch picks the right receiver. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -175,6 +179,7 @@ procedure resetTwoCounters(a: Counter, b: Counter) confirms an extra (unused) method parameter doesn't break call dispatch or framing. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -201,6 +206,7 @@ procedure useAccount() /-! ## 7. Instance method called through a field-selected receiver: `obj#field#method()`. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -226,6 +232,7 @@ procedure useOuter() /-! ## 8. Chained field read: `obj#field#x`. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; From 7ab0725dc7e94b1ee02e5893998b2675f4ee05b2 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 14:23:54 +0000 Subject: [PATCH 31/42] Update Python files --- .../StrataPython/PythonRuntimeLaurelPart.lean | 592 +++++++----------- StrataPython/StrataPython/Specs/ToLaurel.lean | 1 - 2 files changed, 238 insertions(+), 355 deletions(-) diff --git a/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean b/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean index c1352bfd71..1b10e6171b 100644 --- a/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean +++ b/StrataPython/StrataPython/PythonRuntimeLaurelPart.lean @@ -157,150 +157,127 @@ composite re_Match { } // re.Match methods — uninterpreted (capture groups are beyond SMT-LIB) -function re_Match_group (self : re_Match, n : int) : string; -function re_Match_start (self : re_Match, n : int) : int; -function re_Match_end (self : re_Match, n : int) : int; -function re_Match_span_start (self : re_Match, n : int) : int; -function re_Match_span_end (self : re_Match, n : int) : int; -function re_Match_lastindex (self : re_Match) : int; -function re_Match_lastgroup (self : re_Match) : string; -function re_Match_groups (self : re_Match) : ListStr; - -function re_pattern_error(pattern : string) : Error +procedure re_Match_group (self : re_Match, n : int) : string; +procedure re_Match_start (self : re_Match, n : int) : int; +procedure re_Match_end (self : re_Match, n : int) : int; +procedure re_Match_span_start (self : re_Match, n : int) : int; +procedure re_Match_span_end (self : re_Match, n : int) : int; +procedure re_Match_lastindex (self : re_Match) : int; +procedure re_Match_lastgroup (self : re_Match) : string; +procedure re_Match_groups (self : re_Match) : ListStr; + +procedure re_pattern_error(pattern : string) : Error external; // The _bool variants are also factory functions (not inlined here) so that // unsupported patterns leave an uninterpreted Bool UF rather than an // uninterpreted RegLan UF. An uninterpreted Bool UF produces `unknown` // gracefully; an uninterpreted RegLan UF causes cvc5 theory-combination errors. -function re_fullmatch_bool(pattern : string, s : string) : bool +procedure re_fullmatch_bool(pattern : string, s : string) : bool external; -function re_match_bool(pattern : string, s : string) : bool +procedure re_match_bool(pattern : string, s : string) : bool external; -function re_search_bool(pattern : string, s : string) : bool +procedure re_search_bool(pattern : string, s : string) : bool external; -function Str.InRegEx(s: string, r: Core regex): bool external; -function Str.Length(s: string): int external; +procedure Str.InRegEx(s: string, r: Core regex): bool external; +procedure Str.Length(s: string): int external; // ///////////////////////////////////////////////////////////////////////////////////// -function mk_re_Match(s : string) : Any { - from_ClassInstance("re_Match", +procedure mk_re_Match(s : string) : Any + return from_ClassInstance("re_Match", DictStrAny_cons("re_match_string", from_str(s), DictStrAny_cons("re_match_pos", from_int(0), DictStrAny_cons("re_match_endpos", from_int(Str.Length(s)), - DictStrAny_empty())))) -}; + DictStrAny_empty())))); // re.compile is a no-op: returns the pattern string unchanged. -function re_compile(pattern : Any) : Any +procedure re_compile(pattern : Any) : Any requires Any..isfrom_str(pattern) -{ - pattern -}; + return pattern; -function re_fullmatch(pattern : Any, s : Any) : Any +procedure re_fullmatch(pattern : Any, s : Any) : Any requires Any..isfrom_str(pattern) && Any..isfrom_str(s) -{ - if Error..isRePatternError(re_pattern_error(Any..as_string!(pattern))) - then exception(re_pattern_error(Any..as_string!(pattern))) - else if re_fullmatch_bool(Any..as_string!(pattern), Any..as_string!(s)) - then mk_re_Match(Any..as_string!(s)) - else from_None() -}; -function re_match(pattern : Any, s : Any) : Any + return if Error..isRePatternError(re_pattern_error(Any..as_string!(pattern))) + then exception(re_pattern_error(Any..as_string!(pattern))) + else if re_fullmatch_bool(Any..as_string!(pattern), Any..as_string!(s)) + then mk_re_Match(Any..as_string!(s)) + else from_None(); + +procedure re_match(pattern : Any, s : Any) : Any requires Any..isfrom_str(pattern) && Any..isfrom_str(s) -{ - if Error..isRePatternError(re_pattern_error(Any..as_string!(pattern))) - then exception(re_pattern_error(Any..as_string!(pattern))) - else if re_match_bool(Any..as_string!(pattern), Any..as_string!(s)) - then mk_re_Match(Any..as_string!(s)) - else from_None() -}; -function re_search(pattern : Any, s : Any) : Any + return if Error..isRePatternError(re_pattern_error(Any..as_string!(pattern))) + then exception(re_pattern_error(Any..as_string!(pattern))) + else if re_match_bool(Any..as_string!(pattern), Any..as_string!(s)) + then mk_re_Match(Any..as_string!(s)) + else from_None(); + +procedure re_search(pattern : Any, s : Any) : Any requires Any..isfrom_str(pattern) && Any..isfrom_str(s) -{ - if Error..isRePatternError(re_pattern_error(Any..as_string!(pattern))) - then exception(re_pattern_error(Any..as_string!(pattern))) - else if re_search_bool(Any..as_string!(pattern), Any..as_string!(s)) - then mk_re_Match(Any..as_string!(s)) - else from_None() -}; + return if Error..isRePatternError(re_pattern_error(Any..as_string!(pattern))) + then exception(re_pattern_error(Any..as_string!(pattern))) + else if re_search_bool(Any..as_string!(pattern), Any..as_string!(s)) + then mk_re_Match(Any..as_string!(s)) + else from_None(); // ///////////////////////////////////////////////////////////////////////////////////// //Functions that we provide to Python user //to write assertions/contracts about about types of variables -function isBool (v: Any) : Any { - from_bool (Any..isfrom_bool(v)) -}; +procedure isBool (v: Any) : Any + return from_bool (Any..isfrom_bool(v)); -function isInt (v: Any) : Any { - from_bool (Any..isfrom_int(v)) -}; +procedure isInt (v: Any) : Any + return from_bool (Any..isfrom_int(v)); -function isFloat (v: Any) : Any { - from_bool (Any..isfrom_float(v)) -}; +procedure isFloat (v: Any) : Any + return from_bool (Any..isfrom_float(v)); -function isString (v: Any) : Any { - from_bool (Any..isfrom_str(v)) -}; +procedure isString (v: Any) : Any + return from_bool (Any..isfrom_str(v)); -function isdatetime (v: Any) : Any { - from_bool (Any..isfrom_datetime(v)) -}; +procedure isdatetime (v: Any) : Any + return from_bool (Any..isfrom_datetime(v)); -function isDict (v: Any) : Any { - from_bool (Any..isfrom_DictStrAny(v)) -}; +procedure isDict (v: Any) : Any + return from_bool (Any..isfrom_DictStrAny(v)); -function isList (v: Any) : Any { - from_bool (Any..isfrom_ListAny(v)) -}; +procedure isList (v: Any) : Any + return from_bool (Any..isfrom_ListAny(v)); -function isClassInstance (v: Any) : Any { - from_bool (Any..isfrom_ClassInstance(v)) -}; +procedure isClassInstance (v: Any) : Any + return from_bool (Any..isfrom_ClassInstance(v)); -function isInstance_of_Int (v: Any) : Any { - from_bool (Any..isfrom_int(v) || Any..isfrom_bool(v)) -}; +procedure isInstance_of_Int (v: Any) : Any + return from_bool (Any..isfrom_int(v) || Any..isfrom_bool(v)); -function isInstance_of_Float (v: Any) : Any { - from_bool (Any..isfrom_float(v) || Any..isfrom_int(v) || Any..isfrom_bool(v)) -}; +procedure isInstance_of_Float (v: Any) : Any + return from_bool (Any..isfrom_float(v) || Any..isfrom_int(v) || Any..isfrom_bool(v)); // ///////////////////////////////////////////////////////////////////////////////////// //Functions that we provide to Python user //to write assertions/contracts about about types of errors // ///////////////////////////////////////////////////////////////////////////////////// -function isTypeError (e: Error) : Any { - from_bool (Error..isTypeError(e)) -}; +procedure isTypeError (e: Error) : Any +return from_bool (Error..isTypeError(e)); -function isAttributeError (e: Error) : Any { - from_bool (Error..isAttributeError(e)) -}; +procedure isAttributeError (e: Error) : Any +return from_bool (Error..isAttributeError(e)); -function isAssertionError (e: Error) : Any { - from_bool (Error..isAssertionError(e)) -}; +procedure isAssertionError (e: Error) : Any +return from_bool (Error..isAssertionError(e)); -function isUnimplementedError (e: Error) : Any { - from_bool (Error..isUnimplementedError(e)) -}; +procedure isUnimplementedError (e: Error) : Any +return from_bool (Error..isUnimplementedError(e)); -function isUndefinedError (e: Error) : Any { - from_bool (Error..isUndefinedError(e)) -}; +procedure isUndefinedError (e: Error) : Any +return from_bool (Error..isUndefinedError(e)); -function isError (e: Error) : bool { - ! Error..isNoError(e) -}; +procedure isError (e: Error) : bool +return ! Error..isNoError(e); // ///////////////////////////////////////////////////////////////////////////////////// //The following function convert Any type to bool @@ -308,197 +285,152 @@ function isError (e: Error) : bool { // https://docs.python.org/3/library/stdtypes.html // ///////////////////////////////////////////////////////////////////////////////////// -function Any_to_bool (v: Any) : bool -{ - if (Any..isfrom_bool(v)) then Any..as_bool!(v) else +procedure Any_to_bool (v: Any) : bool +return if (Any..isfrom_bool(v)) then Any..as_bool!(v) else if (Any..isfrom_None(v)) then false else if (Any..isfrom_str(v)) then !(Any..as_string!(v) == "") else if (Any..isfrom_int(v)) then !(Any..as_int!(v) == 0) else if (Any..isfrom_float(v)) then !(Any..as_float!(v) == 0.0) else if (Any..isfrom_DictStrAny(v)) then !(Any..as_Dict!(v) == DictStrAny_empty()) else if (Any..isfrom_ListAny(v)) then !(Any..as_ListAny!(v) == ListAny_nil()) else - -}; + ; -function to_bool_any(v: Any) : Any -{ - from_bool(Any_to_bool(v)) -}; +procedure to_bool_any(v: Any) : Any +return from_bool(Any_to_bool(v)); // ///////////////////////////////////////////////////////////////////////////////////// // ListAny functions // ///////////////////////////////////////////////////////////////////////////////////// -function List_len (l : ListAny) : int -{ - if ListAny..isListAny_nil(l) then 0 else 1 + List_len(ListAny..tail!(l)) -}; +procedure List_len (l : ListAny) : int +return if ListAny..isListAny_nil(l) then 0 else 1 + List_len(ListAny..tail!(l)); procedure List_len_pos(l : ListAny) invokeOn List_len(l) opaque ensures List_len(l) >= 0; -function List_contains (l : ListAny, x: Any) : bool -{ - if ListAny..isListAny_nil(l) then false else (ListAny..head!(l) == x) || List_contains(ListAny..tail!(l), x) -}; +procedure List_contains (l : ListAny, x: Any) : bool +return if ListAny..isListAny_nil(l) then false else (ListAny..head!(l) == x) || List_contains(ListAny..tail!(l), x); -function List_extend (l1 : ListAny, l2: ListAny) : ListAny -{ - if ListAny..isListAny_nil(l1) then l2 - else ListAny_cons(ListAny..head!(l1), List_extend(ListAny..tail!(l1), l2)) -}; +procedure List_extend (l1 : ListAny, l2: ListAny) : ListAny +return if ListAny..isListAny_nil(l1) then l2 + else ListAny_cons(ListAny..head!(l1), List_extend(ListAny..tail!(l1), l2)); -function List_get_non_neg (l : ListAny, i : int) : Any +procedure List_get_non_neg (l : ListAny, i : int) : Any requires i >= 0 && i < List_len(l) -{ - if ListAny..isListAny_nil(l) then from_None() +return if ListAny..isListAny_nil(l) then from_None() else if i == 0 then ListAny..head!(l) - else List_get_non_neg(ListAny..tail!(l), i - 1) -}; + else List_get_non_neg(ListAny..tail!(l), i - 1); -function List_get (l : ListAny, i : int) : Any +procedure List_get (l : ListAny, i : int) : Any requires i >= - List_len(l) && i < List_len(l) -{ - if i >= 0 then List_get_non_neg(l, i) - else List_get_non_neg(l, List_len(l) + i) -}; +return if i >= 0 then List_get_non_neg(l, i) + else List_get_non_neg(l, List_len(l) + i); -function List_take (l : ListAny, i: int) : ListAny +procedure List_take (l : ListAny, i: int) : ListAny requires i >= 0 && i <= List_len(l) -{ - if ListAny..isListAny_nil(l) then ListAny_nil() +return if ListAny..isListAny_nil(l) then ListAny_nil() else if i == 0 then ListAny_nil() - else ListAny_cons(ListAny..head!(l), List_take(ListAny..tail!(l), i - 1)) -}; + else ListAny_cons(ListAny..head!(l), List_take(ListAny..tail!(l), i - 1)); procedure List_take_len(l : ListAny, i: int) invokeOn List_len(List_take(l,i)) opaque ensures i >= 0 && i <= List_len(l) ==> List_len(List_take(l,i)) == i; -function List_drop (l : ListAny, i: int) : ListAny +procedure List_drop (l : ListAny, i: int) : ListAny requires i >= 0 && i <= List_len(l) -{ - if ListAny..isListAny_nil(l) then ListAny_nil() +return if ListAny..isListAny_nil(l) then ListAny_nil() else if i == 0 then l - else List_drop(ListAny..tail!(l), i - 1) -}; + else List_drop(ListAny..tail!(l), i - 1); procedure List_drop_len(l : ListAny, i: int) invokeOn List_len(List_drop(l,i)) opaque ensures i >= 0 && i <= List_len(l) ==> List_len(List_drop(l,i)) == List_len(l) - i; -function int_max (i1: int, i2: int) : int -{ - if i1 >= i2 then i1 else i2 -}; +procedure int_max (i1: int, i2: int) : int +return if i1 >= i2 then i1 else i2; -function int_min (i1: int, i2: int) : int -{ - if i1 <= i2 then i1 else i2 -}; +procedure int_min (i1: int, i2: int) : int +return if i1 <= i2 then i1 else i2; -function List_slice_non_neg (l : ListAny, start : int, stop: int) : ListAny +procedure List_slice_non_neg (l : ListAny, start : int, stop: int) : ListAny requires start >= 0 && stop >= 0 -{ - if (start >= List_len(l)) || (start >= stop) then ListAny_nil() - else List_take (List_drop (l, start), int_min(stop, List_len(l)) - start) -}; +return if (start >= List_len(l)) || (start >= stop) then ListAny_nil() + else List_take (List_drop (l, start), int_min(stop, List_len(l)) - start); -function List_slice (l : ListAny, start : int, stop: int) : ListAny -{ - List_slice_non_neg (l, +procedure List_slice (l : ListAny, start : int, stop: int) : ListAny +return List_slice_non_neg (l, if start >= 0 then start else int_max (List_len(l) + start, 0), if stop >= 0 then stop else int_max (List_len(l) + stop, 0) - ) -}; + ); -function List_set_non_neg (l : ListAny, i : int, v: Any) : ListAny +procedure List_set_non_neg (l : ListAny, i : int, v: Any) : ListAny requires i >= 0 && i < List_len(l) -{ - if ListAny..isListAny_nil(l) then ListAny_nil() +return if ListAny..isListAny_nil(l) then ListAny_nil() else if i == 0 then ListAny_cons(v, ListAny..tail!(l)) - else ListAny_cons(ListAny..head!(l), List_set_non_neg(ListAny..tail!(l), i - 1, v)) -}; + else ListAny_cons(ListAny..head!(l), List_set_non_neg(ListAny..tail!(l), i - 1, v)); -function List_set (l : ListAny, i : int, v: Any) : ListAny +procedure List_set (l : ListAny, i : int, v: Any) : ListAny requires i >= - List_len(l) && i < List_len(l) -{ - if i >= 0 then List_set_non_neg(l, i, v) - else List_set_non_neg(l, List_len(l) + i, v) -}; +return if i >= 0 then List_set_non_neg(l, i, v) + else List_set_non_neg(l, List_len(l) + i, v); //Require recursive function on int -function List_repeat (l: ListAny, n: int): ListAny; +procedure List_repeat (l: ListAny, n: int): ListAny; -function range (start: Any, stop: Any, step: Any) : Any +procedure range (start: Any, stop: Any, step: Any) : Any requires Any..isfrom_int(start) && Any..isfrom_None(stop) && Any..isfrom_None(step); // ///////////////////////////////////////////////////////////////////////////////////// // DictStrAny functions // ///////////////////////////////////////////////////////////////////////////////////// -function DictStrAny_contains (d : DictStrAny, key: string) : bool -{ - if DictStrAny..isDictStrAny_empty(d) then false - else (DictStrAny..key!(d) == key) || DictStrAny_contains(DictStrAny..tail!(d), key) -}; +procedure DictStrAny_contains (d : DictStrAny, key: string) : bool +return if DictStrAny..isDictStrAny_empty(d) then false + else (DictStrAny..key!(d) == key) || DictStrAny_contains(DictStrAny..tail!(d), key); -function DictStrAny_get (d : DictStrAny, key: string) : Any +procedure DictStrAny_get (d : DictStrAny, key: string) : Any requires DictStrAny_contains(d, key) -{ - if DictStrAny..isDictStrAny_empty(d) then from_None() +return if DictStrAny..isDictStrAny_empty(d) then from_None() else if DictStrAny..key!(d) == key then DictStrAny..val!(d) - else DictStrAny_get(DictStrAny..tail!(d), key) -}; + else DictStrAny_get(DictStrAny..tail!(d), key); -function DictStrAny_get_or_none (d : DictStrAny, key: string) : Any -{ - if DictStrAny_contains(d, key) then DictStrAny_get(d, key) - else from_None() -}; +procedure DictStrAny_get_or_none (d : DictStrAny, key: string) : Any +return if DictStrAny_contains(d, key) then DictStrAny_get(d, key) + else from_None(); -function Any_get_or_none (dict: Any, key: Any) : Any +procedure Any_get_or_none (dict: Any, key: Any) : Any requires Any..isfrom_DictStrAny(dict) && Any..isfrom_str(key) -{ - DictStrAny_get_or_none(Any..as_Dict!(dict), Any..as_string!(key)) -}; +return DictStrAny_get_or_none(Any..as_Dict!(dict), Any..as_string!(key)); -function DictStrAny_insert (d : DictStrAny, key: string, val: Any) : DictStrAny -{ - if DictStrAny..isDictStrAny_empty(d) then DictStrAny_cons(key, val, DictStrAny_empty()) +procedure DictStrAny_insert (d : DictStrAny, key: string, val: Any) : DictStrAny +return if DictStrAny..isDictStrAny_empty(d) then DictStrAny_cons(key, val, DictStrAny_empty()) else if DictStrAny..key!(d) == key then DictStrAny_cons(key, val, DictStrAny..tail!(d)) - else DictStrAny_cons(DictStrAny..key!(d), DictStrAny..val!(d), DictStrAny_insert(DictStrAny..tail!(d), key, val)) -}; + else DictStrAny_cons(DictStrAny..key!(d), DictStrAny..val!(d), DictStrAny_insert(DictStrAny..tail!(d), key, val)); -function Any_get (dictOrList: Any, index: Any): Any +procedure Any_get (dictOrList: Any, index: Any): Any requires (Any..isfrom_DictStrAny(dictOrList) && Any..isfrom_str(index) && DictStrAny_contains(Any..as_Dict!(dictOrList), Any..as_string!(index))) || (Any..isfrom_ListAny(dictOrList) && Any..isfrom_int(index) && Any..as_int!(index) >= - List_len(Any..as_ListAny!(dictOrList)) && Any..as_int!(index) < List_len(Any..as_ListAny!(dictOrList))) -{ - if Any..isfrom_DictStrAny(dictOrList) then +return if Any..isfrom_DictStrAny(dictOrList) then DictStrAny_get(Any..as_Dict!(dictOrList), Any..as_string!(index)) else - List_get(Any..as_ListAny!(dictOrList), Any..as_int!(index)) -}; + List_get(Any..as_ListAny!(dictOrList), Any..as_int!(index)); -function Any_get_slice (list: Any, index: Any): Any +procedure Any_get_slice (list: Any, index: Any): Any requires (Any..isfrom_ListAny(list) && Any..isfrom_Slice(index)) -{ - from_ListAny(List_slice( +return from_ListAny(List_slice( Any..as_ListAny!(list), Any..start!(index), if OptionInt..isOptSome(Any..stop!(index)) then OptionInt..unwrap!(Any..stop!(index)) - else List_len(Any..as_ListAny!(list)))) -}; + else List_len(Any..as_ListAny!(list)))); -function Any_get! (dictOrList: Any, index: Any): Any -{ - if Any..isexception(dictOrList) then dictOrList +procedure Any_get! (dictOrList: Any, index: Any): Any +return if Any..isexception(dictOrList) then dictOrList else if Any..isexception(index) then index else if !(Any..isfrom_DictStrAny(dictOrList) && Any..isfrom_str(index)) && !(Any..isfrom_ListAny(dictOrList) && Any..isfrom_int(index)) then exception (TypeError("Invalid subscription type")) @@ -507,23 +439,19 @@ function Any_get! (dictOrList: Any, index: Any): Any else if Any..isfrom_ListAny(dictOrList) && Any..isfrom_int(index) && Any..as_int!(index) >= - List_len(Any..as_ListAny!(dictOrList)) && Any..as_int!(index) < List_len(Any..as_ListAny!(dictOrList)) then List_get(Any..as_ListAny!(dictOrList), Any..as_int!(index)) else - exception (IndexError("Invalid subscription")) -}; + exception (IndexError("Invalid subscription")); -function Any_set (dictOrList: Any, index: Any, val: Any): Any +procedure Any_set (dictOrList: Any, index: Any, val: Any): Any requires (Any..isfrom_DictStrAny(dictOrList) && Any..isfrom_str(index)) || (Any..isfrom_ListAny(dictOrList) && Any..isfrom_int(index) && Any..as_int!(index) >= - List_len(Any..as_ListAny!(dictOrList)) && Any..as_int!(index) < List_len(Any..as_ListAny!(dictOrList))) -{ - if Any..isfrom_DictStrAny(dictOrList) then +return if Any..isfrom_DictStrAny(dictOrList) then from_DictStrAny(DictStrAny_insert(Any..as_Dict!(dictOrList), Any..as_string!(index), val)) else - from_ListAny(List_set(Any..as_ListAny!(dictOrList), Any..as_int!(index), val)) -}; + from_ListAny(List_set(Any..as_ListAny!(dictOrList), Any..as_int!(index), val)); -function Any_set! (dictOrList: Any, index: Any, val: Any): Any -{ - if Any..isexception(dictOrList) then dictOrList +procedure Any_set! (dictOrList: Any, index: Any, val: Any): Any +return if Any..isexception(dictOrList) then dictOrList else if Any..isexception(index) then index else if Any..isexception(val) then val else if !(Any..isfrom_DictStrAny(dictOrList) && Any..isfrom_str(index)) && !(Any..isfrom_ListAny(dictOrList) && Any..isfrom_int(index)) then @@ -534,66 +462,57 @@ function Any_set! (dictOrList: Any, index: Any, val: Any): Any Any..as_int!(index) >= - List_len(Any..as_ListAny!(dictOrList)) && Any..as_int!(index) < List_len(Any..as_ListAny!(dictOrList)) then from_ListAny(List_set(Any..as_ListAny!(dictOrList), Any..as_int!(index), val)) else - exception (IndexError("Index out of bound")) -}; + exception (IndexError("Index out of bound")); -function Any_sets! (indices: ListAny, dictOrList: Any, val: Any): Any -{ - if ListAny..isListAny_nil(indices) then dictOrList +procedure Any_sets! (indices: ListAny, dictOrList: Any, val: Any): Any +return if ListAny..isListAny_nil(indices) then dictOrList else if ListAny..isListAny_nil(ListAny..tail!(indices)) then Any_set!(dictOrList, ListAny..head!(indices), val) else Any_set!(dictOrList, ListAny..head!(indices), - Any_sets!(ListAny..tail!(indices), Any_get!(dictOrList, ListAny..head!(indices)), val)) -}; + Any_sets!(ListAny..tail!(indices), Any_get!(dictOrList, ListAny..head!(indices)), val)); -function Any_len (v: Any) : int; +procedure Any_len (v: Any) : int; -function Any_len_to_Any (v: Any) : Any { - from_int(Any_len(v)) -}; +procedure Any_len_to_Any (v: Any) : Any +return from_int(Any_len(v)); procedure Any_len_pos(v: Any) invokeOn Any_len(v) opaque ensures Any_len(v) >= 0; -function Any_iter_index(iter: Any, index: int) : Any; +procedure Any_iter_index(iter: Any, index: int) : Any; -function PIn (v: Any, dictOrList: Any) : Any +procedure PIn (v: Any, dictOrList: Any) : Any requires (Any..isfrom_DictStrAny(dictOrList) && Any..isfrom_str(v)) || Any..isfrom_ListAny(dictOrList) -{ - from_bool( +return from_bool( if Any..isfrom_DictStrAny(dictOrList) then DictStrAny_contains(Any..as_Dict!(dictOrList), Any..as_string!(v)) else List_contains(Any..as_ListAny!(dictOrList), v) - ) -}; + ); -function PNotIn ( v: Any, dictOrList: Any) : Any +procedure PNotIn ( v: Any, dictOrList: Any) : Any requires (Any..isfrom_DictStrAny(dictOrList) && Any..isfrom_str(v)) || Any..isfrom_ListAny(dictOrList) -{ - from_bool( +return from_bool( if Any..isfrom_DictStrAny(dictOrList) then !DictStrAny_contains(Any..as_Dict!(dictOrList), Any..as_string!(v)) else !List_contains(Any..as_ListAny!(dictOrList), v) - ) -}; + ); // ///////////////////////////////////////////////////////////////////////////////////// // Python treats some values of different types to be equivalent // This function models that behavior // ///////////////////////////////////////////////////////////////////////////////////// -function is_IntReal (v: Any) : bool; -function Any_real_to_int (v: Any) : int; +procedure is_IntReal (v: Any) : bool; +procedure Any_real_to_int (v: Any) : int; -function normalize_any (v : Any) : Any { - if v == from_bool(true) then from_int(1) +procedure normalize_any (v : Any) : Any +return if v == from_bool(true) then from_int(1) else (if v == from_bool(false) then from_int(0) else if Any..isfrom_float(v) && is_IntReal(v) then from_int(Any_real_to_int(v)) else - v) -}; + v); // ///////////////////////////////////////////////////////////////////////////////////// @@ -606,21 +525,22 @@ function normalize_any (v : Any) : Any { // ///////////////////////////////////////////////////////////////////////////////////// // This function convert an int to a real // Need to connect to an SMT function -function int_to_real (i: int) : real; +procedure int_to_real (i: int) : real; // ///////////////////////////////////////////////////////////////////////////////////// // Converting bool to int or real // Used to in Python binary operators' modelling -function bool_to_int (bval: bool) : int {if bval then 1 else 0}; -function bool_to_real (b: bool) : real {if b then 1.0 else 0.0}; +procedure bool_to_int (bval: bool) : int +return if bval then 1 else 0; +procedure bool_to_real (b: bool) : real +return if b then 1.0 else 0.0; // ///////////////////////////////////////////////////////////////////////////////////// // Modelling of Python unary operations // ///////////////////////////////////////////////////////////////////////////////////// -function PNeg (v: Any) : Any -{ - if Any..isexception(v) then v +procedure PNeg (v: Any) : Any +return if Any..isexception(v) then v else if Any..isfrom_bool(v) then from_int(- bool_to_int(Any..as_bool!(v))) else if Any..isfrom_int(v) then @@ -628,35 +548,29 @@ function PNeg (v: Any) : Any else if Any..isfrom_float(v) then from_float(- Any..as_float!(v)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function PBitNot (v: Any) : Any -{ - if Any..isexception(v) then v +procedure PBitNot (v: Any) : Any +return if Any..isexception(v) then v else if Any..isfrom_bool(v) then from_int(-(bool_to_int(Any..as_bool!(v)) + 1)) else if Any..isfrom_int(v) then from_int(-(Any..as_int!(v) + 1)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function PNot (v: Any) : Any -{ - if Any..isexception(v) then v - else from_bool(!(Any_to_bool(v))) -}; +procedure PNot (v: Any) : Any +return if Any..isexception(v) then v + else from_bool(!(Any_to_bool(v))); // ///////////////////////////////////////////////////////////////////////////////////// // Modelling of Python binary operations // ///////////////////////////////////////////////////////////////////////////////////// -function Str.Concat(s: string, s2: string): string external; +procedure Str.Concat(s: string, s2: string): string external; -function PAdd (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PAdd (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_int(bool_to_int(Any..as_bool!(v1)) + bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -680,12 +594,10 @@ function PAdd (v1: Any, v2: Any) : Any else if Any..isfrom_datetime(v1) && Any..isfrom_int(v2) then from_datetime((Any..as_datetime!(v1) + Any..as_int!(v2))) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function PSub (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PSub (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_int(bool_to_int(Any..as_bool!(v1)) - bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -709,14 +621,12 @@ function PSub (v1: Any, v2: Any) : Any else if Any..isfrom_datetime(v1) && Any..isfrom_datetime(v2) then from_int(Any..as_datetime!(v1) - Any..as_datetime!(v2)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function string_repeat (s: string, i: int) : string; +procedure string_repeat (s: string, i: int) : string; -function PMul (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PMul (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_int(bool_to_int(Any..as_bool!(v1)) * bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -748,13 +658,11 @@ function PMul (v1: Any, v2: Any) : Any else if Any..isfrom_float(v1) && Any..isfrom_float(v2) then from_float(Any..as_float!(v1) * Any..as_float!(v2)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function PFloorDiv (v1: Any, v2: Any) : Any +procedure PFloorDiv (v1: Any, v2: Any) : Any requires (Any..isfrom_bool(v2)==>Any..as_bool!(v2)) && (Any..isfrom_int(v2)==>Any..as_int!(v2)!=0) -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_int( bool_to_int(Any..as_bool!(v1)) / bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -764,25 +672,23 @@ function PFloorDiv (v1: Any, v2: Any) : Any else if Any..isfrom_int(v1) && Any..isfrom_int(v2) then from_int(Any..as_int!(v1) / Any..as_int!(v2)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); // ///////////////////////////////////////////////////////////////////////////////////// // Modelling of Python comparision operations // ///////////////////////////////////////////////////////////////////////////////////// -function string_lt (s1: string, s2: string) : bool; -function string_le (s1: string, s2: string) : bool; -function string_gt (s1: string, s2: string) : bool; -function string_ge (s1: string, s2: string) : bool; -function List_lt (l1: ListAny, l2: ListAny): bool; -function List_le (l1: ListAny, l2: ListAny): bool; -function List_gt (l1: ListAny, l2: ListAny): bool; -function List_ge (l1: ListAny, l2: ListAny): bool; +procedure string_lt (s1: string, s2: string) : bool; +procedure string_le (s1: string, s2: string) : bool; +procedure string_gt (s1: string, s2: string) : bool; +procedure string_ge (s1: string, s2: string) : bool; +procedure List_lt (l1: ListAny, l2: ListAny): bool; +procedure List_le (l1: ListAny, l2: ListAny): bool; +procedure List_gt (l1: ListAny, l2: ListAny): bool; +procedure List_ge (l1: ListAny, l2: ListAny): bool; -function PLt (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PLt (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_bool(bool_to_int(Any..as_bool!(v1)) < bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -808,12 +714,10 @@ function PLt (v1: Any, v2: Any) : Any else if Any..isfrom_datetime(v1) && Any..isfrom_datetime(v2) then from_bool(Any..as_datetime!(v1) bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -870,12 +772,10 @@ function PGt (v1: Any, v2: Any) : Any else if Any..isfrom_datetime(v1) && Any..isfrom_datetime(v2) then from_bool(Any..as_datetime!(v1) >Any..as_datetime!(v2)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function PGe (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PGe (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_bool(bool_to_int(Any..as_bool!(v1)) >= bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -901,32 +801,25 @@ function PGe (v1: Any, v2: Any) : Any else if Any..isfrom_datetime(v1) && Any..isfrom_datetime(v2) then from_bool(Any..as_datetime!(v1) >=Any..as_datetime!(v2)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function PEq (v: Any, v': Any) : Any { - from_bool(normalize_any(v) == normalize_any (v')) -}; +procedure PEq (v: Any, v': Any) : Any +return from_bool(normalize_any(v) == normalize_any (v')); -function PNEq (v: Any, v': Any) : Any { - from_bool(normalize_any(v) != normalize_any (v')) -}; +procedure PNEq (v: Any, v': Any) : Any +return from_bool(normalize_any(v) != normalize_any (v')); // ///////////////////////////////////////////////////////////////////////////////////// // Modelling of Python Boolean operations And and Or // ///////////////////////////////////////////////////////////////////////////////////// -function PAnd (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else - if ! Any_to_bool (v1) then v1 else v2 -}; +procedure PAnd (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else + if ! Any_to_bool (v1) then v1 else v2; -function POr (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else - if Any_to_bool (v1) then v1 else v2 -}; +procedure POr (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else + if Any_to_bool (v1) then v1 else v2; // ///////////////////////////////////////////////////////////////////////////////////// // Modelling of Python arithmetic and bitwise operations @@ -934,16 +827,15 @@ function POr (v1: Any, v2: Any) : Any // int_pow, int_rshift, and float_pow are provided by the factory (PyFactory.lean) with concreteEval. // Declared here as external so PPow/PRShift can reference them; they are filtered // during Laurel-to-Core translation and the factory provides the Core versions. -function int_pow (base: int, exp: int) : int +procedure int_pow (base: int, exp: int) : int external; -function int_rshift (x: int, n: int) : int +procedure int_rshift (x: int, n: int) : int external; -function float_pow (base: real, exp: real) : real +procedure float_pow (base: real, exp: real) : real external; -function PPow (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PPow (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if (Any..isfrom_int(v1) && Any..isfrom_int(v2) && Any..as_int!(v2) >= 0) then from_int(int_pow(Any..as_int!(v1), Any..as_int!(v2))) else if (Any..isfrom_int(v1) && Any..isfrom_int(v2)) then @@ -955,13 +847,11 @@ function PPow (v1: Any, v2: Any) : Any else if (Any..isfrom_int(v1) && Any..isfrom_bool(v2)) then from_int(int_pow(Any..as_int!(v1), bool_to_int(Any..as_bool!(v2)))) else - exception(UnimplementedError("Pow is not defined on these input types")) -}; + exception(UnimplementedError("Pow is not defined on these input types")); -function PMod (v1: Any, v2: Any) : Any +procedure PMod (v1: Any, v2: Any) : Any requires (Any..isfrom_bool(v2)==>Any..as_bool!(v2)) && (Any..isfrom_int(v2)==>Any..as_int!(v2)!=0) -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_int( bool_to_int(Any..as_bool!(v1)) % bool_to_int(Any..as_bool!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) then @@ -971,16 +861,14 @@ function PMod (v1: Any, v2: Any) : Any else if Any..isfrom_int(v1) && Any..isfrom_int(v2) then from_int(Any..as_int!(v1) % Any..as_int!(v2)) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); // ///////////////////////////////////////////////////////////////////////////////////// // Modelling of Python bitwise shift operations // ///////////////////////////////////////////////////////////////////////////////////// -function PLShift (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PLShift (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_int(v1) && Any..isfrom_int(v2) && Any..as_int!(v2) >= 0 then from_int(Any..as_int!(v1) * int_pow(2, Any..as_int!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) && Any..as_int!(v2) >= 0 then @@ -990,12 +878,10 @@ function PLShift (v1: Any, v2: Any) : Any else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_int(bool_to_int(Any..as_bool!(v1)) * int_pow(2, bool_to_int(Any..as_bool!(v2)))) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); -function PRShift (v1: Any, v2: Any) : Any -{ - if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 +procedure PRShift (v1: Any, v2: Any) : Any +return if Any..isexception(v1) then v1 else if Any..isexception(v2) then v2 else if Any..isfrom_int(v1) && Any..isfrom_int(v2) && Any..as_int!(v2) >= 0 then from_int(int_rshift(Any..as_int!(v1), Any..as_int!(v2))) else if Any..isfrom_bool(v1) && Any..isfrom_int(v2) && Any..as_int!(v2) >= 0 then @@ -1005,20 +891,18 @@ function PRShift (v1: Any, v2: Any) : Any else if Any..isfrom_bool(v1) && Any..isfrom_bool(v2) then from_int(int_rshift(bool_to_int(Any..as_bool!(v1)), bool_to_int(Any..as_bool!(v2)))) else - exception(UndefinedError ("Operand Type is not defined")) -}; + exception(UndefinedError ("Operand Type is not defined")); // ///////////////////////////////////////////////////////////////////////////////////// // Modelling some datetime-related Python operations, for testing purpose // ///////////////////////////////////////////////////////////////////////////////////// -function to_string(a: Any) : string; +procedure to_string(a: Any) : string; -function to_string_any(a: Any) : Any { - from_str(to_string(a)) -}; +procedure to_string_any(a: Any) : Any +return from_str(to_string(a)); -function datetime_strptime(dtstring: Any, format: Any) : Any; +procedure datetime_strptime(dtstring: Any, format: Any) : Any; procedure datetime_tostring_cancel(dt: Any) invokeOn datetime_strptime(to_string_any(dt), from_str ("%Y-%m-%d")) diff --git a/StrataPython/StrataPython/Specs/ToLaurel.lean b/StrataPython/StrataPython/Specs/ToLaurel.lean index d73b80f5e8..afd546ecb1 100644 --- a/StrataPython/StrataPython/Specs/ToLaurel.lean +++ b/StrataPython/StrataPython/Specs/ToLaurel.lean @@ -562,7 +562,6 @@ def funcDeclToLaurel (procName : String) (func : FunctionDecl) outputs := outputs preconditions := [] decreases := none - isFunctional := false body := body } From 5c4a0b671c6a3de2031b115c8692a7a2f759f248 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 17 Jun 2026 21:09:01 +0000 Subject: [PATCH 32/42] update for Python --- StrataPython/StrataPython/PythonToLaurel.lean | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/StrataPython/StrataPython/PythonToLaurel.lean b/StrataPython/StrataPython/PythonToLaurel.lean index e264bcf8f1..6c10e0db87 100644 --- a/StrataPython/StrataPython/PythonToLaurel.lean +++ b/StrataPython/StrataPython/PythonToLaurel.lean @@ -2383,7 +2383,6 @@ def translateFunction (ctx : TranslationContext) (sourceRange: SourceRange) (fun preconditions := typeConstraintPreconditions decreases := none body := bodyTrans - isFunctional := false } return (proc, {newCtx with variableTypes := []}) @@ -2560,7 +2559,6 @@ def mkDefaultInitDecl (className : String) : PythonFunctionDecl × Procedure := inputs := inputs outputs := [{name := "LaurelResult", type := AnyTy}] preconditions := [{ condition := mkStmtExprMd (StmtExpr.LiteralBool true) }] - isFunctional := false decreases := none body := .Opaque [] .none wildcardModifies } @@ -2755,7 +2753,7 @@ def PreludeInfo.ofLaurelProgram (prog : Laurel.Program) : PreludeInfo where | _ => s procedures := prog.staticProcedures.foldl (init := {}) fun m p => - if p.body.isExternal || p.isFunctional then m + if p.body.isExternal then m else -- Use "Any" for all parameter types to match the Python→Laurel -- pipeline's Any-wrapping convention at call sites. @@ -2778,8 +2776,6 @@ def PreludeInfo.ofLaurelProgram (prog : Laurel.Program) : PreludeInfo where typeTesters := pyLauTypeTesters tys : PyRetInfo } some { name := p.name.text, args := args, kwargsName := none, ret := ret } functions := - let funcNames := prog.staticProcedures.filterMap fun p => - if p.body.isExternal || !p.isFunctional then none else some p.name.text let dtFuncs := prog.types.flatMap fun td => match td with | .Datatype dt => @@ -2791,12 +2787,12 @@ def PreludeInfo.ofLaurelProgram (prog : Laurel.Program) : PreludeInfo where let testers := dt.constructors.map fun c => "is" ++ c.name.text ctors ++ destrs ++ testers | _ => [] - funcNames ++ dtFuncs + dtFuncs maybeExceptionFunctions := prog.staticProcedures.filterMap fun p => if p.name.text ∈ AnyMaybeExceptionList then some p.name.text else none procedureNames := prog.staticProcedures.filterMap fun p => - if p.body.isExternal || p.isFunctional then none else some p.name.text + if p.body.isExternal then none else some p.name.text callableProcedures := prog.staticProcedures.foldl (init := {}) fun s p => match p.body with @@ -2980,7 +2976,6 @@ def pythonToLaurel (info : PreludeInfo) preconditions := [], decreases := none, body := .Opaque [] (some bodyBlock) wildcardModifies - isFunctional := false } -- Generate $composite_to_string_ and $composite_to_string_any_ @@ -2998,16 +2993,14 @@ def pythonToLaurel (info : PreludeInfo) outputs := [{ name := "result", type := mkHighTypeMd .TString }] preconditions := [] decreases := none - body := .Opaque [] none wildcardModifies - isFunctional := false } + body := .Opaque [] none wildcardModifies } procedures := procedures.push { name := { text := compositeToStringAnyName ct.name.text } inputs := [selfParam] outputs := [{ name := "result", type := AnyTy }] preconditions := [] decreases := none - body := .Opaque [] none wildcardModifies - isFunctional := false } + body := .Opaque [] none wildcardModifies } let program : Laurel.Program := { staticProcedures := (procedures.push mainProc).toList From f1cb6c4a98050d0e08330d72c96b37162fc269cc Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 23 Jun 2026 10:11:25 +0000 Subject: [PATCH 33/42] returnLabel match_pattern --- Strata/Languages/Laurel/EliminateReturnStatements.lean | 4 ++-- Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Strata/Languages/Laurel/EliminateReturnStatements.lean b/Strata/Languages/Laurel/EliminateReturnStatements.lean index 3600504ad8..5ebec71f08 100644 --- a/Strata/Languages/Laurel/EliminateReturnStatements.lean +++ b/Strata/Languages/Laurel/EliminateReturnStatements.lean @@ -24,8 +24,8 @@ namespace Strata.Laurel public section -private def returnLabel : String := "$return" - +@[expose, match_pattern] +public def returnLabel : String := "$return" /-- Transform a single procedure: wrap body in a labelled block and replace returns. -/ private def eliminateReturnStmts (proc : Procedure) : Procedure := diff --git a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean index 319a0e4d0f..a6a1a218b0 100644 --- a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean +++ b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean @@ -9,6 +9,7 @@ public import Strata.Languages.Core.Program public import Strata.Languages.Core.Options public import Strata.Languages.Laurel.PushOldInward public import Strata.Languages.Laurel.CoreGroupingAndOrdering +public import Strata.Languages.Laurel.EliminateReturnStatements import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator import Strata.Util.Tactics public import Strata.Languages.Laurel.Resolution @@ -427,7 +428,6 @@ Diagnostics are emitted into the monad state. def translateStmt (stmt : StmtExprMd) : TranslateM (List Core.Statement) := do let s ← get - let model := s.model let md := astNodeToCoreMd stmt match _h : stmt.val with | .Assert cond => @@ -681,7 +681,7 @@ instance : Inhabited LaurelVerifyOptions where -/ private def unwrapReturnBlock (b : StmtExprMd) : StmtExprMd := match b.val with - | .Block [⟨.Assign [⟨.Local _, _⟩] value, _⟩, ⟨.Exit "$return", _⟩] (some "$return") => value + | .Block [⟨.Assign [⟨.Local _, _⟩] value, _⟩, ⟨.Exit returnLabel, _⟩] (some returnLabel) => value | _ => b /-- From 4e9173ab950790fbdaf9ea8edfd0a53e5d90d718 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 23 Jun 2026 10:12:17 +0000 Subject: [PATCH 34/42] Fix warning --- Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean | 1 - 1 file changed, 1 deletion(-) diff --git a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean index a6a1a218b0..8e8e2aa904 100644 --- a/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean +++ b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean @@ -427,7 +427,6 @@ Diagnostics are emitted into the monad state. -/ def translateStmt (stmt : StmtExprMd) : TranslateM (List Core.Statement) := do - let s ← get let md := astNodeToCoreMd stmt match _h : stmt.val with | .Assert cond => From ed512715f4e3708722263815d12c14decb8cc07c Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 23 Jun 2026 12:20:20 +0000 Subject: [PATCH 35/42] Fixes --- Strata/Languages/Laurel/ContractPass.lean | 5 ----- Strata/Languages/Laurel/EliminateValueInReturns.lean | 7 ------- 2 files changed, 12 deletions(-) diff --git a/Strata/Languages/Laurel/ContractPass.lean b/Strata/Languages/Laurel/ContractPass.lean index 7efe437fb3..dbbf8b3e14 100644 --- a/Strata/Languages/Laurel/ContractPass.lean +++ b/Strata/Languages/Laurel/ContractPass.lean @@ -289,14 +289,9 @@ private def rewriteCallSites (contractInfoMap : Std.HashMap String ContractInfo) | none => return e' | _ => return e')) let (tempDecls, tempRefs) ← mkTempAssignments args' info.inputParams src -<<<<<<< HEAD - let callWithTemps : StmtExprMd := ⟨.Assign targets ⟨.StaticCall callee tempRefs, callSrc⟩, src⟩ - let preCheck := mkPreChecks info tempRefs src -======= let callArgs := mkCallArgs info args' tempRefs let callWithTemps : StmtExprMd := ⟨.Assign targets ⟨.StaticCall callee callArgs, callSrc⟩, src⟩ let preCheck := mkPreChecks info tempRefs src ->>>>>>> issue-924-contract-and-proof-pass let outputArgs := targets.filterMap fun t => match t.val with | .Local name => some (mkMd (.Var (.Local name))) diff --git a/Strata/Languages/Laurel/EliminateValueInReturns.lean b/Strata/Languages/Laurel/EliminateValueInReturns.lean index 36355b3678..196cb96446 100644 --- a/Strata/Languages/Laurel/EliminateValueInReturns.lean +++ b/Strata/Languages/Laurel/EliminateValueInReturns.lean @@ -54,18 +54,11 @@ def hasValuedReturn (stmt : StmtExprMd) : Bool := all_goals (try term_by_mem) all_goals omega -<<<<<<< HEAD -/-- Apply value-return elimination to a single procedure. Only applies to - procedures with exactly one output parameter. - Emits an error if a valued return is used with multiple output parameters. -/ -def eliminateValuesInReturnsInProc (proc : Procedure) : Procedure × Array DiagnosticModel := -======= /-- Apply value-return elimination to a single procedure. Rewrites `return expr` into `outParam := expr; return` for any procedure with exactly one output parameter. Emits an error if a valued return is used with zero or multiple output parameters. -/ def eliminateValueReturnsInProc (proc : Procedure) : Procedure := ->>>>>>> issue-924-contract-and-proof-pass match proc.outputs with | [outParam] => let pre (stmt : StmtExprMd) : Id (Option (List StmtExprMd)) := From bfd1750c57b4d6151623d3dafe12c3b6341c06b2 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 23 Jun 2026 12:23:25 +0000 Subject: [PATCH 36/42] Fixes --- Strata/Languages/Laurel/ContractPass.lean | 1 - .../Examples/Fundamentals/T2_ImpureExpressionsError.lean | 6 +++--- StrataTest/Util/TestLaurel.lean | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Strata/Languages/Laurel/ContractPass.lean b/Strata/Languages/Laurel/ContractPass.lean index dbbf8b3e14..26e833e512 100644 --- a/Strata/Languages/Laurel/ContractPass.lean +++ b/Strata/Languages/Laurel/ContractPass.lean @@ -122,7 +122,6 @@ private def mkPostConditionProc (name : String) (inputs outputs : List Parameter outputs := [⟨mkId "$result", { val := .TBool, source := none }⟩] preconditions := [] decreases := none - isFunctional := true body := .Transparent (renameOutputsInPostExpr outputNames condition.condition) } /-- Information about a procedure's contracts. -/ diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean index 970d638707..af0b8e952a 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean @@ -36,13 +36,13 @@ procedure transparentWithMutatingAssignment(x: int): int procedure functionWithWhile(x: int): int { while(false) {}; -//^^^^^^^^^^^^^^^ error: loops are not supported in functions or contracts - 3 +//^^^^^^^^^^^^^^^ error: loops are not supported in transparent bodies or contracts + return 3 }; procedure callsHasMutatingAssignment(x: int): int { - hasMutatingAssignment() + return hasMutatingAssignment() }; procedure impureContractIsLegal1(x: int) diff --git a/StrataTest/Util/TestLaurel.lean b/StrataTest/Util/TestLaurel.lean index d0ca1e9767..93382813c2 100644 --- a/StrataTest/Util/TestLaurel.lean +++ b/StrataTest/Util/TestLaurel.lean @@ -79,7 +79,7 @@ private def renderSnippetLocal (basePos : Nat) (snippet : String) /-- Default options used by `testLaurel` when the caller doesn't override: quiet verifier, default solver. Override by passing `(options := …)` to `testLaurel`. -/ -def defaultLaurelTestOptions : LaurelVerifyOptions := +public def defaultLaurelTestOptions : LaurelVerifyOptions := { verifyOptions := .quiet } /-- Run translate + resolve only on a parsed program. Skips SMT verification. @@ -296,7 +296,7 @@ private def runAndCheck (block : SourcedProgram) `showSnippet := true` to also append the snippet-relative range — useful for correlating against the inline `// ^^^` annotations, which are snippet-local. -/ -def testLaurel (block : SourcedProgram) +public def testLaurel (block : SourcedProgram) (options : LaurelVerifyOptions := defaultLaurelTestOptions) (showSnippet : Bool := false) : IO Unit := runAndCheck block (runLaurelPipelineRaw · options) showSnippet @@ -318,7 +318,7 @@ public def testLaurelKeepIntermediates (block : SourcedProgram) : IO Unit := do As with `testLaurel`, each diagnostic's file-relative `line:col` range is printed; `showSnippet := true` appends the snippet-relative range. -/ -def testLaurelResolution (block : SourcedProgram) +public def testLaurelResolution (block : SourcedProgram) (showSnippet : Bool := false) : IO Unit := runAndCheck block runLaurelResolutionRaw showSnippet From 823add64451d249d0ed18ba1f900196964acace4 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 23 Jun 2026 12:37:34 +0000 Subject: [PATCH 37/42] Test fixes --- .../T2_ImpureExpressionsError.lean | 17 +--------- .../T2_ImpureExpressionsError2.lean | 14 ++++++-- .../Laurel/ResolutionTypeCheckTests.lean | 32 +++++++++---------- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean index af0b8e952a..c27da6dc63 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean @@ -15,7 +15,7 @@ open Strata -- pipeline helper because the expected diagnostics are not pure resolution -- errors. -#eval testLaurel <| +#eval testLaurelKeepIntermediates <| #strata program Laurel; procedure hasMutatingAssignment(): int @@ -26,13 +26,6 @@ procedure hasMutatingAssignment(): int x }; -procedure transparentWithMutatingAssignment(x: int): int -{ - x := x + 1; -//^^^^^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts - return 3 -}; - procedure functionWithWhile(x: int): int { while(false) {}; @@ -51,12 +44,4 @@ procedure impureContractIsLegal1(x: int) { assert hasMutatingAssignment() == 1 }; - -procedure impureContractIsNotLegal2(x: int) - requires (x := 2) == 2 -// ^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts - opaque -{ - assert (x := 2) == 2 -}; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean index 8e4cda0262..0af4f2e3fc 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError2.lean @@ -15,13 +15,21 @@ meta section #strata program Laurel; -procedure transparentWithWhile(x: int): int +procedure transparentWithMutatingAssignment(x: int): int { - while(false) {}; -//^^^^^^^^^^^^^^^ error: loops are not supported in transparent bodies or contracts + x := x + 1; +//^^^^^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts return 3 }; +procedure impureContractIsNotLegal2(x: int) + requires (x := 2) == 2 +// ^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts + opaque +{ + assert (x := 2) == 2 +}; + #end end diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index e3888db451..32b94132a1 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -31,7 +31,7 @@ procedure voidReturn(x: int) #eval testLaurelResolution <| #strata program Laurel; -function foo(x: int): int { +procedure foo(x: int): int { if x then 1 else 0 // ^ error: expected 'bool', got 'int' }; @@ -72,7 +72,7 @@ procedure wh() opaque { #eval testLaurelResolution <| #strata program Laurel; -function foo(x: int, y: bool): bool { +procedure foo(x: int, y: bool): bool { x && y //^ error: expected 'bool', got 'int' }; @@ -83,7 +83,7 @@ function foo(x: int, y: bool): bool { #eval testLaurelResolution <| #strata program Laurel; -function cmp(x: string, y: int): bool { +procedure cmp(x: string, y: int): bool { x < y //^ error: '<' expected a numeric type, got 'string' }; @@ -116,8 +116,8 @@ procedure foo(): int { #eval testLaurelResolution <| #strata program Laurel; -function bar(x: int): int { x }; -function foo(): int { +procedure bar(x: int): int { x }; +procedure foo(): int { bar(true) // ^^^^ error: expected 'int', got 'bool' }; @@ -128,7 +128,7 @@ function foo(): int { #eval testLaurelResolution <| #strata program Laurel; -function cmp(x: int, y: string): bool { +procedure cmp(x: int, y: string): bool { x == y //^^^^^^ error: cannot compare 'int' with 'string' using '==' }; @@ -225,7 +225,7 @@ cleanly (no diagnostics). -/ #eval testLaurelResolution <| #strata program Laurel; -function foo(c: bool): bool { +procedure foo(c: bool): bool { (if c then 1 else 2) == 3 }; #end @@ -233,7 +233,7 @@ function foo(c: bool): bool { #eval testLaurelResolution <| #strata program Laurel; -function foo(): bool { +procedure foo(): bool { { 1 } == 1 }; #end @@ -247,7 +247,7 @@ and synthesizes `Unknown` to suppress cascading errors. -/ #eval testLaurelResolution <| #strata program Laurel; -function foo(c: bool): bool { +procedure foo(c: bool): bool { (if c then 1 else true) == 3 // ^^^^^^^^^^^^^^^^^^^^^ error: 'if' branches have incompatible types 'int' and 'bool' }; @@ -269,7 +269,7 @@ errored.) -/ #eval testLaurelResolution <| #strata program Laurel; -function foo(c: bool): bool { +procedure foo(c: bool): bool { (if c then else "x") < 1 // ^^^^^^^^^^^^^^^^^^^^^^ error: '<' expected a numeric type, got 'string' }; @@ -278,7 +278,7 @@ function foo(c: bool): bool { #eval testLaurelResolution <| #strata program Laurel; -function foo(c: bool): bool { +procedure foo(c: bool): bool { (if c then "x" else ) < 1 // ^^^^^^^^^^^^^^^^^^^^^^ error: '<' expected a numeric type, got 'string' }; @@ -294,7 +294,7 @@ than collapsing to `Unknown`. So `if c then else 5` synthesizes a usable #eval testLaurelResolution <| #strata program Laurel; -function bar(c: bool): int { +procedure bar(c: bool): int { if c then else 5 }; #end @@ -328,7 +328,7 @@ rejecting `TBv` and emitting a spurious "expected a numeric type" error.) -/ #eval testLaurelResolution <| #strata program Laurel; -function cmp(x: bv 32, y: bv 32): bool { +procedure cmp(x: bv 32, y: bv 32): bool { x < y }; #end @@ -343,8 +343,8 @@ arguments) is deliberately not flagged. -/ #eval testLaurelResolution <| #strata program Laurel; -function foo(x: int): int { x }; -function bar(): int { +procedure foo(x: int): int { x }; +procedure bar(): int { foo(1, 2) //^^^^^^^^^ error: call to 'foo' expects 1 argument(s) but 2 were provided }; @@ -362,7 +362,7 @@ behavior.) -/ #eval testLaurelResolution <| #strata program Laurel; -function bar(): int { +procedure bar(): int { nope(1, 2) //^^^^^^^^^^ error: 'nope' is not defined }; From d4aba9489506fe99fdee89dddff22f8eaa5aa51e Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 23 Jun 2026 13:08:06 +0000 Subject: [PATCH 38/42] Fix to lift pass --- .../Languages/Laurel/LiftImperativeExpressions.lean | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index e6f19962e3..1a6a19fd81 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -198,10 +198,18 @@ def containsAssignmentOrImperativeCall (imperativeCallees : List String) (expr : mutual def asLifted { t: Type } (runner: LiftM t) : LiftM t := do - let savedState ← get + -- Save only the bookkeeping that `runner` is meant to run in a fresh + -- sub-scope (`prependedStmts` and `subst`). We must NOT restore the whole + -- state: the monotonic counters (`condCounter`, `varCounters`) advanced by + -- `runner` reflect fresh names (e.g. `$cndtn_N`) that escape into the output + -- via the returned/prepended statements. Rolling those counters back would + -- let a later `freshTempVar`/`freshTempFor` reuse the same name, producing a + -- duplicate definition in the same scope. + let savedPrepends := (← get).prependedStmts + let savedSubst := (← get).subst modify fun s => { s with prependedStmts := [], subst := []} let result ← runner - modify fun _ => savedState + modify fun s => { s with prependedStmts := savedPrepends, subst := savedSubst } return result /-- From 49c3e1aac6afdb7d868f408fa004d6df13a11fca Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Tue, 23 Jun 2026 20:32:41 +0200 Subject: [PATCH 39/42] Enable calling procedures in contracts (#1352) ## Functional changes 1. [Debugging] Improve the printing of Laurel if-then-else expressions 1. `EliminateReturnsInExpression` now runs for procedures as well, which enables more types of transparent bodies for procedures. To make it work for both functions and procedures, it was also necessary for the body of functions to be immediately wrapped in a return statement during parsing. 1. Allow calling procedures from contracts. Combined with the previous change this makes procedures strictly more powerful than functions 1. Let the transparency pass rewrite the bodies of assume statements so they don't assert anything. 1. Improve diagnostics related to contracts, using the correct verbiage "precondition" and "postcondition" instead of "assertion" 1. Generalized the `LaurelPass` concept so it works for all transformation between Laurel source and Core, not just the Laurel->Laurel transformation. This helps make the documentation more complete. ### Why let the transparency pass rewrite the bodies of assume statements so they don't assert anything? After the contract pass, a call will look like `assert ; call(..); assume `, where the body of the callee looks like `assume ; ; assert `. If we now do either concrete execution, or we do inlining, then any assertions that occur inside the pre or postconditions will be asserted twice, because they occur once in an assert and once in an assume. By ignoring the assertions inside the assume, we prevent the duplication. Whether you also want this behavior for assumptions that were created by users is something I'm not sure about. However, if we want we can let those behave differently. Right now I think we don't have enough data to decide what we want for user created assumptions, and they are AFAIK not yet used, so I think it's OK to change their behavior. ## Implementation Add these passes: - [New] EliminateReturnStatements: rewrite `return` to `exit` statements, needed for the next pass. - [New] ContractPass: translate away pre and postconditions entirely by introducing assertion and assumptions at call sites and at procedure starts and ends - [Updated] Lift assertions, assumptions and procedure calls when they occur in expressions. Note: the changes in this pass could have been extracted to a different PR to reduce the scope of this one, but I think that keeping them in this PR is most efficient from a developer time perspective. ## Follow-up work - Remove the now obsolete functions from Laurel - Create WF proofs for quantifier bodies - Lift assumptions in expressions to axioms. - In the transparency phase, if something has no asserts and only calls functions, only create a function and no procedure --------- Co-authored-by: keyboardDrummer-bot Co-authored-by: Fabio Madge --- .../Languages/Laurel/ConstrainedTypeElim.lean | 8 +- Strata/Languages/Laurel/ContractPass.lean | 461 +++++++++++++++++ .../Laurel/CoreDefinitionsForLaurel.lean | 11 +- .../Laurel/CoreGroupingAndOrdering.lean | 34 +- .../Languages/Laurel/DesugarShortCircuit.lean | 25 +- .../Laurel/EliminateDeterministicHoles.lean | 4 +- Strata/Languages/Laurel/EliminateDoWhile.lean | 4 +- .../Languages/Laurel/EliminateIncrDecr.lean | 4 +- .../Laurel/EliminateReturnStatements.lean | 83 +++ .../Laurel/EliminateValueInReturns.lean | 50 +- Strata/Languages/Laurel/FilterPrelude.lean | 2 - .../AbstractToConcreteTreeTranslator.lean | 9 +- .../ConcreteToAbstractTreeTranslator.lean | 5 + .../Laurel/Grammar/LaurelGrammar.lean | 1 - .../Languages/Laurel/Grammar/LaurelGrammar.st | 4 +- .../Laurel/HeapParameterization.lean | 23 +- .../Laurel/HeapParameterizationConstants.lean | 3 - Strata/Languages/Laurel/InferHoleTypes.lean | 6 +- Strata/Languages/Laurel/LaurelAST.lean | 6 + .../Laurel/LaurelCompilationPipeline.lean | 166 +++--- Strata/Languages/Laurel/LaurelPass.lean | 49 +- ...lator.lean => LaurelToCoreSchemaPass.lean} | 99 ++-- Strata/Languages/Laurel/LaurelTypes.lean | 1 + .../Laurel/LiftImperativeExpressions.lean | 483 +++++++++++------- .../Laurel/LiftInstanceProcedures.lean | 6 +- Strata/Languages/Laurel/MapStmtExpr.lean | 33 +- .../Languages/Laurel/MergeAndLiftReturns.lean | 165 +++--- Strata/Languages/Laurel/ModifiesClauses.lean | 10 +- Strata/Languages/Laurel/PushOldInward.lean | 10 +- Strata/Languages/Laurel/Resolution.lean | 62 ++- Strata/Languages/Laurel/TransparencyPass.lean | 111 ++-- Strata/Languages/Laurel/TypeAliasElim.lean | 4 +- Strata/Languages/Laurel/TypeHierarchy.lean | 6 +- Strata/Languages/Laurel/UnorderedCore.lean | 34 ++ StrataPython/StrataPython/PySpecPipeline.lean | 4 +- StrataPython/StrataPython/PythonToLaurel.lean | 2 +- .../expected_laurel/test_class_empty.expected | 2 +- .../test_class_field_init.expected | 4 +- .../test_class_field_use.expected | 3 +- .../test_class_methods.expected | 14 +- .../test_class_mixed_init.expected | 4 +- .../test_class_no_init.expected | 2 +- .../test_class_no_init_multi_field.expected | 2 +- .../test_class_no_init_with_method.expected | 2 +- .../test_class_with_methods.expected | 12 +- .../expected_laurel/test_deep_inline.expected | 14 +- ...test_havoc_callee_after_hole_call.expected | 3 +- .../test_with_statement.expected | 6 +- .../AnalyzeLaurelTest.lean | 7 +- .../AbstractToConcreteTreeTranslatorTest.lean | 7 +- .../Laurel/ConstrainedTypeElimTest.lean | 15 +- .../Laurel/EliminateDoWhileTest.lean | 9 +- .../Fundamentals/T10_ConstrainedTypes.lean | 10 +- .../Fundamentals/T15_ShortCircuit.lean | 1 - ...ction.lean => T18_RecursiveProcedure.lean} | 15 +- .../Examples/Fundamentals/T19_InvokeOn.lean | 2 +- .../Fundamentals/T20_TransparentBody.lean | 4 +- .../T20_TransparentBodyError.lean | 6 +- .../Fundamentals/T2_ImpureExpressions.lean | 56 +- .../T2_ImpureExpressionsError.lean | 5 +- .../Examples/Fundamentals/T3_ControlFlow.lean | 33 +- .../Fundamentals/T3_ControlFlowError.lean | 6 - .../Fundamentals/T3_ControlFlowError2.lean | 21 + .../Fundamentals/T5_ProcedureCalls.lean | 2 +- .../Examples/Fundamentals/T7_Decreases.lean | 8 +- .../Fundamentals/T8_Postconditions.lean | 17 +- .../T8b_EarlyReturnPostconditions.lean | 2 +- .../Fundamentals/T9_Nondeterministic.lean | 3 + .../Objects/T10_CompositeBvField.lean | 2 +- .../Examples/Objects/T1_MutableFields.lean | 10 +- .../T1b_HeapMutatingValueReturn.lean} | 2 +- .../Examples/Objects/T3_ReadsClauses.lr.st | 3 +- .../Laurel/Examples/Objects/T6_Datatypes.lean | 28 +- .../Examples/Objects/T9_OldHeapTwoState.lean | 4 +- .../Examples/Objects/WIP/5. Allocation.lr.st | 2 + .../Objects/WIP/5. Constructors.lr.st | 1 + .../Objects/WIP/7. InstanceCallables.lr.st | 3 + .../Examples/Objects/WIP/9. Closures.lr.st | 1 + .../Languages/Laurel/IncrDecrLiftTest.lean | 3 +- .../Laurel/LiftExpressionAssignmentsTest.lean | 4 +- .../Languages/Laurel/LiftHolesTest.lean | 16 +- .../LiftImperativeCallsInAssertTest.lean | 23 +- docs/verso/LaurelDoc.lean | 97 ++-- 83 files changed, 1701 insertions(+), 783 deletions(-) create mode 100644 Strata/Languages/Laurel/ContractPass.lean create mode 100644 Strata/Languages/Laurel/EliminateReturnStatements.lean rename Strata/Languages/Laurel/{LaurelToCoreTranslator.lean => LaurelToCoreSchemaPass.lean} (93%) create mode 100644 Strata/Languages/Laurel/UnorderedCore.lean rename StrataTest/Languages/Laurel/Examples/Fundamentals/{T18_RecursiveFunction.lean => T18_RecursiveProcedure.lean} (76%) create mode 100644 StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean rename StrataTest/Languages/Laurel/Examples/{Fundamentals/T8d_HeapMutatingValueReturn.lean => Objects/T1b_HeapMutatingValueReturn.lean} (92%) diff --git a/Strata/Languages/Laurel/ConstrainedTypeElim.lean b/Strata/Languages/Laurel/ConstrainedTypeElim.lean index 7fdaf3f7c6..566dab3ae6 100644 --- a/Strata/Languages/Laurel/ConstrainedTypeElim.lean +++ b/Strata/Languages/Laurel/ConstrainedTypeElim.lean @@ -75,7 +75,7 @@ def constraintCallFor (ptMap : ConstrainedTypeMap) (ty : HighType) For nested types, the function calls the parent's constraint function. -/ def mkConstraintFunc (ptMap : ConstrainedTypeMap) (ct : ConstrainedType) : Procedure := let baseType := resolveType ptMap ct.base - let bodyExpr := match ct.base.val with + let bodyExpr: StmtExprMd := match ct.base.val with | .UserDefined parent => if ptMap.contains parent.text then let paramId := { ct.valueName with uniqueId := none } @@ -89,7 +89,7 @@ def mkConstraintFunc (ptMap : ConstrainedTypeMap) (ct : ConstrainedType) : Proce { name := mkId s!"{ct.name.text}$constraint" inputs := [{ name := ct.valueName, type := baseType }] outputs := [{ name := mkId "result", type := { val := .TBool, source := none } }] - body := .Transparent { val := .Block [bodyExpr] none, source := none } + body := .Transparent { val := .Return bodyExpr, source := none } isFunctional := true decreases := none preconditions := [] } @@ -244,11 +244,11 @@ public def constrainedTypeElim (model : SemanticModel) (program : Program) funcDiags) /-- Pipeline pass: constrained type elimination. -/ -public def constrainedTypeElimPass : LaurelPass where +public def constrainedTypeElimPass : LoweringPass where name := "ConstrainedTypeElim" documentation := "Eliminates constrained types by replacing them with their base types and generating constraint-checking functions and witness procedures. Type tests against constrained types are rewritten to call the generated constraint function." needsResolves := true - run := fun p m => + run := fun p m _ => let (p', diags) := constrainedTypeElim m p (p', diags, {}) diff --git a/Strata/Languages/Laurel/ContractPass.lean b/Strata/Languages/Laurel/ContractPass.lean new file mode 100644 index 0000000000..cc93ba02f9 --- /dev/null +++ b/Strata/Languages/Laurel/ContractPass.lean @@ -0,0 +1,461 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +public import Strata.Languages.Laurel.MapStmtExpr +public import Strata.Languages.Laurel.LaurelPass +import Strata.Languages.Laurel.EliminateReturnStatements +import Strata.Util.Tactics + +/-! +## Contract Pass (Laurel → Laurel) + +Removes pre- and postconditions from all procedures and replaces them with +explicit precondition/postcondition helper procedures, assumptions, and +assertions. + +For each procedure with contracts: +- Generate a separate precondition procedure (`foo$pre0`, `foo$pre1`, ...) for each precondition. +- Generate a separate postcondition procedure (`foo$post0`, `foo$post1`, ...) for each postcondition. + Each takes all inputs and all outputs as parameters and returns the condition. +- Insert `assume foo$pre0(inputs); assume foo$pre1(inputs); ...` at the start of the body. +- Insert `assert foo$post0(inputs, outputs); assert foo$post1(inputs, outputs); ...` at the end of the body. + +For each call to a contracted procedure: +- Assign all input arguments to temporary variables before the call. +- Insert `assert foo$pre0(temps); assert foo$pre1(temps); ...` before the call. +- After the call, insert `assume foo$post0(temps, outputs); assume foo$post1(temps, outputs); ...`. +-/ + +namespace Strata.Laurel + +public section + +private def mkMd (e : StmtExpr) : StmtExprMd := { val := e, source := none } +private def mkVarMd (v : Variable) : VariableMd := { val := v, source := none } + +/-- Name for the i-th precondition helper procedure. -/ +def preCondProcName (procName : String) (i : Nat) : String := s!"{procName}$pre{i}" + +/-- Name for the i-th postcondition helper procedure. -/ +def postCondProcName (procName : String) (i : Nat) : String := s!"{procName}$post{i}" + +/-- Get postconditions from a procedure body. -/ +private def getPostconditions (body : Body) : List Condition := + match body with + | .Opaque postconds _ _ => postconds + | .Abstract postconds => postconds + | _ => [] + +/-- Build a call expression. -/ +private def mkCall (callee : String) (args : List StmtExprMd) : StmtExprMd := + mkMd (.StaticCall (mkId callee) args) + +/-- Convert parameters to identifier expressions. -/ +private def paramsToArgs (params : List Parameter) : List StmtExprMd := + params.map fun p => mkMd (.Var (.Local p.name)) + +/-- Build a helper function for a single condition over the given parameters. + Preconditions pass `proc.inputs`; postconditions use `mkPostConditionProc`. -/ +private def mkConditionProc (name : String) (params : List Parameter) + (condition : Condition) : Procedure := + { name := mkId name + inputs := params + outputs := [⟨mkId "$result", { val := .TBool, source := none }⟩] + preconditions := [] + decreases := none + isFunctional := true + body := .Transparent condition.condition } + +/-- Suffix appended to a procedure's output-parameter names when they are lowered + into a postcondition helper *function*. + + A postcondition helper takes both the procedure's inputs and outputs as plain + function parameters. When an output shares a name with an input (e.g. an inout + `$heap`, which the heap-parameterization pass lists in both `inputs` and + `outputs`), the two would collide in the helper's single parameter scope and + produce "Duplicate definition '…' is already defined in this scope". Suffixing + every output keeps the two distinct. The choice is heap-agnostic: it applies to + all outputs, not just `$heap`. -/ +public def outParamSuffix : String := "$out" + +/-- Rewrite a postcondition body so it refers to the renamed output parameters. + + In a postcondition, a bare reference to an output parameter denotes its + post-state value, while `old(x)` denotes the pre-state value of an inout + parameter (the corresponding *input*). The helper is a pure function with two + distinct parameters, so: + - bare `Var (Local n)` where `n` is an output → suffixed name (`n ++ outParamSuffix`) + - `old(Var (Local n))` → `Var (Local n)`, i.e. strip `old` and keep the + un-suffixed name so it resolves to the input parameter (functions have no + two-state semantics, so an unstripped `old` would later be rejected). + + `pushOldInward` guarantees every `old` immediately wraps a local variable. -/ +private def renameOutputsInPostExpr (outputNames : List String) (expr : StmtExprMd) : StmtExprMd := + let suffixIfOutput (n : Identifier) : Identifier := + if outputNames.contains n.text then mkId (n.text ++ outParamSuffix) else n + mapStmtExprPrePostM (m := Id) + (fun e => + match e.val with + | .Old value => + match value.val with + | .Var (.Local _) => some value + | _ => none + | _ => none) + (fun e => + match e.val with + | .Var (.Local n) => ⟨.Var (.Local (suffixIfOutput n)), e.source⟩ + | _ => e) + expr + +/-- Build a postcondition helper function over the procedure's inputs and outputs. + Output parameters are renamed (see `outParamSuffix`) to avoid colliding with + identically-named inputs, and the condition body is rewritten to match. -/ +private def mkPostConditionProc (name : String) (inputs outputs : List Parameter) + (condition : Condition) : Procedure := + let outputNames := outputs.map (·.name.text) + let renamedOutputs := outputs.map (fun p => { p with name := mkId (p.name.text ++ outParamSuffix) }) + { name := mkId name + inputs := inputs ++ renamedOutputs + outputs := [⟨mkId "$result", { val := .TBool, source := none }⟩] + preconditions := [] + decreases := none + isFunctional := true + body := .Transparent (renameOutputsInPostExpr outputNames condition.condition) } + +/-- Information about a procedure's contracts. -/ +private structure ContractInfo where + preNames : List (String × Option String) -- (procName, summary) for each precondition + postNames : List (String × Option String) -- (procName, summary) for each postcondition + inputParams : List Parameter + outputParams : List Parameter + +private def ContractInfo.hasPreCondition (info : ContractInfo) : Bool := !info.preNames.isEmpty +private def ContractInfo.hasPostCondition (info : ContractInfo) : Bool := !info.postNames.isEmpty + +/-- Collect contract info for all procedures with contracts. -/ +private def collectContractInfo (procs : List Procedure) : Std.HashMap String ContractInfo := + procs.foldl (fun m proc => + let postconds := getPostconditions proc.body + let hasPre := !proc.preconditions.isEmpty + let hasPost := !postconds.isEmpty + if !proc.isFunctional && (hasPre || hasPost) then + let preNames := proc.preconditions.zipIdx.map fun (c, i) => + (preCondProcName proc.name.text i, c.summary) + let postNames := postconds.zipIdx.map fun (c, i) => + (postCondProcName proc.name.text i, c.summary) + m.insert proc.name.text { + preNames := preNames + postNames := postNames + inputParams := proc.inputs + outputParams := proc.outputs + } + else m) {} + +/-- Transform a procedure body to add assume/assert for its own contracts. -/ +private def transformProcBody (proc : Procedure) (info : ContractInfo) : Body := + let inputArgs := paramsToArgs proc.inputs + let postconds := getPostconditions proc.body + let preAssumes : List StmtExprMd := + proc.preconditions.zip info.preNames |>.map fun (pc, name, _) => + ⟨.Assume (mkCall name inputArgs), pc.condition.source⟩ + match proc.body with + | .Transparent body => + let postAsserts : List StmtExprMd := + postconds.zip info.postNames |>.map fun (pc, _name, _summary) => + let summary := pc.summary.getD "postcondition" + ⟨.Assert { condition := pc.condition, summary := some summary }, pc.condition.source⟩ + .Transparent ⟨.Block (preAssumes ++ [body] ++ postAsserts) none, body.source⟩ + | .Opaque _ (some impl) _ => + .Opaque postconds (some ⟨.Block (preAssumes ++ [impl]) none, impl.source⟩) [] + | .Opaque _ none mods => + .Opaque postconds none mods + | .Abstract _ => + .Abstract postconds + | b => b + +/-- Monad used by the contract-pass rewriter; carries a global counter for + generating fresh temporary variable names. -/ +private abbrev ContractM := StateM Nat + +/-- Allocate a fresh temporary name with the `$cp_` prefix. The global counter + guarantees uniqueness across the entire pass. -/ +private def freshTemp : ContractM String := do + let n ← get + set (n + 1) + return s!"$cp_{n}" + +/-- Generate temporary variable assignments for input arguments at a call site. + Returns (temp declarations+assignments, temp variable references). -/ +private def mkTempAssignments (args : List StmtExprMd) + (inputParams : List Parameter) (src : Option FileRange) + : ContractM (List StmtExprMd × List StmtExprMd) := do + let mut decls : List StmtExprMd := [] + let mut refs : List StmtExprMd := [] + for arg in args, i in List.range args.length do + let tempName ← freshTemp + let paramType := match inputParams[i]? with + | some p => p.type + | none => { val := .Unknown, source := none } + let param : Parameter := { name := mkId tempName, type := paramType } + decls := decls ++ [⟨StmtExpr.Assign [mkVarMd (.Declare param)] arg, src⟩] + refs := refs ++ [mkMd (.Var (.Local (mkId tempName)))] + return (decls, refs) + +/-- Generate precondition checks (one per precondition) for a call site. -/ +private def mkPreChecks (info : ContractInfo) (isFunctional : Bool) + (tempRefs : List StmtExprMd) (src : Option FileRange) : List StmtExprMd := + if !info.hasPreCondition then [] + else info.preNames.map fun (name, summary) => + let call := mkCall name tempRefs + if isFunctional then + ⟨.Assume call, src⟩ + else + ⟨.Assert { condition := call, summary := some (summary.getD "precondition") }, src⟩ + +/-- Generate postcondition assumes (one per postcondition) for a call site. -/ +private def mkPostAssumes (info : ContractInfo) + (tempRefs : List StmtExprMd) (outputArgs : List StmtExprMd) (src : Option FileRange) : List StmtExprMd := + if !info.hasPostCondition then [] + else info.postNames.map fun (name, _) => + ⟨.Assume (mkCall name (tempRefs ++ outputArgs)), src⟩ + +/-- Names of the callee's inout parameters: those appearing in both the input and + output lists. By the Laurel inout convention an inout is declared by giving the + input and output the same name, so at a call site the inout argument is the same + variable as the corresponding output target — and the Core lowering relies on + that identity. -/ +private def ContractInfo.inoutNames (info : ContractInfo) : List String := + info.inputParams.filterMap fun p => + if info.outputParams.any (·.name.text == p.name.text) then some p.name.text else none + +/-- Build the positional argument list for the rewritten call. + + Ordinary inputs are passed via their snapshot temp (so pre/postconditions can + reference the pre-call value). Inout inputs, however, are passed as the + *original* argument variable rather than the temp: the call mutates that + variable in place, so it must coincide with the corresponding output target + (the Laurel inout invariant). The temp is still created and used by + `mkPostAssumes` to supply the inout's pre-state to `$post*`. -/ +private def mkCallArgs (info : ContractInfo) (origArgs tempRefs : List StmtExprMd) : List StmtExprMd := + let inout := info.inoutNames + tempRefs.zipIdx.map fun (tempRef, i) => + match info.inputParams[i]? with + | some p => if inout.contains p.name.text then origArgs[i]?.getD tempRef else tempRef + | none => tempRef + +/-- Rewrite call sites in a statement/expression tree. -/ +private def rewriteCallSites (contractInfoMap : Std.HashMap String ContractInfo) + (isFunctional : Bool) (expr : StmtExprMd) : ContractM StmtExprMd := do + let rewriteStaticCall (callee : Identifier) (args : List StmtExprMd) + (info : ContractInfo) (src : Option FileRange) + : ContractM (List StmtExprMd) := do + let (tempDecls, tempRefs) ← mkTempAssignments args info.inputParams src + let preCheck := mkPreChecks info isFunctional tempRefs src + let (callStmt, postAssume, returnValue) ← + if info.hasPostCondition && !info.outputParams.isEmpty then do + let mut outputTempDecls : List VariableMd := [] + let mut outputRefs : List StmtExprMd := [] + for p in info.outputParams do + let tempName ← freshTemp + outputTempDecls := outputTempDecls ++ [mkVarMd (.Declare { name := mkId tempName, type := p.type })] + outputRefs := outputRefs ++ [mkMd (.Var (.Local (mkId tempName)))] + let callWithOutputs : StmtExprMd := + ⟨.Assign outputTempDecls ⟨.StaticCall callee tempRefs, src⟩, src⟩ + let assume := mkPostAssumes info tempRefs outputRefs src + let retVal : List StmtExprMd := match outputRefs with + | [single] => [single] + | _ => [] + pure (callWithOutputs, assume, retVal) + else + pure (⟨.StaticCall callee tempRefs, src⟩, [], []) + return tempDecls ++ preCheck ++ [callStmt] ++ postAssume ++ returnValue + let result ← + mapStmtExprFlattenM (m := ContractM) + -- Pre: intercept Assign targets (StaticCall ...) before recursion + (fun _ e => do + match e.val with + | .Assign targets (.mk (.StaticCall callee args) callSrc) => + match contractInfoMap.get? callee.text with + | some info => + let src := e.source + -- Recurse into arguments + let args' ← args.mapM (mapStmtExprM (m := ContractM) (fun e' => do + match e'.val with + | .StaticCall callee' args' => + match contractInfoMap.get? callee'.text with + | some info' => + let stmts ← rewriteStaticCall callee' args' info' e'.source + return ⟨.Block stmts none, e'.source⟩ + | none => return e' + | _ => return e')) + let (tempDecls, tempRefs) ← mkTempAssignments args' info.inputParams src + let callArgs := mkCallArgs info args' tempRefs + let callWithTemps : StmtExprMd := ⟨.Assign targets ⟨.StaticCall callee callArgs, callSrc⟩, src⟩ + let preCheck := mkPreChecks info isFunctional tempRefs src + let outputArgs := targets.filterMap fun t => + match t.val with + | .Local name => some (mkMd (.Var (.Local name))) + | .Declare param => some (mkMd (.Var (.Local param.name))) + | _ => none + let postAssume := mkPostAssumes info tempRefs outputArgs src + return some (tempDecls ++ preCheck ++ [callWithTemps] ++ postAssume) + | none => return none + | _ => return none) + -- Post: handle bare StaticCall + (fun _ e => do + match e.val with + | .StaticCall callee args => + match contractInfoMap.get? callee.text with + | some info => + let stmts ← rewriteStaticCall callee args info e.source + return stmts + | none => return [e] + | _ => return [e]) true expr + return result + +/-- Rewrite call sites in all bodies of a procedure. -/ +private def rewriteCallSitesInProc (contractInfoMap : Std.HashMap String ContractInfo) + (proc : Procedure) : ContractM Procedure := do + let rw := rewriteCallSites contractInfoMap proc.isFunctional + match proc.body with + | .Transparent body => + let body' ← rw body + return { proc with body := .Transparent body' } + | .Opaque posts impl mods => + let posts' ← posts.mapM (·.mapM rw) + let impl' ← impl.mapM rw + let mods' ← mods.mapM rw + return { proc with body := Body.Opaque posts' impl' mods' } + | _ => return proc + +/-- Conjoin a list of conditions into a single expression with `&&`. -/ +private def conjoin (conds : List Condition) : Option StmtExprMd := + match conds.map (·.condition) with + | [] => none + | e :: rest => some (rest.foldl (fun acc x => mkMd (.PrimitiveOp .And [acc, x])) e) + +/-- Build an axiom expression from `invokeOn` trigger and ensures clauses. + Produces `∀ p1, ∀ p2, ..., ∀ pn :: { trigger } (preconds => ensures)`. + The trigger controls when the SMT solver instantiates the axiom. -/ +private def mkInvokeOnAxiom (params : List Parameter) (trigger : StmtExprMd) + (preconds : List Condition) (postconds : List Condition) : StmtExprMd := + let ensures := (conjoin postconds).getD (mkMd (.LiteralBool true)) + let body := match conjoin preconds with + | some pre => mkMd (.PrimitiveOp .Implies [pre, ensures]) + | none => ensures + -- Wrap in nested Forall from last param (innermost) to first (outermost). + -- The trigger is placed on the innermost quantifier. + params.foldr (init := (body, true)) (fun p (acc, isInnermost) => + let trig := if isInnermost then some trigger else none + (mkMd (.Quantifier .Forall p trig acc), false)) |>.1 + +/-- Check whether a `StmtExprMd` tree mentions a local variable by name. -/ +private def exprMentions (name : String) (expr : StmtExprMd) : Bool := + match expr with + | AstNode.mk val _ => + match val with + | .Var (.Local id) => id.text == name + | .StaticCall _ args => args.attach.any (fun x => exprMentions name x.val) + | .PrimitiveOp _ args _ => args.attach.any (fun x => exprMentions name x.val) + | .IfThenElse c t e => exprMentions name c || exprMentions name t || + match e with | some el => exprMentions name el | none => false + | .Block stmts _ => stmts.attach.any (fun x => exprMentions name x.val) + | .Quantifier _ _ trigger body => exprMentions name body || + match trigger with | some t => exprMentions name t | none => false + | .ReferenceEquals l r => exprMentions name l || exprMentions name r + | .Assign _ v => exprMentions name v + | .Old v => exprMentions name v + | .Fresh v => exprMentions name v + | .Assume c => exprMentions name c + | .Assert c => exprMentions name c.condition + | .Return (some v) => exprMentions name v + | .InstanceCall t _ args => exprMentions name t || args.attach.any (fun x => exprMentions name x.val) + | .AsType t _ => exprMentions name t + | .IsType t _ => exprMentions name t + | .PureFieldUpdate t _ v => exprMentions name t || exprMentions name v + | .ProveBy v p => exprMentions name v || exprMentions name p + | .ContractOf _ f => exprMentions name f + | .Assigned n => exprMentions name n + | _ => false + termination_by expr + decreasing_by + all_goals (try cases x) + all_goals (try simp_all) + all_goals (try have := Condition.sizeOf_condition_lt ‹_›) + all_goals (try term_by_mem) + all_goals omega + +/-- Emit a diagnostic if an `invokeOn` procedure has postconditions referencing + output parameters (which are not quantified in the axiom). -/ +private def invokeOnOutputRefError (proc : Procedure) : Option DiagnosticModel := + if proc.invokeOn.isNone then none else + let postconds := getPostconditions proc.body + let referenced := proc.outputs.filterMap (fun out => + if postconds.any (fun c => exprMentions out.name.text c.condition) + then some out.name.text else none) + match referenced with + | [] => none + | names => some (diagnosticFromSource proc.name.source + s!"'invokeOn' procedure '{proc.name.text}' has an ensures referencing its output(s) ({String.intercalate ", " names}); the auto-invocation axiom is quantified over inputs only." + DiagnosticType.UserError) + +/-- Run the contract pass on a Laurel program. + All procedures with contracts are transformed. -/ +def lowerContracts (program : Program) : Program × List DiagnosticModel := + let contractInfoMap := collectContractInfo program.staticProcedures + + -- Check for output-referencing ensures in invokeOn procedures + let diagnostics := program.staticProcedures.filterMap invokeOnOutputRefError + + -- Generate helper procedures for all procedures with contracts + let helperProcs := (program.staticProcedures.filter (fun proc => !proc.isFunctional)).flatMap fun proc => + let postconds := getPostconditions proc.body + let preProcs := proc.preconditions.zipIdx.map fun (c, i) => + mkConditionProc (preCondProcName proc.name.text i) proc.inputs c + let postProcs := postconds.zipIdx.map fun (c, i) => + mkPostConditionProc (postCondProcName proc.name.text i) proc.inputs proc.outputs c + preProcs ++ postProcs + + -- Transform procedures: strip contracts, add assume/assert, rewrite call sites + -- Run all call-site rewriting in a single ContractM to share the global counter. + let (transformedProcs, _) := (program.staticProcedures.mapM fun (proc : Procedure) => do + if proc.isFunctional then + return proc + else + let proc : Procedure := match proc.invokeOn with + | some trigger => + let postconds := getPostconditions proc.body + if postconds.isEmpty then { proc with invokeOn := none } + else if invokeOnOutputRefError proc |>.isSome then + -- Skip axiom generation; diagnostic already emitted + { proc with invokeOn := none } + else { proc with + axioms := [mkInvokeOnAxiom proc.inputs trigger proc.preconditions postconds] + invokeOn := none } + | none => proc + let proc : Procedure := match contractInfoMap.get? proc.name.text with + | some info => + { proc with + preconditions := [] + body := transformProcBody proc info } + | none => proc + -- Rewrite call sites in the procedure body + rewriteCallSitesInProc contractInfoMap proc).run 0 + + ({ program with staticProcedures := helperProcs ++ transformedProcs }, diagnostics) + +public def contractPass : LoweringPass where + name := "ContractPass" + documentation := "Lowers pre and postcondition to assertions and assumptions around call-sites and procedure bodies" + comesAfter := [⟨ eliminateReturnStatementsPass.meta, "The contract pass wraps the body of procedures to get: `assume preconditions; body; assert postconditions`. Eliminating returns first means that the postcondition assertions are guaranteed to execute."⟩ ] + needsResolves := true + run := fun p _m _ => + let (p', diags) := lowerContracts p + (p', diags, {}) + +end -- public section +end Strata.Laurel diff --git a/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean b/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean index 362d45110d..dfe3e09782 100644 --- a/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean +++ b/Strata/Languages/Laurel/CoreDefinitionsForLaurel.lean @@ -27,16 +27,21 @@ program Laurel; datatype LaurelResolutionErrorPlaceholder {} datatype Float64IsNotSupportedYet {} +datatype LaurelUnit { MkLaurelUnit() } // The types for these Map functions are incorrect. // We'll fix them when Laurel supports polymorphism -function select(map: int, key: int) : int +// And then we can remove the datatype Box as well +// And remove the hacky filter in HeapParameterization +datatype Box { MkBox() } + +function select(map: int, key: int) : Box external; -function update(map: int, key: int, value: int) : int +function update(map: int, key: int, value: int) : Box external; -function const(value: int) : int +function const(value: int) : Box external; #end diff --git a/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean b/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean index 34c526e26d..57007a95a5 100644 --- a/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean +++ b/Strata/Languages/Laurel/CoreGroupingAndOrdering.lean @@ -5,7 +5,9 @@ -/ module -public import Strata.Languages.Laurel.TransparencyPass +public import Strata.Languages.Laurel.LaurelAST +public import Strata.Languages.Laurel.UnorderedCore +public import Strata.Languages.Laurel.LaurelPass import Strata.DL.Lambda.LExpr import StrataDDM.Util.Graph.Tarjan import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator @@ -27,6 +29,7 @@ declarations before they are emitted as Strata Core declarations. namespace Strata.Laurel open Lambda (LMonoTy LExpr) +open Std (Format ToFormat) /-- Collect all `UserDefined` type names referenced in a `HighType`, including nested ones. -/ def collectTypeRefs : HighTypeMd → List String @@ -54,7 +57,7 @@ def collectStaticCallNames (expr : StmtExprMd) : List String := match val with | .StaticCall callee args => callee.text :: args.flatMap (fun a => collectStaticCallNames a) - | .PrimitiveOp _ args => args.flatMap (fun a => collectStaticCallNames a) + | .PrimitiveOp _ args _ => args.flatMap (fun a => collectStaticCallNames a) | .IfThenElse cond t e => collectStaticCallNames cond ++ collectStaticCallNames t ++ @@ -113,18 +116,18 @@ Build the procedure call graph, run Tarjan's SCC algorithm, and return each SCC as a list of procedures paired with a flag indicating whether the SCC is recursive. Results are in reverse topological order: dependencies before dependents. -Procedures with `invokeOn` are placed as early as possible — before +Procedures with axioms are placed as early as possible — before unrelated procedures without them — by stably partitioning them first before building the graph. Tarjan then naturally assigns them lower indices, causing them to appear earlier in the output. -/ public def computeSccDecls (program : UnorderedCoreWithLaurelTypes) : List (List Procedure × Bool) := - -- Stable partition: procedures with invokeOn come first, preserving relative + -- Stable partition: procedures with axioms come first, preserving relative -- order within each group. Tarjan then places them earlier in the topological output. let allProcs := program.functions ++ program.coreProcedures - let (withInvokeOn, withoutInvokeOn) := - allProcs.partition (fun p => p.invokeOn.isSome) - let orderedProcs : List Procedure := withInvokeOn ++ withoutInvokeOn + let (withAxioms, withoutAxioms) := + allProcs.partition (fun p => !p.axioms.isEmpty) + let orderedProcs : List Procedure := withAxioms ++ withoutAxioms -- Build a call-graph over all procedures. -- An edge proc → callee means proc's body/contracts contain a StaticCall to callee. @@ -142,7 +145,8 @@ public def computeSccDecls (program : UnorderedCoreWithLaurelTypes) : List (List | _ => [] let contractExprs : List StmtExprMd := proc.preconditions.map (·.condition) ++ - proc.invokeOn.toList + proc.invokeOn.toList ++ + proc.axioms (bodyExprs ++ contractExprs).flatMap collectStaticCallNames -- Build the OutGraph for Tarjan. @@ -225,7 +229,7 @@ Functions are grouped into SCCs (for mutual recursion). Proofs are emitted as individual `procedure` decls. Both participate in the topological ordering so that axioms are available to functions that need them. -/ -public def orderFunctionsAndProcedures (program : UnorderedCoreWithLaurelTypes) : CoreWithLaurelTypes := +def orderFunctionsAndProcedures (program : UnorderedCoreWithLaurelTypes) : CoreWithLaurelTypes := let datatypeDecls := (groupDatatypesByScc' program).map OrderedDecl.datatypes let constantDecls := program.constants.map OrderedDecl.constant let funcNames : Std.HashSet String := @@ -254,4 +258,16 @@ where let members := comp.toList.filterMap fun idx => dtsArr[idx]? if members.isEmpty then none else some members +public def orderingPass : LaurelPass UnorderedCoreWithLaurelTypes CoreWithLaurelTypes where + name := "OrderingPass" + comesBefore := [] + documentation := "Produce a `CoreWithLaurelTypes` from a `UnorderedCoreWithLaurelTypes` by +computing a combined ordering of functions and proofs using the call graph, +then collecting datatypes and constants. +Functions are grouped into SCCs (for mutual recursion). Proofs are emitted +as individual `procedure` decls. Both participate in the topological ordering +so that axioms are available to functions that need them." + run := fun p _ _ => + (orderFunctionsAndProcedures p, [], {}) + end Strata.Laurel diff --git a/Strata/Languages/Laurel/DesugarShortCircuit.lean b/Strata/Languages/Laurel/DesugarShortCircuit.lean index acd1c02eb4..b8d65c1e40 100644 --- a/Strata/Languages/Laurel/DesugarShortCircuit.lean +++ b/Strata/Languages/Laurel/DesugarShortCircuit.lean @@ -25,11 +25,11 @@ namespace Strata.Laurel public section -private def bare (v : StmtExpr) : StmtExprMd := ⟨v, none⟩ /-- Local rewrite of a single short-circuit node. Recursion is handled by `mapStmtExpr`. -/ -private def desugarShortCircuitNode (model : SemanticModel) (expr : StmtExprMd) : StmtExprMd := +private def desugarShortCircuitNode (imperativeCallees : List String) (expr : StmtExprMd) : StmtExprMd := let source := expr.source + let wrap (v : StmtExpr) : StmtExprMd := ⟨v, source⟩ match expr.val with | .PrimitiveOp op args _ => match op, args with @@ -37,30 +37,31 @@ private def desugarShortCircuitNode (model : SemanticModel) (expr : StmtExprMd) -- short-circuits converted to IfThenElse). The check still works because -- `containsAssignmentOrImperativeCall` recurses into IfThenElse. | .AndThen, [a, b] | .Implies, [a, b] => - if containsAssignmentOrImperativeCall model b then + if containsAssignmentOrImperativeCall imperativeCallees b then let elseVal := match op with | .AndThen => false | _ => true - ⟨.IfThenElse a b (some (bare (.LiteralBool elseVal))), source⟩ + ⟨.IfThenElse a b (some (wrap (.LiteralBool elseVal))), source⟩ else expr | .OrElse, [a, b] => - if containsAssignmentOrImperativeCall model b then - ⟨.IfThenElse a (bare (.LiteralBool true)) (some b), source⟩ + if containsAssignmentOrImperativeCall imperativeCallees b then + ⟨.IfThenElse a (wrap (.LiteralBool true)) (some b), source⟩ else expr | _, _ => expr | _ => expr /-- Desugar short-circuit operators in a program. -/ -def desugarShortCircuit (model : SemanticModel) (program : Program) : Program := - mapProgram (mapStmtExpr (desugarShortCircuitNode model)) program +def desugarShortCircuit (program : Program) : Program := + let imperativeCallees := (program.staticProcedures.filter (!·.isFunctional)).map (·.name.text) + mapProgram (mapStmtExpr (desugarShortCircuitNode imperativeCallees)) program end -- public section /-- Pipeline pass: desugar short-circuit operators. -/ -public def desugarShortCircuitPass : LaurelPass where +public def desugarShortCircuitPass : LoweringPass where name := "DesugarShortCircuit" documentation := "Rewrites short-circuit boolean operators (`&&` and `||`) into equivalent conditional expressions. This simplifies subsequent passes and the final translation to Core, which does not have short-circuit semantics built in." - run := fun p m => - (desugarShortCircuit m p, [], {}) + run := fun p _ _ => + (desugarShortCircuit p, [], {}) comesBefore := [ - ⟨ liftExpressionAssignmentsPass, "The desugar short circuit pass introduces if-then-else expressions whose control-flow must be taken into account by the lifting pass."⟩] + ⟨ liftImperativeExpressionsPass.meta, "The desugar short circuit pass introduces if-then-else expressions whose control-flow must be taken into account by the lifting pass."⟩] end Strata.Laurel diff --git a/Strata/Languages/Laurel/EliminateDeterministicHoles.lean b/Strata/Languages/Laurel/EliminateDeterministicHoles.lean index c1784d41fc..d7286e86c7 100644 --- a/Strata/Languages/Laurel/EliminateDeterministicHoles.lean +++ b/Strata/Languages/Laurel/EliminateDeterministicHoles.lean @@ -90,10 +90,10 @@ def eliminateDeterministicHoles (program : Program) : Program × Statistics := end -- public section /-- Pipeline pass: eliminate deterministic holes. -/ -public def eliminateDeterministicHolesPass : LaurelPass where +public def eliminateDeterministicHolesPass : LoweringPass where name := "EliminateDeterministicHoles" documentation := "Replaces every deterministic hole with a call to a freshly generated uninterpreted function. After this pass the program contains only non-deterministic holes. Assumes `InferHoleTypes` has already annotated holes with types." - run := fun p _m => + run := fun p _m _ => let (p', stats) := eliminateDeterministicHoles p (p', [], stats) diff --git a/Strata/Languages/Laurel/EliminateDoWhile.lean b/Strata/Languages/Laurel/EliminateDoWhile.lean index 0c3539eff4..345b1ad694 100644 --- a/Strata/Languages/Laurel/EliminateDoWhile.lean +++ b/Strata/Languages/Laurel/EliminateDoWhile.lean @@ -73,10 +73,10 @@ def eliminateDoWhile (program : Program) : Program := (mapProgramProceduresM rewrite program |>.run {}).fst /-- Pipeline pass: eliminate post-test (`do … while`) loops. -/ -public def eliminateDoWhilePass : LaurelPass where +public def eliminateDoWhilePass : LoweringPass where name := "EliminateDoWhile" documentation := "Lowers post-test `While` loops (the `do … while` form) into the pre-test loop `{ while(true) invariant I { BODY; if (!COND) exit L } } L`, with a fresh `$`-prefixed exit label `L`. Runs early so no later pass observes a post-test loop; the invariant is checked at the loop head, matching `while`." - run := fun p _m => (eliminateDoWhile p, [], {}) + run := fun p _m _ => (eliminateDoWhile p, [], {}) end -- public section end Strata.Laurel diff --git a/Strata/Languages/Laurel/EliminateIncrDecr.lean b/Strata/Languages/Laurel/EliminateIncrDecr.lean index 85be3c24ed..7f1c06a2a5 100644 --- a/Strata/Languages/Laurel/EliminateIncrDecr.lean +++ b/Strata/Languages/Laurel/EliminateIncrDecr.lean @@ -100,10 +100,10 @@ def eliminateIncrDecr (program : Program) : Program := mapProgramProcedures lowerProcedure program /-- Pipeline pass: eliminate increment/decrement operators. -/ -public def eliminateIncrDecrPass : LaurelPass where +public def eliminateIncrDecrPass : LoweringPass where name := "EliminateIncrDecr" documentation := "Lowers Java-style increment/decrement operators (`++x`, `x++`, `--x`, `x--`) into existing Laurel assignment and arithmetic constructs. Prefix forms yield the new value; postfix forms yield the old value. Runs early so that no later pass observes an `.IncrDecr` node." - run := fun p _m => (eliminateIncrDecr p, [], {}) + run := fun p _m _ => (eliminateIncrDecr p, [], {}) end -- public section end Strata.Laurel diff --git a/Strata/Languages/Laurel/EliminateReturnStatements.lean b/Strata/Languages/Laurel/EliminateReturnStatements.lean new file mode 100644 index 0000000000..d2a7743765 --- /dev/null +++ b/Strata/Languages/Laurel/EliminateReturnStatements.lean @@ -0,0 +1,83 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ +module + +public import Strata.Languages.Laurel.MapStmtExpr +public import Strata.Languages.Laurel.LaurelPass + +/-! +# Eliminate Return Statements + +Replaces `return` statements in imperative procedure bodies with assignments +to the output parameters followed by an `exit` to a labelled block that wraps +the entire body. This ensures that code placed after the body block (e.g., +postcondition assertions inserted by the contract pass) is always reached. + +This pass should run after `EliminateReturnsInExpression` (which handles +functional/expression-position returns) and before the contract pass. +-/ + +namespace Strata.Laurel + +public section + +private def returnLabel : String := "$return" + + + + +/-- Transform a single procedure: wrap body in a labelled block and replace returns. -/ +private def eliminateReturnStmts (proc : Procedure) : Procedure := + match proc.body with + | .Opaque postconds (some impl) mods => + let impl' := replaceReturn proc.outputs impl + let wrapped := match impl'.val with + | .Block stmts none => ⟨.Block stmts (some returnLabel), impl'.source⟩ + | _ => ⟨ .Block [impl'] (some returnLabel), proc.name.source ⟩ + { proc with body := .Opaque postconds (some wrapped) mods } + | .Transparent body => + let body' := replaceReturn proc.outputs body + let wrapped := match body'.val with + | .Block stmts none => ⟨.Block stmts (some returnLabel), body'.source⟩ + | _ => ⟨ .Block [body'] (some returnLabel), proc.name.source ⟩ + { proc with body := .Transparent wrapped } + | _ => proc +where + + /-- Replace `Return val` with `output := val; exit "$return"` (or just `exit` + for valueless returns). Uses `mapStmtExpr` for bottom-up traversal. -/ + replaceReturn (outputs : List Parameter) (expr : StmtExprMd) : StmtExprMd := + mapStmtExpr (fun e => + match e.val with + | .Return (some val) => + /- Handling valued return is required because the heap param pass introduces valued return in + Strata/Languages/Laurel/HeapParameterizationConstants.lean + We should change that so we can remove this case. + -/ + match outputs with + | [out] => + let assign := ⟨ .Assign [⟨ .Local out.name, expr.source ⟩] val, expr.source ⟩ + let exit := ⟨ .Exit returnLabel, expr.source ⟩ + ⟨.Block [assign, exit] none, e.source⟩ + | _ => ⟨ .Exit returnLabel, expr.source ⟩ + | .Return none => ⟨ .Exit returnLabel, expr.source ⟩ + | _ => e) expr + +/-- Transform a program by eliminating return statements in all procedure bodies. -/ +def eliminateReturnStatements (program : Program) : Program := + { program with staticProcedures := program.staticProcedures.map eliminateReturnStmts } + +public def eliminateReturnStatementsPass : LoweringPass where + name := "EliminateReturnStatements" + documentation := "Lower return statements to exit statements. Wrap each procedure body with a 'return' block" + run := fun p _m _ => + let p' := eliminateReturnStatements p + (p', [], {}) + -- comesBefore := [contractPass] + +end -- public section + +end Strata.Laurel diff --git a/Strata/Languages/Laurel/EliminateValueInReturns.lean b/Strata/Languages/Laurel/EliminateValueInReturns.lean index 38ea5f4b99..d209465bf6 100644 --- a/Strata/Languages/Laurel/EliminateValueInReturns.lean +++ b/Strata/Languages/Laurel/EliminateValueInReturns.lean @@ -8,16 +8,13 @@ module public import Strata.Languages.Laurel.LaurelAST public import Strata.Languages.Laurel.LaurelPass import Strata.Languages.Laurel.MapStmtExpr -import Strata.Languages.Laurel.HeapParameterization import Strata.Util.Tactics /-! -# Eliminate Value Returns +# Eliminate Values In Returns Rewrites `return expr` into `outParam := expr; return` for imperative -(non-functional) procedures that have an output parameter. This decouples -the return-value assignment from the `LaurelToCoreTranslator`, which no -longer needs to know about output parameters when translating returns. +(non-functional) procedures that have an output parameter. The pass is a Laurel-to-Laurel rewrite that runs before Core translation. It only applies to static procedures, hence LiftInstanceProcedures must @@ -26,18 +23,19 @@ be executed before it. namespace Strata.Laurel -/-- Rewrite a single `Return (some value)` node into - `Block [Assign [Identifier outParam] value, Return none]`. - Recursion into children is handled by `mapStmtExpr`. -/ -private def eliminateValueReturnNode (outParam : Identifier) (stmt : StmtExprMd) : StmtExprMd := +/-- Rewrite a single `Return (some value)` node into the list + `[Assign outParam value, Return none]`. + When used with `mapStmtExprFlattenM`, these statements are flattened + into the enclosing block rather than wrapped in a nested block. -/ +private def eliminateValueReturnNode (outParam : Identifier) (stmt : StmtExprMd) : Id (List StmtExprMd) := match stmt.val with | .Return (some value) => -- Synthesized nodes use default metadata since no diagnostics should be reported on them - let target : VariableMd := { val := .Local outParam, source := none } - let assign : StmtExprMd := { val := .Assign [target] value, source := none } + let target : VariableMd := { val := .Local outParam, source := stmt.source } + let assign : StmtExprMd := { val := .Assign [target] value, source := stmt.source } let ret : StmtExprMd := { val := .Return none, source := stmt.source } - { val := .Block [assign, ret] none, source := none } - | _ => stmt + [assign, ret] + | _ => [stmt] /-- Check whether a statement tree contains any `Return (some _)`. -/ def hasValuedReturn (stmt : StmtExprMd) : Bool := @@ -56,14 +54,23 @@ def hasValuedReturn (stmt : StmtExprMd) : Bool := all_goals (try term_by_mem) all_goals omega -/-- Apply value-return elimination to a single procedure. Only applies to - non-functional procedures with exactly one output parameter. - Emits an error if a valued return is used with multiple output parameters. -/ +/-- Apply value-return elimination to a single procedure. Rewrites `return expr` + into `outParam := expr; return` for any procedure with exactly one output + parameter (functional and non-functional alike). + Emits an error if a valued return is used with zero or multiple output parameters. -/ def eliminateValueReturnsInProc (proc : Procedure) : Procedure := - if proc.isFunctional then proc - else match proc.outputs with + match proc.outputs with | [outParam] => - let rewrite := mapStmtExpr (eliminateValueReturnNode outParam.name) + let pre (_: Bool) (stmt : StmtExprMd) : Id (Option (List StmtExprMd)) := + match stmt.val with + | .Return (some value) => + let target : VariableMd := { val := .Local outParam.name, source := stmt.source } + let assign : StmtExprMd := { val := .Assign [target] value, source := stmt.source } + let ret : StmtExprMd := { val := .Return none, source := stmt.source } + some [assign, ret] + | _ => none + let post (_: Bool) (stmt : StmtExprMd) : Id (List StmtExprMd) := pure [stmt] + let rewrite := mapStmtExprFlattenM (m := Id) pre post true match proc.body with | .Transparent body => { proc with body := .Transparent (rewrite body) } @@ -85,10 +92,9 @@ def eliminateValueInReturnsTransform (program : Program) : Program := end -- public section /-- Pipeline pass: eliminate value returns. -/ -public def eliminateValueInReturnsPass : LaurelPass where +public def eliminateValueInReturnsPass : LoweringPass where name := "EliminateValueInReturns" documentation := "Rewrites `return expr` into `outParam := expr; return` for imperative procedures that have an output parameter. This decouples the return-value assignment from the final Core translation, which no longer needs to know about output parameters when translating returns." - run := fun p _m => (eliminateValueInReturnsTransform p, [], {}) - comesBefore := [⟨ heapParameterizationPass, "eliminate value in returns need to come before any passes that change the amount of output parameters of procedures." ⟩] + run := fun p _m _ => (eliminateValueInReturnsTransform p, [], {}) end Laurel diff --git a/Strata/Languages/Laurel/FilterPrelude.lean b/Strata/Languages/Laurel/FilterPrelude.lean index 2e6a45ba48..52fd25fcbc 100644 --- a/Strata/Languages/Laurel/FilterPrelude.lean +++ b/Strata/Languages/Laurel/FilterPrelude.lean @@ -221,8 +221,6 @@ private def buildDependencyMap (prog : Laurel.Program) for c in dt.constructors do insertNew c.name.text (deps.insert name) s!"constructor '{c.name.text}' of datatype '{name}'" - insertNew (dt.testerName c) (deps.insert name) - s!"tester '{dt.testerName c}' of datatype '{name}'" for a in c.args do insertNew (dt.destructorName a) (deps.insert name) s!"destructor '{dt.destructorName a}'" diff --git a/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean b/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean index 5ae406e820..145c39694f 100644 --- a/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean +++ b/Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean @@ -256,7 +256,14 @@ private def procedureToOp (proc : Procedure) : StrataDDM.Operation := laurelOp "invokeOnClause" #[stmtExprToArg e]) let (opaqueSpecArg, bodyArg) := match proc.body with | .Transparent body => - (optionArg none, optionArg (some (laurelOp "body" #[stmtExprToArg body]))) + -- For functions, the body is implicitly wrapped in a Return by ConcreteToAbstract; + -- unwrap it here so the concrete output doesn't show an explicit `return`. + let emitBody := if proc.isFunctional then + match body.val with + | .Return (some inner) => inner + | _ => body + else body + (optionArg none, optionArg (some (laurelOp "body" #[stmtExprToArg emitBody]))) | .Opaque postconds impl modifies => let ens := postconds.map ensuresClauseToArg |>.toArray let mods := if modifies.isEmpty then #[] else modifiesClausesToArgs modifies diff --git a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean index 349775d760..bf5e10dd12 100644 --- a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean +++ b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean @@ -568,6 +568,11 @@ def parseProcedure (arg : Arg) : TransM Procedure := do | _, _ => TransM.error s!"Expected body or externalBody operation, got {repr bodyOp.name}" | .option _ none => pure none | _ => TransM.error s!"Expected body, got {repr bodyArg}" + -- For functions, wrap the body in a Return so the last expression + -- is treated as the return value by downstream passes. + let body := if op.name == q`Laurel.function then + body.map fun b => ⟨.Return (some b), b.source⟩ + else body -- Determine procedure body kind let procBody := if isExternal then Body.External diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean index 3eadd5dd90..dc54d7617a 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.lean @@ -9,7 +9,6 @@ module -- 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: fieldAccess prec raised 90 -> 95 (paren-free `c#n++`); shares prec(95) with `call`. - public import StrataDDM.AST import StrataDDM.BuiltinDialects.Init import StrataDDM.Integration.Lean.HashCommands diff --git a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st index 40af1e116c..53f0c33f99 100644 --- a/Strata/Languages/Laurel/Grammar/LaurelGrammar.st +++ b/Strata/Languages/Laurel/Grammar/LaurelGrammar.st @@ -115,10 +115,10 @@ op errorSummary(msg: Str): ErrorSummary => " summary " msg; // If-else category ElseBranch; -op elseBranch(stmts : StmtExpr) : ElseBranch => @[prec(0)] " else " stmts; +op elseBranch(stmts : StmtExpr) : ElseBranch => @[prec(0)] "\nelse " stmts; op ifThenElse (cond: StmtExpr, thenBranch: StmtExpr, elseBranch: Option ElseBranch): StmtExpr => - @[prec(20)] "if " cond " then " thenBranch:0 elseBranch:0; + @[prec(20)] "if " cond "\nthen " thenBranch:0 elseBranch:0; op assert (cond : StmtExpr, errorMessage: Option ErrorSummary) : StmtExpr => @[prec(0)] "assert " cond:0 errorMessage:0; op assume (cond : StmtExpr) : StmtExpr => @[prec(0)] "assume " cond:0; diff --git a/Strata/Languages/Laurel/HeapParameterization.lean b/Strata/Languages/Laurel/HeapParameterization.lean index 06c1e6d04a..4fb4ff74a4 100644 --- a/Strata/Languages/Laurel/HeapParameterization.lean +++ b/Strata/Languages/Laurel/HeapParameterization.lean @@ -12,9 +12,8 @@ import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator import Strata.Languages.Laurel.HeapParameterizationConstants import Strata.Languages.Laurel.LaurelTypes import Strata.Util.Tactics -import Strata.Languages.Laurel.TypeHierarchy -import Strata.Languages.Laurel.ModifiesClauses import Strata.Languages.Laurel.LiftImperativeExpressions +import Strata.Languages.Laurel.EliminateValueInReturns /- Heap Parameterization Pass @@ -248,7 +247,7 @@ def readsHeap (name : Identifier) : TransformM Bool := do def writesHeap (name : Identifier) : TransformM Bool := do return (← get).heapWriters.contains name -def freshVarName : TransformM Identifier := do +private def freshVarName : TransformM Identifier := do let s ← get set { s with freshCounter := s.freshCounter + 1 } return s!"$tmp{s.freshCounter}" @@ -571,21 +570,25 @@ def heapParameterization (model: SemanticModel) (program : Program) : Program := -- Generate Box datatype from all constructors used during transformation let boxDatatype : TypeDefinition := .Datatype { name := "Box", typeArgs := [], constructors := state1.usedBoxConstructors } + + let types := fieldDatatype :: boxDatatype :: heapConstants.types ++ + -- The filter is a hack to deal with another hack, + -- the box that was added in CoreDefinitionsForLaurel.lean + -- because Laurel does not support polymorphism yet + types'.filter (fun td => td.name.text != "Box") { program with staticProcedures := heapConstants.staticProcedures ++ procs', - types := fieldDatatype :: boxDatatype :: heapConstants.types ++ types' } + types } /-- Pipeline pass: heap parameterization. -/ -public def heapParameterizationPass : LaurelPass where +public def heapParameterizationPass : LoweringPass where name := "HeapParameterization" documentation := "Transforms procedures that interact with the heap by adding explicit heap parameters. The heap is modeled as `Map Composite (Map Field Box)`. Procedures that write the heap receive both an input and output heap parameter; procedures that only read the heap receive an input heap parameter. Field reads and writes are rewritten to use `readField` and `updateField` functions." needsResolves := false -- Only resolve again after completing HeapParam, ModifiesClauses and TypeHierarchy. These are logically one pass. - run := fun p m => + run := fun p m _ => (heapParameterization m p, [], {}) - comesBefore := [ - ⟨ typeHierarchyTransformPass, "the type hierarchy pass modifies the 'Composite' datatype that is introduced by this pass." ⟩, - ⟨ modifiesClausesTransformPass, "the modifies pass refers to several types and variables introduced by heap parameterization: Composite, Field, $heap_in, $heap." ⟩, - ⟨ liftExpressionAssignmentsPass, "the heap parameterization pass introduces assignments (to the heap variables) that need to be lifted."⟩] + comesAfter := [⟨ eliminateValueInReturnsPass.meta, "eliminate value in returns need to come before any passes that change the amount of output parameters of procedures." ⟩] + comesBefore := [⟨ liftImperativeExpressionsPass.meta, "the heap parameterization pass introduces assignments (to the heap variables) that need to be lifted."⟩] end Strata.Laurel diff --git a/Strata/Languages/Laurel/HeapParameterizationConstants.lean b/Strata/Languages/Laurel/HeapParameterizationConstants.lean index 7258f29d6a..a9c6e80206 100644 --- a/Strata/Languages/Laurel/HeapParameterizationConstants.lean +++ b/Strata/Languages/Laurel/HeapParameterizationConstants.lean @@ -18,9 +18,6 @@ public section /-- The name of the heap variable used by the heap parameterization pass. -/ def heapVarName : Identifier := "$heap" -/-- The name of the input heap parameter used by the heap parameterization pass. -/ -def heapInVarName : Identifier := "$heap_in" - /-- The Laurel Core prelude defines the heap model types and operations used by the Laurel-to-Core translator. These declarations are expressed diff --git a/Strata/Languages/Laurel/InferHoleTypes.lean b/Strata/Languages/Laurel/InferHoleTypes.lean index 46ecf93701..994627c3e1 100644 --- a/Strata/Languages/Laurel/InferHoleTypes.lean +++ b/Strata/Languages/Laurel/InferHoleTypes.lean @@ -192,13 +192,13 @@ def inferHoleTypes (model : SemanticModel) (program : Program) : Program × List end -- public section /-- Pipeline pass: infer hole types. -/ -public def inferHoleTypesPass : LaurelPass where +public def inferHoleTypesPass : LoweringPass where name := "InferHoleTypes" documentation := "Annotates every verification hole (`.Hole`) in the program with a type inferred from context. This type information is needed by subsequent passes that replace holes with uninterpreted functions or nondeterministic values." - run := fun p m => + run := fun p m _ => let (p', diags, stats) := inferHoleTypes m p (p', diags, stats) comesBefore := [ - ⟨ eliminateDeterministicHolesPass, "eliminating deterministic holes relies on knowing the type of holes"⟩] + ⟨ eliminateDeterministicHolesPass.meta, "eliminating deterministic holes relies on knowing the type of holes"⟩] end Laurel diff --git a/Strata/Languages/Laurel/LaurelAST.lean b/Strata/Languages/Laurel/LaurelAST.lean index d7ba9cd8ca..fb02d7cd59 100644 --- a/Strata/Languages/Laurel/LaurelAST.lean +++ b/Strata/Languages/Laurel/LaurelAST.lean @@ -229,6 +229,9 @@ structure Procedure : Type where whose body is the ensures clause universally quantified over the procedure's inputs, with this expression as the SMT trigger. -/ invokeOn : Option (AstNode StmtExpr) := none + /-- Axioms to emit alongside this procedure. Populated by the contract pass from + `invokeOn` and ensures clauses. -/ + axioms : List (AstNode StmtExpr) := [] /-- A typed parameter for a procedure. @@ -837,6 +840,9 @@ structure ConstrainedType where structure DatatypeConstructor where name : Identifier args : List Parameter + /-- Identifier for the auto-generated tester function (e.g. `IntList..isNil`). + Populated with a `uniqueId` during resolution. -/ + testerName : Identifier := mkId "" /-- A Laurel datatype definition with optional type parameters. Zero constructors produces an opaque (abstract) type in Core. diff --git a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean index d0cfe695fc..298b93f1b6 100644 --- a/Strata/Languages/Laurel/LaurelCompilationPipeline.lean +++ b/Strata/Languages/Laurel/LaurelCompilationPipeline.lean @@ -5,8 +5,9 @@ -/ module -public import Strata.Languages.Laurel.LaurelToCoreTranslator +public import Strata.Languages.Laurel.LaurelToCoreSchemaPass import Strata.Languages.Laurel.DesugarShortCircuit +import Strata.Languages.Laurel.EliminateReturnStatements import Strata.Languages.Laurel.EliminateDoWhile import Strata.Languages.Laurel.EliminateIncrDecr import Strata.Languages.Laurel.MergeAndLiftReturns @@ -17,8 +18,11 @@ import Strata.Languages.Laurel.TypeHierarchy import Strata.Languages.Laurel.InferHoleTypes import Strata.Languages.Laurel.EliminateDeterministicHoles import Strata.Languages.Laurel.CoreDefinitionsForLaurel +import Strata.Languages.Laurel.CoreGroupingAndOrdering +import Strata.Languages.Laurel.TransparencyPass import Strata.Languages.Laurel.LiftImperativeExpressions import Strata.Languages.Laurel.ConstrainedTypeElim +import Strata.Languages.Laurel.ContractPass import Strata.Languages.Laurel.PushOldInward import Strata.Languages.Laurel.LiftInstanceProcedures import Strata.Languages.Laurel.TypeAliasElim @@ -38,7 +42,7 @@ to Strata Core. The pipeline is: 1. Prepend core definitions for Laurel. 2. Run a sequence of Laurel-to-Laurel lowering passes (resolution, heap parameterization, type hierarchy, modifies clauses, hole inference, - desugaring, lifting, constrained type elimination). + desugaring, lifting, constrained type elimination, contract pass). 3. Run the transparency pass to produce an `UnorderedCoreWithLaurelTypes`. 4. Group and order declarations into a `CoreWithLaurelTypes`. 5. Translate the `CoreWithLaurelTypes` to a `Core.Program`. @@ -93,45 +97,26 @@ public section abbrev TranslateResultWithLaurel := (Option Core.Program) × (List DiagnosticModel) × Program × Statistics /-- The ordered sequence of Laurel-to-Laurel lowering passes. -/ -def laurelPipeline : Array LaurelPass := #[ +def laurelPipeline : Array LoweringPass := #[ eliminateDoWhilePass, eliminateIncrDecrPass, typeAliasElimPass, constrainedTypeElimPass, filterNonCompositeModifiesPass, + mergeAndLiftReturnsPass, liftInstanceProceduresPass, eliminateValueInReturnsPass, heapParameterizationPass, typeHierarchyTransformPass, modifiesClausesTransformPass, - { name := "PushOldInward" - documentation := "Distributes `old(...)` over its subexpressions until each `old` immediately wraps an inout variable. Warns on `old(e)` where `e` mentions no inout parameter and on nested `old(old(...))`." - run := fun p _m => - let (p', diags) := pushOldInward p - (p', diags, {}) }, + pushOldInwardPass, inferHoleTypesPass, eliminateDeterministicHolesPass, desugarShortCircuitPass, - liftExpressionAssignmentsPass, - mergeAndLiftReturnsPass + eliminateReturnStatementsPass, + contractPass ] -/-- Every `comesBefore` constraint is respected by the pipeline order. - Checked at elaboration time so that mis-ordered passes are caught immediately. -/ -def comesBeforeRespected : Bool := - let names := laurelPipeline.toList.map (·.name) - (List.range laurelPipeline.size).zip laurelPipeline.toList |>.all fun (i, p) => - p.comesBefore.all fun cb => - match names.findIdx? (· == cb.pass.name) with - | some j => i < j - | none => false -- target not in laurelPipeline - --- Use `initialize` to check at load time instead of `#guard` which requires --- interpreter IR that is not available for passes defined in `module` files. -initialize do - unless comesBeforeRespected do - throw <| .userError "laurelPipeline: comesBefore ordering constraints violated" - /-- Run all Laurel-to-Laurel lowering passes on a program, returning the lowered program, the semantic model, accumulated diagnostics, and merged statistics. @@ -141,6 +126,7 @@ program state after each named Laurel pass is written to `{prefix}.{n}.{passName}.laurel.st`. -/ private def runLaurelPasses + (options: LaurelTranslateOptions) (pctx : Strata.Pipeline.PipelineContext) (program : Program) : PipelineM (Program × SemanticModel × List DiagnosticModel × Statistics) := do let program := { program with @@ -162,7 +148,7 @@ private def runLaurelPasses let mut allStats : Statistics := {} for pass in laurelPipeline do - let (program', diags, stats) ← pctx.withPhase pass.name do pure (pass.run program model) + let (program', diags, stats) ← pctx.withPhase pass.name do pure (pass.run program model options) program := program' allDiags := allDiags ++ diags allStats := allStats.merge stats @@ -174,7 +160,7 @@ private def runLaurelPasses let newDiags := newErrors.toList.map fun d => { d with message := - s!"Internal error: resolution after '{pass.name}' introduced this diagnostic: {d.message}" + s!"Internal error: resolution after '{pass.name}' introduced this diagnostic: {d}. Existing diagnostics were: {resolutionErrors.toList}" type := .StrataBug } emit pass.name "laurel.st" program return (program, model, allDiags ++ newDiags, allStats) @@ -184,6 +170,11 @@ private def runLaurelPasses return (program, model, allDiags, allStats) +/-- The ordered sequence of passes on the unordered Core representation. -/ +private def unorderedCorePipeline : Array (LaurelPass UnorderedCoreWithLaurelTypes UnorderedCoreWithLaurelTypes) := #[ + liftImperativeExpressionsPass +] + /-- Translate Laurel Program to Core Program, also returning the lowered Laurel program. @@ -197,47 +188,55 @@ def translateWithLaurel (options : LaurelTranslateOptions) (program : Program) | some ctx => pure ctx | none => Strata.Pipeline.PipelineContext.create (outputMode := .quiet) runPipelineM options.keepAllFilesPrefix do - let (program, model, passDiags, stats) ← runLaurelPasses pctx program - -- Sanity check: `LiftInstanceProcedures` should have cleared every - -- composite's `instanceProcedures` list. - let mut passDiags := passDiags - for td in program.types do - if let .Composite ct := td then - for proc in ct.instanceProcedures do - passDiags := passDiags ++ [diagnosticFromSource proc.name.source - s!"Instance procedure '{proc.name.text}' on composite type '{ct.name.text}' was not lifted before Core translation (pipeline-ordering bug)" - DiagnosticType.StrataBug] - let unorderedCore := transparencyPass program - emit "transparencyPass" "core.st" unorderedCore - - -- Resolve so that identifiers introduced by earlier passes get uniqueIds. - let compositeTypes := program.types.filter (fun t => match t with | .Composite _ => true | _ => false) - let (unorderedCore, model) := resolveUnorderedCore unorderedCore (existingModel := some model) (additionalTypes := compositeTypes) - - let coreWithLaurelTypes := orderFunctionsAndProcedures unorderedCore - - -- This early return is a simple way to protect against duplicative errors. Without this return, - -- resolution errors reported by Laurel would also be reported by Core. - -- There might be better solution that allows getting some resolution errors from Laurel and some verification errors from Core, - -- but that would need more consideration. - if passDiags.any (·.type != .Warning) then - return (none, passDiags, program, stats) - - emit "CoreWithLaurelTypes" "core.st" coreWithLaurelTypes - let initState : TranslateState := { model := model, overflowChecks := options.overflowChecks } - let (coreProgramOption, translateState) := - runTranslateM initState (translateLaurelToCore options coreWithLaurelTypes) - -- Because of the duplication between functions and procedures, this translation is liable to create duplicate diagnostics - let mut allDiagnostics: List DiagnosticModel := passDiags ++ translateState.diagnostics.eraseDups; - - if !translateState.coreDiagnostics.isEmpty && allDiagnostics.isEmpty then - allDiagnostics := allDiagnostics ++ translateState.coreDiagnostics - - if coreProgramOption.isSome then - emit "Core" "core.st" coreProgramOption.get! - let coreProgramOption := - if translateState.coreDiagnostics.isEmpty then coreProgramOption else none - return (coreProgramOption, allDiagnostics, program, stats) + let (program, model, passDiags, stats) ← runLaurelPasses options pctx program + + -- Sanity check: `LiftInstanceProcedures` should have cleared every + -- composite's `instanceProcedures` list. + let mut passDiags := passDiags + for td in program.types do + if let .Composite ct := td then + for proc in ct.instanceProcedures do + passDiags := passDiags ++ [diagnosticFromSource proc.name.source + s!"Instance procedure '{proc.name.text}' on composite type '{ct.name.text}' was not lifted before Core translation (pipeline-ordering bug)" + DiagnosticType.StrataBug] + + -- This early return is a simple way to protect against duplicative errors. Without this return, + -- resolution errors reported by Laurel would also be reported by Core. + -- There might be better solution that allows getting some resolution errors from Laurel and some verification errors from Core, + -- but that would need more consideration. + if passDiags.any (·.type != .Warning) then + return (none, passDiags, program, stats) + + let unorderedCore := (transparencyPass.run program model options).1 + emit "transparencyPass" "core.st" unorderedCore + let mut unorderedCore := unorderedCore + let mut fnModel := model + + for pass in unorderedCorePipeline do + unorderedCore := (pass.run unorderedCore fnModel options).1 + if pass.needsResolves then + let compositeTypes := program.types.filter (fun t => match t with | .Composite _ => true | _ => false) + let (uc', m', errors) := resolveUnorderedCore unorderedCore (some fnModel) compositeTypes + if !errors.isEmpty then + let newDiags := errors.toList.map fun d => + { d with message := + s!"Internal error: resolution after '{pass.name}' introduced this diagnostic: {d.message}" } + emit pass.name "unorderedCoreWithLaurelTypes.st" unorderedCore + return (none, passDiags ++ newDiags, program, stats) + unorderedCore := uc' + fnModel := m' + emit pass.name "unorderedCoreWithLaurelTypes.st" unorderedCore + + let coreWithLaurelTypes := (orderingPass.run unorderedCore model options).1 + + emit "CoreWithLaurelTypes" "core.st" coreWithLaurelTypes + let (coreProgram, coreDiagnostics, _) := laurelToCoreSchemaPass.run coreWithLaurelTypes fnModel options + let mut allDiagnostics: List DiagnosticModel := passDiags ++ coreDiagnostics; + + emit "Core" "core.st" coreProgram + let coreProgramOption := + if coreDiagnostics.isEmpty then some coreProgram else none + return (coreProgramOption, allDiagnostics, program, stats) /-- Translate Laurel Program to Core Program. @@ -347,4 +346,33 @@ def verifyToDiagnosticModelsCapturing (program : Program) return (translateDiags ++ vcDiags).toArray end -- public section + +public def allPasses: Array PassMeta := laurelPipeline.map (fun p => p.meta) ++ + [transparencyPass.meta] ++ + unorderedCorePipeline.map (fun p => p.meta) ++ + [orderingPass.meta, laurelToCoreSchemaPass.meta] + +/-- Every `comesBefore` and `comesAfter` constraint is respected by the + pipeline order. A `comesBefore` dependency requires this pass to appear + earlier than its target; a `comesAfter` dependency requires it to appear + later. -/ +def orderingRespected : Bool := + let names := allPasses.map (·.name) + (List.range allPasses.size).zip allPasses.toList |>.all fun (i, p) => + (p.comesBefore.all fun cb => + match names.findIdx? (· == cb.pass.name) with + | some j => i < j + | none => false) -- target not in allPasses + && + (p.comesAfter.all fun ca => + match names.findIdx? (· == ca.pass.name) with + | some j => j < i + | none => false) -- target not in allPasses + +-- Use `initialize` to check at load time instead of `#guard` which requires +-- interpreter IR that is not available for passes defined in `module` files. +initialize do + unless orderingRespected do + throw <| .userError "laurelPipeline: comesBefore/comesAfter ordering constraints violated" + end Laurel diff --git a/Strata/Languages/Laurel/LaurelPass.lean b/Strata/Languages/Laurel/LaurelPass.lean index 5a682db402..130ae2cf09 100644 --- a/Strata/Languages/Laurel/LaurelPass.lean +++ b/Strata/Languages/Laurel/LaurelPass.lean @@ -7,31 +7,60 @@ module public import Strata.Languages.Laurel.SemanticModel public import Strata.Util.Statistics +public import Strata.Languages.Core.Options namespace Strata.Laurel public section +structure LaurelTranslateOptions where + inlineFunctionsWhenPossible : Bool := false + overflowChecks : Core.OverflowChecks := {} + keepAllFilesPrefix : Option String := none + +instance : Inhabited LaurelTranslateOptions where + default := {} + mutual -structure ComesBefore where - pass : LaurelPass - reason: String -/-- A single Laurel-to-Laurel pass. Each pass receives the current program and - semantic model and returns the (possibly modified) program, accumulated - diagnostics, and statistics. -/ -structure LaurelPass where +/-- The parameter-free metadata of a pass, independent of the `Input`/`Output` + types it operates on. `LaurelPass` extends this so that passes with + different parameterizations (e.g. `LaurelPass Program Program` and + `LaurelPass UnorderedCoreWithLaurelTypes UnorderedCoreWithLaurelTypes`) + share a common, type-parameter-free view that can be collected into a + single homogeneous list. -/ +structure PassMeta where /-- Human-readable name, used for profiling and file emission. -/ name : String /-- Whether `resolve` should be run after the pass. -/ needsResolves : Bool := false - /-- The pass action. -/ - run : Program → SemanticModel → Program × List DiagnosticModel × Statistics /-- A description of what this pass does, used for documentation generation. -/ documentation : String - comesBefore : List ComesBefore := [] + /-- Passes that must run before this one. -/ + comesBefore : List PassDependency := [] + /-- Passes that must run after this one. -/ + comesAfter : List PassDependency := [] + +structure PassDependency where + pass : PassMeta + reason: String end +/-- A single Laurel-to-Laurel pass. Each pass receives the current program and + semantic model and returns the (possibly modified) program, accumulated + diagnostics, and statistics. Extends `PassMeta` with the `run` action; the + metadata fields remain directly accessible (e.g. `p.name`). -/ +structure LaurelPass (Input: Type) (Output: Type) extends PassMeta where + /-- The pass action. -/ + run : Input → SemanticModel → LaurelTranslateOptions → Output × List DiagnosticModel × Statistics + +abbrev LoweringPass := LaurelPass Laurel.Program Laurel.Program + +/-- Project a `LaurelPass` to its parameter-free metadata, discarding the + `run` action and the `Input`/`Output` type parameters. -/ +abbrev LaurelPass.meta {Input Output : Type} (p : LaurelPass Input Output) : PassMeta := + p.toPassMeta + end -- public section end Strata.Laurel diff --git a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean similarity index 93% rename from Strata/Languages/Laurel/LaurelToCoreTranslator.lean rename to Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean index cbc317c6d8..3c6cda03cd 100644 --- a/Strata/Languages/Laurel/LaurelToCoreTranslator.lean +++ b/Strata/Languages/Laurel/LaurelToCoreSchemaPass.lean @@ -145,10 +145,7 @@ def translateExpr (expr : StmtExprMd) let model := s.model let md := astNodeToCoreMd expr let disallowed (source : Option FileRange) (msg : String) : TranslateM Core.Expression.Expr := do - if isPureContext then throwExprDiagnostic $ diagnosticFromSource source msg - else - throwExprDiagnostic $ diagnosticFromSource source s!"{msg} (should have been lifted)" DiagnosticType.StrataBug match h: expr.val with | .LiteralBool b => return .const () (.boolConst b) @@ -282,9 +279,6 @@ def translateExpr (expr : StmtExprMd) | .Block (⟨ .IfThenElse cond thenBranch (some elseBranch), innerSrc⟩ :: rest) label => disallowed innerSrc "if-then-else only supported as the last statement in a block" - | .IsType _ _ => - throwExprDiagnostic $ diagnosticFromSource expr.source "IsType should have been lowered" DiagnosticType.StrataBug - | .New _ => throwExprDiagnostic $ diagnosticFromSource expr.source s!"New should have been eliminated by typeHierarchyTransform" DiagnosticType.StrataBug | .Var (.Field target fieldId) => -- Field selects should have been eliminated by heap parameterization -- If we see one here, it's an error in the pipeline @@ -298,7 +292,9 @@ def translateExpr (expr : StmtExprMd) | .Block [] _ => throwExprDiagnostic $ diagnosticFromSource expr.source "empty block expression should have been lowered in a separate pass" DiagnosticType.StrataBug | .Return _ => disallowed expr.source "return expression should be lowered in a separate pass" - + | .IsType _ _ => + throwExprDiagnostic $ diagnosticFromSource expr.source "IsType should have been lowered" DiagnosticType.StrataBug + | .New _ => throwExprDiagnostic $ diagnosticFromSource expr.source s!"New should have been eliminated by typeHierarchyTransform" DiagnosticType.StrataBug | .AsType target _ => throwExprDiagnostic $ diagnosticFromSource expr.source "AsType expression translation" DiagnosticType.NotYetImplemented | .Assigned _ => throwExprDiagnostic $ diagnosticFromSource expr.source "assigned expression translation" DiagnosticType.NotYetImplemented | .Old value => @@ -600,12 +596,13 @@ Translate a list of checks (preconditions or postconditions) to Core checks. Each check gets a label like `"requires"` or `"requires_0"`, `"requires_1"`, etc. -/ private def translateChecks (checks : List Condition) (labelBase : String) (overrideFree: Bool) + (defaultSummary : Option String := none) : TranslateM (ListMap Core.CoreLabel Core.Procedure.Check) := checks.mapIdxM (fun i check => do let label := if checks.length == 1 then labelBase else s!"{labelBase}_{i}" let checkExpr ← translateExpr check.condition [] (isPureContext := true) let baseMd := astNodeToCoreMd check.condition - let md := match check.summary with + let md := match check.summary.orElse (fun _ => defaultSummary) with | some msg => baseMd.pushElem Imperative.MetaData.propertySummary (.msg msg) | none => baseMd let attr := if check.free || overrideFree then Core.Procedure.CheckAttr.Free else .Default @@ -659,6 +656,7 @@ def translateProcedure (proc : Procedure) : TranslateM Core.Procedure := do match proc.body with | .Opaque postconds _ _ | .Abstract postconds => translateChecks postconds s!"postcondition" bodyStmts.isNone + (defaultSummary := "postcondition") | _ => pure [] -- Wrap body in a labeled block so early returns (exit) work correctly. -- `bodyLabel` is the shared "$body" constant the resolver pre-registers. @@ -666,50 +664,6 @@ def translateProcedure (proc : Procedure) : TranslateM Core.Procedure := do let spec : Core.Procedure.Spec := { preconditions, postconditions } return { header, spec, body := .structured body } -def translateInvokeOnAxiom (proc : Procedure) (trigger : StmtExprMd) - : TranslateM (Option Core.Decl) := do - let postconds := match proc.body with - | .Opaque postconds _ _ | .Abstract postconds => postconds - | _ => [] - if postconds.isEmpty then return none - -- All input param names become bound variables. - -- buildQuants nests ∀ p1, ∀ p2, ..., ∀ pn :: body, so inside body the innermost - -- binder (pn) is de Bruijn index 0, and the outermost (p1) is index n-1. - -- translateExpr uses findIdx? on boundVars, so we must list params innermost-first - -- (i.e. reversed) so that pn → 0, ..., p1 → n-1. - let boundVars := proc.inputs.reverse.map (·.name) - -- Translate postconditions and trigger with the full bound-var context - let postcondExprs ← postconds.mapM (fun pc => translateExpr pc.condition boundVars (isPureContext := true)) - let bodyExpr : Core.Expression.Expr := match postcondExprs with - | [] => .const () (.boolConst true) - | [e] => e - | e :: rest => rest.foldl (fun acc x => LExpr.mkApp () boolAndOp [acc, x]) e - let triggerExpr ← translateExpr trigger boundVars (isPureContext := true) - -- Wrap in ∀ from outermost (first param) to innermost (last param). - -- The trigger is placed on the innermost quantifier. - let quantified ← buildQuants proc.inputs bodyExpr triggerExpr - return some (.ax { name := s!"invokeOn_{proc.name.text}", e := quantified } (identifierToCoreMd proc.name)) -where - /-- Build `∀ p1 ... pn :: { trigger } body`. The trigger is on the innermost quantifier. -/ - buildQuants (params : List Parameter) - (body : Core.Expression.Expr) (trigger : Core.Expression.Expr) - : TranslateM Core.Expression.Expr := do - match params with - | [] => return body - | [p] => - return LExpr.allTr () p.name.text (some (← translateType p.type)) trigger body - | p :: rest => do - let inner ← buildQuants rest body trigger - return LExpr.all () p.name.text (some (← translateType p.type)) inner - -structure LaurelTranslateOptions where - inlineFunctionsWhenPossible : Bool := false - overflowChecks : Core.OverflowChecks := {} - keepAllFilesPrefix : Option String := none - -instance : Inhabited LaurelTranslateOptions where - default := {} - structure LaurelVerifyOptions where translateOptions : LaurelTranslateOptions := {} verifyOptions : Core.VerifyOptions := .default @@ -717,6 +671,13 @@ structure LaurelVerifyOptions where instance : Inhabited LaurelVerifyOptions where default := {} +/-- Unwrap the pattern produced by EliminateValuesInReturns + EliminateReturnStatements: + `{ result := ; exit "$return" } $return` → `` -/ +private def unwrapReturnBlock (b : StmtExprMd) : StmtExprMd := + match b.val with + | .Block [⟨.Assign [⟨.Local _, _⟩] value, _⟩, ⟨.Exit "$return", _⟩] (some "$return") => value + | _ => b + /-- Translate a Laurel Procedure to a Core Function (when applicable) using `TranslateM`. Diagnostics for disallowed constructs in the function body are emitted into the monad state. @@ -755,10 +716,11 @@ def translateProcedureToFunction (options: LaurelTranslateOptions) (isRecursive: | none => if options.inlineFunctionsWhenPossible then #[.inline] else #[] let body ← match proc.body with - | .Transparent bodyExpr => some <$> translateExpr bodyExpr [] (isPureContext := true) + | .Transparent bodyExpr => + some <$> translateExpr (unwrapReturnBlock bodyExpr) [] (isPureContext := true) | .Opaque _ (some bodyExpr) _ => emitDiagnostic (diagnosticFromSource proc.name.source "functions with postconditions are not yet supported") - some <$> translateExpr bodyExpr [] (isPureContext := true) + some <$> translateExpr (unwrapReturnBlock bodyExpr) [] (isPureContext := true) | _ => pure none let f : Core.Function := { name := ⟨proc.name.text, ()⟩ @@ -816,13 +778,11 @@ def translateLaurelToCore (options: LaurelTranslateOptions) (ordered : CoreWithL return coreFuncs | .procedure proc => do let procDecl ← translateProcedure proc - -- Translate axioms from invokeOn - let invokeOnDecls ← match proc.invokeOn with - | some trigger => do - let axDecl? ← translateInvokeOnAxiom proc trigger - pure axDecl?.toList - | none => pure [] - return [Core.Decl.proc procDecl (identifierToCoreMd proc.name)] ++ invokeOnDecls + -- Translate axioms (populated by the contract pass from invokeOn + ensures) + let axiomDecls ← proc.axioms.mapM fun ax => do + let coreExpr ← translateExpr ax [] (isPureContext := true) + return Core.Decl.ax { name := s!"invokeOn_{proc.name.text}", e := coreExpr } (identifierToCoreMd proc.name) + return [Core.Decl.proc procDecl (identifierToCoreMd proc.name)] ++ axiomDecls | .datatypes dts => do let ldatatypes ← dts.mapM translateDatatypeDefinition return [Core.Decl.type (.data ldatatypes) mdWithUnknownLoc] @@ -839,5 +799,22 @@ def translateLaurelToCore (options: LaurelTranslateOptions) (ordered : CoreWithL pure { decls := coreDecls } +public def laurelToCoreSchemaPass : LaurelPass CoreWithLaurelTypes Core.Program where + name := "LaurelToCoreSchemaPass" + comesBefore := [] + documentation := "Produce a `Core` program from a `CoreWithLaurelTypes` program. Intended to be dumb 1-to-1 translation. However, there are several smart translations still happening: + - The @[cases] parameter is inferred for recursive functions. + - Laurel parameter definitions are translated to Core ones. + - Laurel calling conventions are translated to Core ones." + run := fun p fnModel options => + let initState : TranslateState := { model := fnModel, overflowChecks := options.overflowChecks } + let (coreProgramOption, translateState) := + runTranslateM initState (translateLaurelToCore options p) + let diagnostics : List DiagnosticModel := + -- Because of the duplication between functions and procedures, this translation is liable to create duplicate diagnostics + let d := translateState.diagnostics.eraseDups + if d.isEmpty then translateState.coreDiagnostics else d + (coreProgramOption.getD default, diagnostics, {}) + end -- public section end Laurel diff --git a/Strata/Languages/Laurel/LaurelTypes.lean b/Strata/Languages/Laurel/LaurelTypes.lean index dd29c80bab..d462f5242e 100644 --- a/Strata/Languages/Laurel/LaurelTypes.lean +++ b/Strata/Languages/Laurel/LaurelTypes.lean @@ -129,6 +129,7 @@ triggers heap parameterization. -/ def isHeapRelevantType (ty : HighType) : Bool := (classifyModifiesHighType ty).isSome + end Strata.Laurel end diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index dcd2954729..971bafbf91 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -9,6 +9,7 @@ import Strata.Util.Tactics public import Strata.Languages.Laurel.LaurelPass public import Strata.Languages.Laurel.Resolution import Strata.Languages.Laurel.LaurelTypes +import Strata.Languages.Laurel.TransparencyPass namespace Strata namespace Laurel @@ -80,47 +81,38 @@ structure LiftState where condCounter : Nat := 0 /-- All procedures in the program, used to look up return types of imperative calls -/ procedures : List Procedure := [] + /-- Names of callees whose calls should be treated as imperative (lifted) -/ + imperativeCallees : List String := [] @[expose] abbrev LiftM := StateM LiftState -private def emptyMd : Option String := none - private def freshTempFor (varName : Identifier) : LiftM Identifier := do let counters := (← get).varCounters let counter := counters.find? (·.1 == varName) |>.map (·.2) |>.getD 0 modify fun s => { s with varCounters := (varName, counter + 1) :: s.varCounters.filter (·.1 != varName) } return s!"${varName.text}_{counter}" -private def freshCondVar : LiftM Identifier := do +private def freshTempVar : LiftM Identifier := do let n := (← get).condCounter modify fun s => { s with condCounter := n + 1 } - return s!"$c_{n}" + return s!"$cndtn_{n}" private def prepend (stmt : StmtExprMd) : LiftM Unit := modify fun s => { s with prependedStmts := stmt :: s.prependedStmts } +private def prependList (stmts : List StmtExprMd) : LiftM Unit := + modify fun s => { s with prependedStmts := stmts ++ s.prependedStmts } + private def onlyKeepSideEffectStmtsAndLast (stmts : List StmtExprMd) : LiftM (List StmtExprMd) := do match stmts with | [] => return [] | _ => + -- return stmts let last := stmts.getLast! let nonLast ← stmts.dropLast.flatMapM (fun s => match s.val with | .Var (.Declare ..) | .Assign ([⟨.Declare .., _⟩]) _ => do - -- This addPrepend is a hack to work around Core not having let expressions - -- Otherwise we could keep them in the block - prepend s - pure [] - | .Assert _ => do - -- Hack to work around Core not supporting assert expressions - -- Otherwise we could keep them in the block - prepend s - pure [] - | .Assume _ => do - -- Hack to work around Core not supporting assume expressions - -- Otherwise we could keep them in the block - prepend s - pure [] + pure [s] /- Any other impure StmtExpr, like .Assign, .Exit or .Return, @@ -149,97 +141,104 @@ private def computeType (expr : StmtExprMd) : LiftM HighTypeMd := do let s ← get return computeExprType s.model expr -/-- Check if an expression contains any assignments (recursively). -/ -def containsAssignment (expr : StmtExprMd) : Bool := +/-- Check if an expression contains any assignments or imperative calls +(recursively). When `liftsAssertsAssumes` is set, asserts and assumes also +count — these are lifted into statement position by `transformExpr`, so an +if-then-else whose branch contains one must itself be lifted to keep the +statement guarded by the condition. -/ +def containsAssignmentOrImperativeCall (imperativeCallees : List String) (expr : StmtExprMd) + (liftsAssertsAssumes : Bool := false) : Bool := match expr with | AstNode.mk val _ => match val with | .Assign .. => true | .IncrDecr .. => true - | .StaticCall _ args => args.attach.any (fun x => containsAssignment x.val) - | .PrimitiveOp _ args _ => args.attach.any (fun x => containsAssignment x.val) - | .Block stmts _ => stmts.attach.any (fun x => containsAssignment x.val) + | .StaticCall name args1 => + imperativeCallees.contains name.text || + args1.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) + | .PrimitiveOp _ args2 _ => args2.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) + | .Block stmts _ => stmts.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) | .IfThenElse cond th el => - containsAssignment cond || containsAssignment th || - match el with | some e => containsAssignment e | none => false + containsAssignmentOrImperativeCall imperativeCallees cond liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees th liftsAssertsAssumes || + match el with | some e => containsAssignmentOrImperativeCall imperativeCallees e liftsAssertsAssumes | none => false + | .Assume cond => liftsAssertsAssumes || containsAssignmentOrImperativeCall imperativeCallees cond liftsAssertsAssumes + | .Assert cond => liftsAssertsAssumes || containsAssignmentOrImperativeCall imperativeCallees cond.condition liftsAssertsAssumes + | .InstanceCall target _ args => + containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes || + args.attach.any (fun x => containsAssignmentOrImperativeCall imperativeCallees x.val liftsAssertsAssumes) + | .Quantifier _ _ trigger body => + containsAssignmentOrImperativeCall imperativeCallees body liftsAssertsAssumes || + match trigger with | some t => containsAssignmentOrImperativeCall imperativeCallees t liftsAssertsAssumes | none => false + | .Old value => containsAssignmentOrImperativeCall imperativeCallees value liftsAssertsAssumes + | .Fresh value => containsAssignmentOrImperativeCall imperativeCallees value liftsAssertsAssumes + | .ProveBy value proof => + containsAssignmentOrImperativeCall imperativeCallees value liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees proof liftsAssertsAssumes + | .ReferenceEquals lhs rhs => + containsAssignmentOrImperativeCall imperativeCallees lhs liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees rhs liftsAssertsAssumes + | .PureFieldUpdate target _ newValue => + containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes || + containsAssignmentOrImperativeCall imperativeCallees newValue liftsAssertsAssumes + | .AsType target _ => containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes + | .IsType target _ => containsAssignmentOrImperativeCall imperativeCallees target liftsAssertsAssumes + | .Assigned name => containsAssignmentOrImperativeCall imperativeCallees name liftsAssertsAssumes + | .ContractOf _ func => containsAssignmentOrImperativeCall imperativeCallees func liftsAssertsAssumes + | .Return (some v) => containsAssignmentOrImperativeCall imperativeCallees v liftsAssertsAssumes | _ => false termination_by expr decreasing_by - all_goals ((try cases x); simp_all; try term_by_mem) + all_goals (try cases x) + all_goals (try simp_all) + all_goals (try have := Condition.sizeOf_condition_lt ‹_›) + all_goals (try term_by_mem) + all_goals omega -/-- Like containsAssignment but does NOT recurse into Blocks (treats them as opaque). - Used by assert/assume handlers to allow generated Block wrappers through. -/ -def containsBareAssignment (expr : StmtExprMd) : Bool := - match expr with - | AstNode.mk val _ => - match val with - | .Assign .. => true - | .IncrDecr .. => true - | .StaticCall _ args => args.attach.any (fun x => containsBareAssignment x.val) - | .PrimitiveOp _ args _ => args.attach.any (fun x => containsBareAssignment x.val) - | .Block _ _ => false - | .IfThenElse cond th el => - containsBareAssignment cond || containsBareAssignment th || - match el with | some e => containsBareAssignment e | none => false - | _ => false - termination_by expr - decreasing_by - all_goals ((try cases x); simp_all; try term_by_mem) +mutual -/-- Check if an expression contains any non-functional procedure calls (recursively). -/ -def containsImperativeCall (model : SemanticModel) (expr : StmtExprMd) : Bool := - match expr with - | AstNode.mk val _ => - match val with - | .StaticCall name args => - (match model.get name with - | .staticProcedure proc => !proc.isFunctional - | _ => false) || - args.attach.any (fun x => containsImperativeCall model x.val) - | .PrimitiveOp _ args _ => args.attach.any (fun x => containsImperativeCall model x.val) - | .Block stmts _ => stmts.attach.any (fun x => containsImperativeCall model x.val) - | .IfThenElse cond th el => - containsImperativeCall model cond || - containsImperativeCall model th || - match el with | some e => containsImperativeCall model e | none => false - | _ => false - termination_by expr - decreasing_by - all_goals ((try cases x); simp_all; try term_by_mem) +def asLifted { t: Type } (runner: LiftM t) : LiftM t := do + let savedState ← get + modify fun s => { s with prependedStmts := [], subst := []} + let result ← runner + modify fun _ => savedState + return result -/-- Check if an expression contains any assignments or non-functional procedure calls (recursively). -/ -def containsAssignmentOrImperativeCall (model : SemanticModel) (expr : StmtExprMd) : Bool := - containsAssignment expr || containsImperativeCall model expr +/-- +Process an expression in expression context, traversing arguments right to left. +Assignments are lifted to prependedStmts and replaced with snapshot variable references. +-/ +def transformLiftedExpr (expr : StmtExprMd) : LiftM (List StmtExprMd × StmtExprMd) := do + let savedSubst := (← get).subst + let savedPrepends ← takePrepends + modify fun s => { s with prependedStmts := [], subst := []} + let result ← transformExpr expr + let newPrepends ← takePrepends + modify fun s => { s with prependedStmts := savedPrepends, subst := savedSubst } + return (newPrepends, result) + termination_by (sizeOf expr, 3) /-- -Shared logic for lifting an assignment in expression position: -prepends the assignment, creates before-snapshots for all targets, -and updates substitutions. The value should already be transformed by the caller. +Process an expression in expression context, traversing arguments right to left. +Assignments are lifted to prependedStmts and replaced with snapshot variable references. -/ -private def liftAssignExpr (targets : List VariableMd) (seqValue : StmtExprMd) - (source : Option FileRange) : LiftM Unit := do - -- Prepend the assignment itself - prepend (⟨.Assign targets seqValue, source⟩) - -- Create a before-snapshot for each target and update substitutions - for target in targets do - match target.val with - | .Local varName => - let snapshotName ← freshTempFor varName - let varType ← computeType (⟨ .Var (.Local varName), source⟩) - -- Snapshot goes before the assignment (cons pushes to front) - prepend (⟨.Assign [⟨.Declare ⟨snapshotName, varType⟩, source⟩] (⟨.Var (.Local varName), source⟩), source⟩) - setSubst varName snapshotName - | _ => pure () +def transformLiftedStmt (expr : StmtExprMd) : LiftM Unit := do + let savedSubst := (← get).subst + let previousPrepends := (← get).prependedStmts + modify fun s => { s with subst := [], prependedStmts := [] } + let result ← transformStmt expr + modify fun s => { s with subst := savedSubst, prependedStmts := previousPrepends } + prependList result + termination_by (sizeOf expr, 1) -mutual /-- Process an expression in expression context, traversing arguments right to left. Assignments are lifted to prependedStmts and replaced with snapshot variable references. -/ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do - match expr with + match h_node : expr with | AstNode.mk val source => - match val with + match h_val : val with | .Var (.Local name) => return ⟨.Var (.Local (← getSubst name)), source⟩ @@ -247,7 +246,7 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do | .Hole false (some holeType) => -- Nondeterministic typed hole: lift to a fresh variable with no initializer (havoc) - let holeVar ← freshCondVar + let holeVar ← freshTempVar prepend ⟨ .Var (.Declare ⟨holeVar, holeType⟩), source⟩ return ⟨ .Var (.Local holeVar), source ⟩ @@ -266,14 +265,23 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do if hasSubst then pure (⟨.Var (.Local (← getSubst param.name)), source⟩) else - return expr + pure (⟨.Var (.Local param.name), source⟩) | _ => dbg_trace "Strata bug: non-identifier targets should have been removed before the lift expression phase"; return expr - -- Use the original value (not seqValue) for the prepended assignment, - -- because prepended statements execute in program order and don't need substitutions. - liftAssignExpr targets value source + transformLiftedStmt expr + + -- Create a before-snapshot for each target and update substitutions + for target in targets do + match target.val with + | .Local varName => + let snapshotName ← freshTempFor varName + let varType ← computeType ⟨ .Var (.Local varName), source ⟩ + -- Snapshot goes before the assignment (cons pushes to front) + prepend (⟨.Assign [⟨.Declare ⟨snapshotName, varType⟩, source⟩] (⟨.Var (.Local varName), source⟩), source⟩) + setSubst varName snapshotName + | _ => pure () return resultExpr @@ -283,71 +291,76 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do return ⟨.PrimitiveOp op seqArgs.reverse, source⟩ | .StaticCall callee args => - let model := (← get).model - let seqArgs ← args.reverse.mapM transformExpr - let seqCall := ⟨.StaticCall callee seqArgs.reverse, source⟩ - if model.isFunction callee then + let imperativeCallees := (← get).imperativeCallees + if !imperativeCallees.contains callee.text then + let seqArgs ← args.reverse.mapM transformExpr + let seqCall := ⟨.StaticCall callee seqArgs.reverse, source⟩ return seqCall else - -- Imperative call in expression position: lift to an assignment. - -- Only valid for single-output procedures (or unresolved ones where we - -- fall back to a single target). Multi-output procedures in expression - -- position are a bug in the upstream translation — Resolution should - -- emit a diagnostic for that case. - let outputs := match model.get callee with - | .staticProcedure proc => proc.outputs - | .instanceProcedure _ proc => proc.outputs - | _ => [] - let callResultVar ← freshCondVar - let callResultType ← match outputs with - | [single] => pure single.type - | _ => computeType expr - let liftedCall := [ - ⟨.Var (.Declare ⟨callResultVar, callResultType⟩), source⟩, - ⟨.Assign [⟨.Local callResultVar, source⟩] seqCall, source⟩ - ] - modify fun s => { s with prependedStmts := s.prependedStmts ++ liftedCall} + let callResultVar ← freshTempVar + let callResultType ← computeType expr + + let prepends ← asLifted (transformStmtAssignImperativeCall + [⟨ .Declare ⟨callResultVar, callResultType⟩, source⟩] callee args source source) + prependList prepends return ⟨.Var (.Local callResultVar), source⟩ | .IfThenElse cond thenBranch elseBranch => - let model := (← get).model - let thenHasAssign := containsAssignmentOrImperativeCall model thenBranch + let imperativeCallees := (← get).imperativeCallees + -- A branch must be lifted if it contains anything `transformExpr` would + -- hoist: assignments, imperative calls, asserts, or assumes. (Asserts and + -- assumes matter because hoisting them out of the branch would drop the + -- condition's guard — see `liftsAssertsAssumes`.) + let thenHasAssign := containsAssignmentOrImperativeCall imperativeCallees thenBranch (liftsAssertsAssumes := true) let elseHasAssign := match elseBranch with - | some e => containsAssignmentOrImperativeCall model e + | some e => containsAssignmentOrImperativeCall imperativeCallees e (liftsAssertsAssumes := true) | none => false if thenHasAssign || elseHasAssign then + + -- Infer type from the ORIGINAL then-branch (not the transformed one), + -- because the transformed expression may reference freshly generated + -- variables (e.g. $c_2) that don't exist in the SemanticModel yet. + let condType ← computeType thenBranch + let needsCondVar := condType.val != .TVoid + -- Lift the entire if-then-else. Introduce a fresh variable for the result. - let condVar ← freshCondVar - let seqCond ← transformExpr cond + let condVar ← freshTempVar -- Save outer state let savedSubst := (← get).subst - let savedPrepends := (← get).prependedStmts + let savedPrepends ← takePrepends + + let seqCond ← transformExpr cond + let condPrepends ← takePrepends -- Process then-branch from scratch modify fun s => { s with prependedStmts := [], subst := [] } let seqThen ← transformExpr thenBranch let thenPrepends ← takePrepends - let thenBlock := ⟨.Block (thenPrepends ++ [⟨.Assign [⟨ .Local condVar, source⟩] seqThen, source⟩]) none, source⟩ + let assignStmts := if needsCondVar then [⟨.Assign [⟨ .Local condVar, source⟩] seqThen, source⟩] else [seqThen] + let thenBlock := ⟨.Block (thenPrepends ++ assignStmts) none, source ⟩ -- Process else-branch from scratch modify fun s => { s with prependedStmts := [], subst := [] } let seqElse ← match elseBranch with | some e => do let se ← transformExpr e let elsePrepends ← takePrepends - pure (some ⟨.Block (elsePrepends ++ [⟨.Assign [⟨ .Local condVar, source⟩] se, source⟩]) none, source⟩) + let assignStmts: List StmtExprMd := if needsCondVar then [⟨.Assign [⟨ .Local condVar, source⟩] se, source⟩] else [se]; + pure (some (⟨.Block (elsePrepends ++ assignStmts) none, source ⟩)) | none => pure none -- Restore outer state modify fun s => { s with subst := savedSubst, prependedStmts := savedPrepends } - -- Infer type from the ORIGINAL then-branch (not the transformed one), - -- because the transformed expression may reference freshly generated - -- variables (e.g. $c_2) that don't exist in the SemanticModel yet. - let condType ← computeType thenBranch -- IfThenElse added first (cons puts it deeper), then declaration (cons puts it on top) -- Output order: declaration, then if-then-else prepend (⟨.IfThenElse seqCond thenBlock seqElse, source⟩) - prepend ⟨.Var (.Declare ⟨condVar, condType⟩), source⟩ - return ⟨.Var (.Local condVar), source⟩ + if needsCondVar then + prepend ⟨.Var (.Declare ⟨condVar, condType⟩), source ⟩ + modify fun s => { s with prependedStmts := condPrepends ++ s.prependedStmts } + return ⟨.Var (.Local condVar), source⟩ + else + modify fun s => { s with prependedStmts := condPrepends ++ s.prependedStmts } + -- Unused value + return ⟨ .Hole, expr.source ⟩ else - -- No assignments in branches — recurse normally + -- No liftable statements in branches — recurse normally. let seqCond ← transformExpr cond let seqThen ← transformExpr thenBranch let seqElse ← match elseBranch with @@ -393,26 +406,105 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do else return expr - | .Assert _ => - -- An assert in expression position (e.g. inside a block used as a value) - -- is lifted as a side effect. Prepend it *here*, during the right-to-left - -- traversal, so it keeps its position relative to assignments lifted from - -- the same block. (If it were left for `onlyKeepSideEffectStmtsAndLast` to - -- prepend afterwards, it would be moved ahead of those assignments.) - -- Core has no assert-expression, so the expression yields a dummy value - -- that the surrounding block discards as a non-final statement. - prepend expr - return ⟨.LiteralBool true, source⟩ - - | .Assume _ => - -- See the `.Assert` case above: same side-effect lifting for assumes. - prepend expr - return ⟨.LiteralBool true, source⟩ + | .Assume cond => + let (argPrepends, newCond) ← transformLiftedExpr cond + prepend ⟨ .Assume newCond, source⟩ + prependList argPrepends + default + + | .Assert cond => + let (argPrepends, newCond) ← transformLiftedExpr cond.condition + prepend ⟨ .Assert {cond with condition := newCond}, source⟩ + prependList argPrepends + default + + | .Return (some retExpr) => + let seqRet ← transformExpr retExpr + return ⟨.Return (some seqRet), source⟩ + + | .While cond invs dec body => + let seqCond ← transformExpr cond + let seqInvs ← invs.mapM transformExpr + let seqDec ← match dec with + | some d => pure (some (← transformExpr d)) + | none => pure none + let seqBody ← transformExpr body + return ⟨.While seqCond seqInvs seqDec seqBody, source⟩ + + | .PureFieldUpdate target fieldName newValue => + let seqTarget ← transformExpr target + let seqNewValue ← transformExpr newValue + return ⟨.PureFieldUpdate seqTarget fieldName seqNewValue, source⟩ + + | .ReferenceEquals lhs rhs => + let seqRhs ← transformExpr rhs + let seqLhs ← transformExpr lhs + return ⟨.ReferenceEquals seqLhs seqRhs, source⟩ + + | .AsType target ty => + let seqTarget ← transformExpr target + return ⟨.AsType seqTarget ty, source⟩ + + | .IsType target ty => + let seqTarget ← transformExpr target + return ⟨.IsType seqTarget ty, source⟩ + + | .InstanceCall target callee args => + let seqArgs ← args.reverse.mapM transformExpr + let seqTarget ← transformExpr target + return ⟨.InstanceCall seqTarget callee seqArgs.reverse, source⟩ + + | .Quantifier mode param trigger body => + let seqBody ← transformExpr body + let seqTrigger ← match trigger with + | some t => pure (some (← transformExpr t)) + | none => pure none + return ⟨.Quantifier mode param seqTrigger seqBody, source⟩ + + | .Old value => + let seqValue ← transformExpr value + return ⟨.Old seqValue, source⟩ + + | .Fresh value => + let seqValue ← transformExpr value + return ⟨.Fresh seqValue, source⟩ + + | .Assigned name => + let seqName ← transformExpr name + return ⟨.Assigned seqName, source⟩ + + | .ProveBy value proof => + let seqValue ← transformExpr value + let seqProof ← transformExpr proof + return ⟨.ProveBy seqValue seqProof, source⟩ + + | .ContractOf ty func => + let seqFunc ← transformExpr func + return ⟨.ContractOf ty seqFunc, source⟩ | _ => return expr - termination_by (sizeOf expr, 0) + termination_by (sizeOf expr, 2) + decreasing_by + all_goals first + | (apply Prod.Lex.left; (try have := Condition.sizeOf_condition_lt ‹_›); term_by_mem) + | (apply Prod.Lex.left; simp <;> omega) + | (try subst h_node; try subst h_val; apply Prod.Lex.right; omega) + +def transformStmtAssignImperativeCall + (targets : List (AstNode Variable)) + (callee: Identifier) + (args: List StmtExprMd) + (source: Option FileRange) + (callSource: Option FileRange): LiftM (List StmtExprMd) := do + let seqArgs ← args.reverse.mapM transformExpr + let argPrepends ← takePrepends + modify fun s => { s with subst := [] } + return argPrepends ++ [⟨.Assign targets ⟨.StaticCall callee seqArgs.reverse, callSource⟩, source⟩] + termination_by (sizeOf args, 0) decreasing_by - all_goals (simp_all; try term_by_mem) + all_goals try (apply Prod.Lex.right; omega) + all_goals (try simp_all; try have := Condition.sizeOf_condition_lt ‹_›; try term_by_mem) + all_goals (try (apply Prod.Lex.left); try term_by_mem; try omega) /-- Process a statement, handling any assignments in its sub-expressions. @@ -423,26 +515,24 @@ def transformStmt (stmt : StmtExprMd) : LiftM (List StmtExprMd) := do | AstNode.mk val source => match val with | .Assert cond => - -- Do not transform assert conditions with bare assignments — they are - -- semantic errors that should be rejected downstream. - -- But Blocks with assignments (generated multi-output call wrappers) - -- are handled by the Block case in transformExpr above. - if !containsBareAssignment cond.condition then + -- Do not transform assert conditions with assignments — they must be rejected. + -- But nondeterministic holes need to be lifted. + -- if containsNondetHole cond.condition && !containsAssignmentOrImperativeCall (← get).model cond.condition then let seqCond ← transformExpr cond.condition let prepends ← takePrepends modify fun s => { s with subst := [] } return prepends ++ [⟨.Assert { cond with condition := seqCond }, source⟩] - else - return [stmt] + -- else + -- return [stmt] | .Assume cond => - if !containsBareAssignment cond then + -- if containsNondetHole cond && !containsAssignmentOrImperativeCall (← get).model cond then let seqCond ← transformExpr cond let prepends ← takePrepends modify fun s => { s with subst := [] } return prepends ++ [⟨.Assume seqCond, source⟩] - else - return [stmt] + -- else + -- return [stmt] | .Block stmts metadata => let seqStmts ← stmts.mapM transformStmt @@ -458,17 +548,14 @@ def transformStmt (stmt : StmtExprMd) : LiftM (List StmtExprMd) := do | AstNode.mk value callSource => match _: value with | .StaticCall callee args => - let model := (← get).model - if model.isFunction callee then + let imperativeCallees := (← get).imperativeCallees + if imperativeCallees.contains callee.text then + transformStmtAssignImperativeCall targets callee args source callSource + else let seqValue ← transformExpr valueMd let prepends ← takePrepends modify fun s => { s with subst := [] } return prepends ++ [⟨.Assign targets seqValue, source⟩] - else - let seqArgs ← args.mapM transformExpr - let argPrepends ← takePrepends - modify fun s => { s with subst := [] } - return argPrepends ++ [⟨.Assign targets ⟨.StaticCall callee seqArgs, callSource⟩, source⟩] | _ => let seqValue ← transformExpr valueMd let prepends ← takePrepends @@ -480,11 +567,11 @@ def transformStmt (stmt : StmtExprMd) : LiftM (List StmtExprMd) := do let condPrepends ← takePrepends let seqThen ← do let stmts ← transformStmt thenBranch - pure ⟨.Block stmts none, source⟩ + pure ⟨ .Block stmts none, source ⟩ let seqElse ← match elseBranch with | some e => do let se ← transformStmt e - pure $ some ⟨.Block se none, source⟩ + pure (some (⟨.Block se none, source ⟩)) | none => pure none return condPrepends ++ [⟨.IfThenElse seqCond seqThen seqElse, source⟩] @@ -509,16 +596,16 @@ def transformStmt (stmt : StmtExprMd) : LiftM (List StmtExprMd) := do let prepends ← takePrepends return prepends ++ [⟨.StaticCall name seqArgs, source⟩] - | .PrimitiveOp _ _ => + | .PrimitiveOp _ args => -- A `PrimitiveOp` in statement position. If it carries any side effects -- (an embedded assignment or imperative call — typically the result of -- the postfix increment lowering `(x := x + 1) - 1`), lift them out and -- discard the unused pure result. Otherwise leave the expression -- statement intact so the Core translator can preserve it via -- `exprAsUnusedInit`. - let model := (← get).model - if containsAssignmentOrImperativeCall model stmt then - let _ ← transformExpr stmt + let imperativeCallees := (← get).imperativeCallees + if containsAssignmentOrImperativeCall imperativeCallees stmt then + let _ ← args.reverse.mapM transformExpr let prepends ← takePrepends modify fun s => { s with subst := [] } return prepends @@ -531,28 +618,33 @@ def transformStmt (stmt : StmtExprMd) : LiftM (List StmtExprMd) := do modify fun s => { s with subst := [] } return prepends ++ [⟨.Return (some seqRet), source⟩] + | .PrimitiveOp name args _ => + let seqArgs ← args.mapM transformExpr + let prepends ← takePrepends + return prepends ++ [⟨.PrimitiveOp name seqArgs, source⟩] | _ => return [stmt] termination_by (sizeOf stmt, 0) decreasing_by - all_goals (try term_by_mem) - all_goals (apply Prod.Lex.left; try term_by_mem) + all_goals try (apply Prod.Lex.right; omega) + all_goals (try simp_all; try have := Condition.sizeOf_condition_lt ‹_›; try term_by_mem) + all_goals (try (apply Prod.Lex.left); try term_by_mem; try omega) end -def transformProcedureBody (proc : Procedure) (body : StmtExprMd) : LiftM StmtExprMd := do +def transformProcedureBody (source: Option FileRange) (body : StmtExprMd) : LiftM StmtExprMd := do let stmts ← transformStmt body match stmts with | [single] => pure single - | multiple => pure ⟨.Block multiple none, proc.name.source⟩ + | multiple => pure ⟨.Block multiple none, source ⟩ def transformProcedure (proc : Procedure) : LiftM Procedure := do modify fun s => { s with subst := [], prependedStmts := [], varCounters := [] } match proc.body with | .Transparent bodyExpr => - let seqBody ← transformProcedureBody proc bodyExpr + let seqBody ← transformProcedureBody proc.name.source bodyExpr pure { proc with body := .Transparent seqBody } | .Opaque postconds impl modif => - let impl' ← impl.mapM (transformProcedureBody proc) + let impl' ← impl.mapM (transformProcedureBody proc.name.source) pure { proc with body := .Opaque postconds impl' modif } | .Abstract _ => pure proc @@ -561,19 +653,42 @@ def transformProcedure (proc : Procedure) : LiftM Procedure := do /-- Transform a program to lift all assignments that occur in an expression context. +When `procedureNames` is non-empty, only procedures whose name appears in the +list are transformed; all others are left unchanged. When `procedureNames` is +empty, no procedures are transformed. -/ -def liftExpressionAssignments (model: SemanticModel) (program : Program) : Program := - let initState : LiftState := { model := model } - let (seqProcedures, _) := (program.staticProcedures.mapM transformProcedure).run initState +def liftExpressionAssignments (program : Program) + (model : SemanticModel) (imperativeCallees : List String) : Program := + let initState : LiftState := { model := model, imperativeCallees := imperativeCallees } + let transform := program.staticProcedures.mapM transformProcedure + let (seqProcedures, _) := transform.run initState { program with staticProcedures := seqProcedures } end -- public section -/-- Pipeline pass: lift expression assignments. -/ -public def liftExpressionAssignmentsPass : LaurelPass where - name := "LiftExpressionAssignments" - documentation := "Lifts assignments that appear in expression contexts into preceding statements. This is necessary because Strata Core does not support assignments within expressions. The pass introduces fresh temporary variables where needed." - run := fun p m => - (liftExpressionAssignments m p, [], {}) +/-- +Apply `liftExpressionAssignments` to the core (non-functional) procedures in an +`UnorderedCoreWithLaurelTypes`. Only procedures whose names appear in the core +procedure list are transformed; functions are left unchanged. +-/ +def liftImperativeExpressionsInCore (uc : UnorderedCoreWithLaurelTypes) + (model : SemanticModel) : UnorderedCoreWithLaurelTypes := + let imperativeCallees := uc.coreProcedures.map (·.name.text) + let liftedProgram := liftExpressionAssignments + { staticProcedures := uc.coreProcedures, staticFields := [], types := [], constants := [] } + model imperativeCallees + let liftedProcs := liftedProgram.staticProcedures + { uc with + functions := uc.functions + coreProcedures := liftedProcs + } + +public def liftImperativeExpressionsPass : LaurelPass UnorderedCoreWithLaurelTypes UnorderedCoreWithLaurelTypes where + name := "LiftImperativeExpressionsPass" + documentation := "Lifts assignments and other imperative expressions that appear in expression contexts into preceding statements. This is necessary because Strata Core does not support assignments within expressions. The pass introduces fresh temporary variables where needed." + comesAfter := [⟨ transparencyPass.meta, "The imperative expression lifting is only done in procedures, so it comes after the transparency pass"⟩] + needsResolves := true + run := fun p m _ => + (liftImperativeExpressionsInCore p m, [], {}) end Laurel diff --git a/Strata/Languages/Laurel/LiftInstanceProcedures.lean b/Strata/Languages/Laurel/LiftInstanceProcedures.lean index 2178269623..34ae4366c0 100644 --- a/Strata/Languages/Laurel/LiftInstanceProcedures.lean +++ b/Strata/Languages/Laurel/LiftInstanceProcedures.lean @@ -125,11 +125,11 @@ end -- public section /-- Pipeline pass: lift instance procedures to top-level static procedures and rewrite call sites to use the lifted names. -/ -public def liftInstanceProceduresPass : LaurelPass where +public def liftInstanceProceduresPass : LoweringPass where name := "LiftInstanceProcedures" documentation := "Lifts every procedure declared inside a `composite` block to a top-level static procedure named `$` and rewrites call sites resolved to an instance procedure (including `obj#method(args)` surface syntax) to point at the lifted name. Clears `instanceProcedures` on every composite. Must run before HeapParameterization." needsResolves := true - run := fun p m => (liftInstanceProcedures m p, [], {}) - comesBefore := [⟨ eliminateValueInReturnsPass, "eliminateValueInReturns only applies to static methods, hence all instance methods must have been lifted before." ⟩] + run := fun p m _ => (liftInstanceProcedures m p, [], {}) + comesBefore := [⟨ eliminateValueInReturnsPass.meta, "eliminateValueInReturns only applies to static methods, hence all instance methods must have been lifted before." ⟩] end Strata.Laurel diff --git a/Strata/Languages/Laurel/MapStmtExpr.lean b/Strata/Languages/Laurel/MapStmtExpr.lean index 6d27d37aaf..2ba11ebea7 100644 --- a/Strata/Languages/Laurel/MapStmtExpr.lean +++ b/Strata/Languages/Laurel/MapStmtExpr.lean @@ -248,7 +248,6 @@ children, `pre` is called. If `pre` returns `some result`, that result is used directly (children are NOT recursed into). If `pre` returns `none`, normal bottom-up recursion proceeds and `post` is applied after children are rebuilt. -/ -@[expose] def mapStmtExprPrePostM [Monad m] (pre : StmtExprMd → m (Option StmtExprMd)) (post : StmtExprMd → m StmtExprMd) (expr : StmtExprMd) : m StmtExprMd := do match ← pre expr with @@ -278,16 +277,17 @@ def mapStmtExprPrePostM [Monad m] (pre : StmtExprMd → m (Option StmtExprMd)) pure ⟨.Assign targets' (← mapStmtExprPrePostM pre post value), source⟩ | .Var (.Field target fieldName) => pure ⟨.Var (.Field (← mapStmtExprPrePostM pre post target) fieldName), source⟩ - | .IncrDecr mode op ⟨.Field tgt fieldName, vs⟩ => - pure ⟨.IncrDecr mode op ⟨.Field (← mapStmtExprPrePostM pre post tgt) fieldName, vs⟩, source⟩ - | .IncrDecr _ _ ⟨.Local _, _⟩ | .IncrDecr _ _ ⟨.Declare _, _⟩ => pure expr + | .IncrDecr mode op target => match target with + | ⟨.Field tgt fieldName, vs⟩ => pure ⟨.IncrDecr mode op ⟨.Field (← mapStmtExprPrePostM pre post tgt) fieldName, vs⟩, source⟩ + | ⟨.Local _, _⟩ + | ⟨.Declare _, _⟩ => pure expr + | .PureFieldUpdate target fieldName newValue => - pure ⟨.PureFieldUpdate (← mapStmtExprPrePostM pre post target) fieldName - (← mapStmtExprPrePostM pre post newValue), source⟩ + pure ⟨.PureFieldUpdate (← mapStmtExprPrePostM pre post target) fieldName (← mapStmtExprPrePostM pre post newValue), source⟩ | .StaticCall callee args => pure ⟨.StaticCall callee (← args.attach.mapM fun ⟨e, _⟩ => mapStmtExprPrePostM pre post e), source⟩ - | .PrimitiveOp op args skipProof => - pure ⟨.PrimitiveOp op (← args.attach.mapM fun ⟨e, _⟩ => mapStmtExprPrePostM pre post e) skipProof, source⟩ + | .PrimitiveOp op args skipProofs => + pure ⟨.PrimitiveOp op (← args.attach.mapM fun ⟨e, _⟩ => mapStmtExprPrePostM pre post e) skipProofs, source⟩ | .ReferenceEquals lhs rhs => pure ⟨.ReferenceEquals (← mapStmtExprPrePostM pre post lhs) (← mapStmtExprPrePostM pre post rhs), source⟩ | .AsType target ty => @@ -314,16 +314,15 @@ def mapStmtExprPrePostM [Monad m] (pre : StmtExprMd → m (Option StmtExprMd)) pure ⟨.ProveBy (← mapStmtExprPrePostM pre post value) (← mapStmtExprPrePostM pre post proof), source⟩ | .ContractOf ty func => pure ⟨.ContractOf ty (← mapStmtExprPrePostM pre post func), source⟩ + -- Leaves: no StmtExprMd children. + -- ⚠ If a new StmtExpr constructor with StmtExprMd children is added, + -- it must get its own arm above; otherwise all passes will silently + -- skip recursion into those children. | .Exit _ | .LiteralInt _ | .LiteralBool _ | .LiteralString _ | .LiteralDecimal _ | .LiteralBv _ _ | .Var (.Local _) | .Var (.Declare _) | .New _ | .This | .Abstract | .All | .Hole .. => pure expr post rebuilt termination_by sizeOf expr -decreasing_by - all_goals simp_wf - all_goals (try have := AstNode.sizeOf_val_lt expr) - all_goals (try have := Condition.sizeOf_condition_lt ‹_›) - all_goals (try term_by_mem) - all_goals (cases expr; simp_all; omega) +decreasing_by map_stmt_expr_decreasing expr /-- Apply a monadic transformation to all procedure bodies. -/ @[expose] @@ -336,14 +335,14 @@ def mapProcedureBodiesM [Monad m] (f : StmtExprMd → m StmtExprMd) (proc : Proc | .External => return proc /-- Apply a monadic transformation to all `StmtExprMd` nodes in a procedure - (preconditions, decreases, body, and invokeOn). -/ -@[expose] + (preconditions, decreases, body, invokeOn, and axioms). -/ def mapProcedureM [Monad m] (f : StmtExprMd → m StmtExprMd) (proc : Procedure) : m Procedure := do let proc ← mapProcedureBodiesM f proc return { proc with preconditions := ← proc.preconditions.mapM (·.mapM f) decreases := ← proc.decreases.mapM f - invokeOn := ← proc.invokeOn.mapM f } + invokeOn := ← proc.invokeOn.mapM f + axioms := ← proc.axioms.mapM f } /-- Apply a monadic transformation to every procedure in a program — both top-level static procedures and the instance procedures of composite types. diff --git a/Strata/Languages/Laurel/MergeAndLiftReturns.lean b/Strata/Languages/Laurel/MergeAndLiftReturns.lean index 567dd276fc..06f44102d1 100644 --- a/Strata/Languages/Laurel/MergeAndLiftReturns.lean +++ b/Strata/Languages/Laurel/MergeAndLiftReturns.lean @@ -6,124 +6,95 @@ module public import Strata.Languages.Laurel.LaurelAST +import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator import Strata.Util.Tactics +import Strata.Languages.Laurel.EliminateValueInReturns public import Strata.Languages.Laurel.LaurelPass /-! -# Eliminate Returns in Expression Position -Rewrites functional procedure bodies so that `return` statements are removed -and early-return guard patterns become if-then-else expressions. This makes -the body a pure expression tree suitable for translation to a Core function. - -The algorithm walks a block backwards (from last statement to first), -accumulating a result expression: - -- The last statement is converted via `lastStmtToExpr` which strips `return`, - recurses into blocks, and handles if-then-else. -- Each preceding statement wraps around the accumulated result via `stmtsToExpr`: - - `if (cond) { body }` (no else) becomes `if cond then lastStmtToExpr(body) else acc` - - Other statements are kept in a two-element block with the accumulator. +Given a transparent body, merge the returns into a single outer return. +Emits a diagnostic if it fails at any step. -/ namespace Strata.Laurel -/-- Appending a singleton strictly increases `sizeOf`. -/ -private theorem List.sizeOf_lt_append_singleton [SizeOf α] (xs : List α) (y : α) : - sizeOf xs < sizeOf (xs ++ [y]) := by - induction xs with - | nil => simp_all; omega - | cons hd tl ih => simp_all [List.cons_append] - -/-- `dropLast` of a non-empty list has strictly smaller `sizeOf`. -/ -private theorem List.sizeOf_dropLast_lt [SizeOf α] {l : List α} (h_ne : l ≠ []) : - sizeOf l.dropLast < sizeOf l := by - have h_concat := List.dropLast_concat_getLast h_ne - have : sizeOf l = sizeOf (l.dropLast ++ [l.getLast h_ne]) := by rw [h_concat] - rw [this] - exact List.sizeOf_lt_append_singleton l.dropLast (l.getLast h_ne) - -mutual - -/-- -Fold a list of preceding statements (right-to-left) around an accumulator -expression. Each `if-then` (no else) guard wraps as -`if cond then lastStmtToExpr(body) else acc`; other statements produce -`Block [stmt, acc]`. --/ -def stmtsToExpr (stmts : List StmtExprMd) (acc : StmtExprMd) - : StmtExprMd := - match stmts with - | [] => acc - | s :: rest => - let acc' := stmtsToExpr rest acc - match s with - | ⟨.IfThenElse cond thenBr none, ssrc⟩ => - ⟨.IfThenElse cond (lastStmtToExpr thenBr) (some acc'), ssrc⟩ - | _ => - { val := .Block [s, acc'] none, source := none } - termination_by (sizeOf stmts, 1) - -/-- -Convert the last statement of a block into an expression. -- `return expr` → `expr` -- A non-empty block → process last element, fold preceding statements -- `if cond then A else B` → recurse into both branches -- Anything else → kept as-is --/ -def lastStmtToExpr (stmt : StmtExprMd) : StmtExprMd := - match stmt with - | ⟨.Return (some val), _⟩ => val - | ⟨.Block stmts _, _⟩ => - match h_last : stmts.getLast? with - | some last => - have := List.mem_of_getLast? h_last - let lastExpr := lastStmtToExpr last - let dropped := stmts.dropLast - have h : sizeOf stmts.dropLast < sizeOf stmts := - List.sizeOf_dropLast_lt (by intro h; simp [h] at h_last) - stmtsToExpr dropped lastExpr - | none => stmt - | ⟨.IfThenElse cond thenBr (some elseBr), source⟩ => - ⟨.IfThenElse cond (lastStmtToExpr thenBr) (some (lastStmtToExpr elseBr)), source⟩ - | _ => stmt - termination_by (sizeOf stmt, 0) - decreasing_by - all_goals (simp_all; term_by_mem) - -end - -/-- -Apply return elimination to a functional procedure's body. -The entire body is treated as an expression to be converted. --/ -def eliminateReturnsInExpression (proc : Procedure) : Procedure := - if !proc.isFunctional then proc - else - match proc.body with - | .Transparent bodyExpr => - { proc with body := .Transparent (lastStmtToExpr bodyExpr) } - | .Opaque postconds (some impl) modif => - { proc with body := .Opaque postconds (some (lastStmtToExpr impl)) modif } - | _ => proc +/-- Recurse into a statement, applying the guard-return rewrite inside blocks and branches. + Returns `Except DiagnosticModel StmtExprMd` so that unsupported statement forms produce + a diagnostic instead of panicking. -/ +def removeReturns (stmt : StmtExprMd) : Except DiagnosticModel StmtExprMd := + match _h : stmt.val with + | .Return (some expr) => .ok expr + | .Block [head] _ => removeReturns head + | .Block (head :: tail) label => do + let newTail ← removeReturns ⟨.Block tail none, stmt.source⟩ + let passThrough: StmtExprMd := + let tailList := match newTail.val with + | .Block stmts _ => stmts + | _ => [newTail] + ⟨ .Block (head :: tailList) label, stmt.source ⟩ + match _hhead : head.val with + | .IfThenElse cond thenBr none => + let newThen ← removeReturns thenBr + .ok ⟨ .IfThenElse cond newThen newTail, head.source ⟩ + | .Assign _ _ => .ok passThrough + | .Assume _ => .ok passThrough + | .Assert _ => .ok passThrough + | .Block _ _ => .ok passThrough + | .IfThenElse _ _ (some _) => .error (diagnosticFromSource head.source "in a transparent body, if-then-else is only supported as the last statement in a block") + | _ => .error (diagnosticFromSource head.source + s!"unsupported statement {head.val.constructorName} in block head") + | .IfThenElse cond thenBr (some elseBr) => do + let thenExpr ← removeReturns thenBr + let elseExpr ← removeReturns elseBr + .ok ⟨ .IfThenElse cond thenExpr (some elseExpr), stmt.source⟩ + | _ => .error (diagnosticFromSource stmt.source + s!"ending a transparent body with a {stmt.val.constructorName} statement is not supported") +termination_by sizeOf stmt.val +decreasing_by + all_goals + simp only [_h, StmtExpr.Block.sizeOf_spec, StmtExpr.IfThenElse.sizeOf_spec, + List.cons.sizeOf_spec, List.nil.sizeOf_spec, Option.none.sizeOf_spec, + Option.some.sizeOf_spec] + try have hhd := AstNode.sizeOf_val_lt head + try have htb := AstNode.sizeOf_val_lt thenBr + try have heb := AstNode.sizeOf_val_lt elseBr + try simp only [_hhead, StmtExpr.IfThenElse.sizeOf_spec, Option.none.sizeOf_spec] at hhd + omega + +/-- Transform a single procedure by applying the guard-return elimination to its body. + Returns the procedure and any diagnostic emitted on failure. -/ +private def eliminateReturnsInExpression (proc : Procedure) : Procedure × List DiagnosticModel := + match proc.body with + | .Transparent body => + match removeReturns body with + | .ok result => ({ proc with body := .Transparent ⟨.Return result, body.source ⟩ }, []) + | .error diag => (proc, [diag]) + | _ => (proc, []) public section /-- -Transform a program by eliminating returns in all functional procedure bodies. +Transform a program by eliminating returns in all procedure bodies. -/ -def mergeAndLiftReturns (program : Program) : Program := - { program with staticProcedures := program.staticProcedures.map eliminateReturnsInExpression } +def mergeAndLiftReturns (program : Program) : Program × List DiagnosticModel := + let (procs, diags) := program.staticProcedures.foldl (fun (ps, ds) proc => + let (proc', procDiags) := eliminateReturnsInExpression proc + (proc' :: ps, ds ++ procDiags) + ) ([], []) + ({ program with staticProcedures := procs.reverse }, diags) end -- public section /-- Pipeline pass: merge and lift returns. -/ -public def mergeAndLiftReturnsPass : LaurelPass where +public def mergeAndLiftReturnsPass : LoweringPass where name := "MergeAndLiftReturns" documentation := "Attempts to merge and lift returns so that only a single outer return remains, enabling the procedure to be more easily converted to a functional form." needsResolves := true - run := fun p _m => - (mergeAndLiftReturns p, [], {}) + comesBefore := [⟨ eliminateValueInReturnsPass.meta, "Lifts returns with a value, so the value should not yet have been lowered."⟩] + run := fun p _m _ => + let (p', diags) := mergeAndLiftReturns p + (p', diags, {}) end Laurel diff --git a/Strata/Languages/Laurel/ModifiesClauses.lean b/Strata/Languages/Laurel/ModifiesClauses.lean index b5045ff97a..dbe7eef50f 100644 --- a/Strata/Languages/Laurel/ModifiesClauses.lean +++ b/Strata/Languages/Laurel/ModifiesClauses.lean @@ -8,6 +8,7 @@ module public import Strata.Languages.Laurel.Resolution public import Strata.Languages.Laurel.LaurelPass import Strata.Languages.Laurel.HeapParameterizationConstants +import Strata.Languages.Laurel.HeapParameterization import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator import Strata.Languages.Laurel.LaurelTypes @@ -233,19 +234,20 @@ def modifiesClausesTransform (model: SemanticModel) (program : Program) : Progra end -- public section /-- Pipeline pass: filter non-composite modifies clauses. -/ -public def filterNonCompositeModifiesPass : LaurelPass where +public def filterNonCompositeModifiesPass : LoweringPass where name := "FilterNonCompositeModifies" documentation := "Filters modifies clauses that refer to non-composite types (e.g. primitives), which cannot be heap-parameterized. Emits a warning for each removed clause. Should run before heap parameterization so that phase remains agnostic to modifies clauses." - run := fun p m => + run := fun p m _ => let (p', diags) := filterNonCompositeModifies m p (p', diags, {}) /-- Pipeline pass: translate modifies clauses into ensures clauses. -/ -public def modifiesClausesTransformPass : LaurelPass where +public def modifiesClausesTransformPass : LoweringPass where name := "ModifiesClausesTransform" documentation := "Translates modifies clauses into additional ensures clauses. The modifies clause of a procedure is translated into a quantified assertion that states objects not mentioned in the modifies clause have their field values preserved between the input and output heap." needsResolves := true - run := fun p m => + comesAfter := [⟨ heapParameterizationPass.meta, "the modifies pass refers to several types and variables introduced by heap parameterization: Composite, Field, $heap_in, $heap."⟩] + run := fun p m _ => let (p', diags) := modifiesClausesTransform m p (p', diags, {}) diff --git a/Strata/Languages/Laurel/PushOldInward.lean b/Strata/Languages/Laurel/PushOldInward.lean index 6fd6119023..c9db47123a 100644 --- a/Strata/Languages/Laurel/PushOldInward.lean +++ b/Strata/Languages/Laurel/PushOldInward.lean @@ -6,6 +6,7 @@ module public import Strata.Languages.Laurel.MapStmtExpr +public import Strata.Languages.Laurel.LaurelPass /-! # Push `Old` Inward @@ -70,12 +71,19 @@ def transformProcedurePushOld (proc : Procedure) : PushOldM Procedure := do /-- Push every `StmtExpr.Old` inward until it immediately wraps an inout variable. Returns the rewritten program and any warnings emitted. -/ -@[expose] def pushOldInward (program : Program) : Program × List DiagnosticModel := let (program', finalState) := (program.staticProcedures.mapM transformProcedurePushOld).run {} ({ program with staticProcedures := program' }, finalState.diagnostics) +/-- Pipeline pass: translate modifies clauses into ensures clauses. -/ +public def pushOldInwardPass : LoweringPass where + name := "pushOldInward" + documentation := "Distributes `old(...)` over its subexpressions until each `old` immediately wraps an inout variable. Warns on `old(e)` where `e` mentions no inout parameter and on nested `old(old(...))`." + run := fun p _ _ => + let (p', diags) := pushOldInward p + (p', diags, {}) + end -- public section end Laurel end Strata diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 96af7cab53..a3e3e5090d 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -6,8 +6,8 @@ module public import Strata.Languages.Laurel.LaurelAST +public import Strata.Languages.Laurel.UnorderedCore public import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator -public import Strata.Languages.Laurel.TransparencyPass import Strata.Util.Tactics public import Strata.Languages.Laurel.SemanticModel public import Strata.Languages.Laurel.LaurelTypes @@ -2579,8 +2579,8 @@ def resolveOutputParameter (inputNames : List String) (param : Parameter) : Reso else resolveParameter param -/-- Resolve a procedure body by synthesizing its impl block (if any). - Bodies without an impl block (`Abstract`, `External`) resolve +/-- Resolve a procedure body by synthesizing its body (if any). + Bodies without an body (`Abstract`, `External`) resolve postconditions only. -/ def resolveBody (body : Body) : ResolveM Body := do match body with @@ -2632,10 +2632,12 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do -- (e.g. destructive assignments) inside a transparent body. So there is -- no transparent-body rejection here, unlike `resolveInstanceProcedure`. let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr + let axioms' ← proc.axioms.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', invokeOn := invokeOn', + axioms := axioms', body := body' } /-- Resolve a field: define its name under the qualified key (OwnerType.fieldName) and resolve its type. -/ @@ -2667,16 +2669,14 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv -- See `resolveProcedure` for the rationale on `bodyLabel`. let body' ← withLabel (some bodyLabel) <| resolveBody proc.body modify fun s => { s with answerType := savedAnswer } - if !proc.isFunctional && body'.isTransparent && !proc.name.text.any (· == '$') then - let diag := diagnosticFromSource proc.name.source - s!"transparent statement bodies are not supported. Add 'opaque' to make the procedure opaque" - modify fun s => { s with errors := s.errors.push diag } let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr modify fun s => { s with instanceTypeName := savedInstType } + let axioms' ← proc.axioms.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', invokeOn := invokeOn', + axioms := axioms', body := body' } /-- Resolve a type definition. -/ @@ -2730,7 +2730,12 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do -- parameter's own name should stay unqualified. let destructorId := { p.name with uniqueId := resolved.uniqueId } return ⟨ destructorId, ty' ⟩ - return { name := ctorName', args := args' : DatatypeConstructor } + -- Resolve the tester name so its uniqueId is set. + let testerResolved ← resolveRef (dt.testerName ctor) + let testerName' := { ctor.testerName with + text := testerResolved.text + uniqueId := testerResolved.uniqueId } + return { name := ctorName', args := args', testerName := testerName' : DatatypeConstructor } return .Datatype { name := dtName', typeArgs := dt.typeArgs, constructors := ctors' } | .Alias ta => let target' ← resolveHighType ta.target @@ -2746,6 +2751,28 @@ def resolveConstant (c : Constant) : ResolveM Constant := do /-! ## Phase 2: Build refToDef map from the resolved program -/ +/-- Generate a virtual tester procedure for a single constructor of a datatype. + The tester takes a single argument of the datatype's type and returns `bool`. + Used during resolution to synthesize the scope entry for tester calls + (e.g. `IntList..isNil(x)`) without requiring a separate AST pass. -/ +private def mkTesterProcedure (dt : DatatypeDefinition) (ctor : DatatypeConstructor) : Procedure := + let tName := dt.testerName ctor + let inputParam : Parameter := { + name := mkId "value" + type := { val := .UserDefined dt.name, source := none } + } + let outputParam : Parameter := { + name := mkId "$result" + type := { val := .TBool, source := none } + } + { name := mkId tName + inputs := [inputParam] + outputs := [outputParam] + preconditions := [] + decreases := none + isFunctional := true + body := .External } + /-- Insert a definition into the refToDef map using the ID already on the identifier. -/ private def register (map : Std.HashMap Nat ResolvedNode) (iden : Identifier) (node : ResolvedNode) : Std.HashMap Nat ResolvedNode := @@ -2883,6 +2910,9 @@ private def collectTypeDefinition (map : Std.HashMap Nat ResolvedNode) (td : Typ let map := register map dt.name (.datatypeDefinition dt) dt.constructors.foldl (fun map ctor => let map := register map ctor.name (.datatypeConstructor dt.name ctor) + -- Register the tester function in the refToDef map. + let testerProc := mkTesterProcedure dt ctor + let map := register map ctor.testerName (.staticProcedure testerProc) ctor.args.foldl (fun map p => -- The constructor parameter's `uniqueId` (set by `resolveTypeDefinition`) -- is the shared uniqueId of the safe/unsafe destructor scope entries, @@ -3063,13 +3093,11 @@ private def preRegisterTopLevel (program : Program) : ResolveM Unit := do | .Datatype dt => let _ ← defineNameCheckDup dt.name (.datatypeDefinition dt) for ctor in dt.constructors do - -- Register the tester override first; the second call reuses the - -- returned Identifier (now carrying a uniqueId) so the unprefixed - -- constructor name and the `TypeName..isCtor` tester name resolve to - -- the same uniqueId, which `buildRefToDef` in turn maps to - -- `.datatypeConstructor`. - let ctorName ← defineNameCheckDup ctor.name (.datatypeConstructor dt.name ctor) (some (dt.testerName ctor)) - let _ ← defineNameCheckDup ctorName (.datatypeConstructor dt.name ctor) + let _ ← defineNameCheckDup ctor.name (.datatypeConstructor dt.name ctor) + -- Register the tester function (e.g. `IntList..isNil`) as a static procedure. + let testerProc := mkTesterProcedure dt ctor + let _ ← defineNameCheckDup (mkId (dt.testerName ctor)) + (.staticProcedure testerProc) (some (dt.testerName ctor)) for p in ctor.args do -- Same chaining trick for the safe and unsafe destructor names: both -- point to the same uniqueId so `IntList..head` and `IntList..head!` @@ -3155,10 +3183,10 @@ but they are because certain type references have incorrectly not been updated. public def resolveUnorderedCore (uc : UnorderedCoreWithLaurelTypes) (existingModel : Option SemanticModel := none) (additionalTypes : List TypeDefinition := []) - : UnorderedCoreWithLaurelTypes × SemanticModel := + : UnorderedCoreWithLaurelTypes × SemanticModel × Array DiagnosticModel := let fnProgram := unorderedCoreToProgram uc additionalTypes let fnResolveResult := resolve fnProgram existingModel - (fromResolvedProgram fnResolveResult.program, fnResolveResult.model) + (fromResolvedProgram fnResolveResult.program, fnResolveResult.model, fnResolveResult.errors) end -- public section end Strata.Laurel diff --git a/Strata/Languages/Laurel/TransparencyPass.lean b/Strata/Languages/Laurel/TransparencyPass.lean index b4b59dcec4..c44751ff96 100644 --- a/Strata/Languages/Laurel/TransparencyPass.lean +++ b/Strata/Languages/Laurel/TransparencyPass.lean @@ -7,6 +7,8 @@ module public import Strata.Languages.Laurel.MapStmtExpr public import Strata.Languages.Laurel.LaurelAST +public import Strata.Languages.Laurel.LaurelPass +public import Strata.Languages.Laurel.CoreGroupingAndOrdering import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator import Strata.DL.Lambda.TypeFactory @@ -28,18 +30,6 @@ namespace Strata.Laurel public section -/-- -An intermediate representation produced by the transparency pass. -Functions are pure computational procedures (suffixed `$asFunction`); -coreProcedures are the original procedures with any free postconditions -embedded in their `Body.Opaque` postcondition lists. --/ -public structure UnorderedCoreWithLaurelTypes where - functions : List Procedure - coreProcedures : List Procedure - datatypes : List DatatypeDefinition - constants : List Constant - /-- Deep traversal that strips all Assert and Assume nodes from a StmtExpr tree. Assert/Assume nodes are replaced with `LiteralBool true`, and Block nodes are collapsed by filtering out trivial `LiteralBool true` leftovers. -/ @@ -84,6 +74,35 @@ private def rewriteCallsToFunctional (asFunctionNames : Std.HashSet String) (exp | .PrimitiveOp operator arguments _ => ⟨ .PrimitiveOp operator arguments true, e.source⟩ | _ => e) expr +/-- Rewrite quantifier bodies like function bodies: strip assert/assume and + rewrite calls to their `$asFunction` variants. This ensures that calls + inside quantifiers (e.g. in modifies frame conditions) reference the + pure functional version and are not treated as imperative by later passes. -/ +private def rewriteQuantifierBodies (nonExternalNames : Std.HashSet String) (expr : StmtExprMd) : StmtExprMd := + mapStmtExpr (fun e => + match e.val with + | .Quantifier mode param trigger body => + let body' := rewriteCallsToFunctional nonExternalNames (stripAssertAssume body) + let trigger' := trigger.map (rewriteCallsToFunctional nonExternalNames) + ⟨.Quantifier mode param trigger' body', e.source⟩ + | _ => e) expr + +/-- Apply quantifier body rewriting to all postconditions and the implementation + of a procedure. -/ +private def rewriteQuantifierBodiesInProc (nonExternalNames : Std.HashSet String) (proc : Procedure) : Procedure := + let rewrite := rewriteQuantifierBodies nonExternalNames + match proc.body with + | .Opaque postconds impl modif => + let postconds' := postconds.map fun c => { c with condition := rewrite c.condition } + let impl' := impl.map rewrite + { proc with body := .Opaque postconds' impl' modif } + | .Transparent body => + { proc with body := .Transparent (rewrite body) } + | .Abstract postconds => + let postconds' := postconds.map fun c => { c with condition := rewrite c.condition } + { proc with body := .Abstract postconds' } + | .External => proc + /-- Build a free postcondition equating the procedure's output to its functional version. For a procedure `foo(a, b) returns (r)`, produces: `r == foo$asFunction(a, b)` -/ @@ -100,12 +119,15 @@ private def mkFreePostcondition (proc : Procedure) : StmtExprMd := If the procedure is transparent, include a functional body. Otherwise the function is opaque. -/ private def mkFunctionCopy (asFunctionNames : Std.HashSet String) (proc : Procedure) : Procedure := - let funcName := { proc.name with text := proc.name.text ++ "$asFunction", uniqueId := none } + let hasProcedureTwin := asFunctionNames.contains proc.name.text + let funcName := if hasProcedureTwin then + { proc.name with text := proc.name.text ++ "$asFunction", uniqueId := none } + else proc.name let body := match proc.body with - | .Transparent b => .Transparent (rewriteCallsToFunctional asFunctionNames (stripAssertAssume b)) - | .Opaque _ _ _ => .Opaque [] none [] + | .Transparent b => .Transparent (rewriteCallsToFunctional asFunctionNames (if hasProcedureTwin then stripAssertAssume b else b)) + | .Opaque _ _ _ => if hasProcedureTwin then .Opaque [] none [] else proc.body | x => x - { proc with name := funcName, isFunctional := true, body := body, preconditions := [] } + { proc with name := funcName, isFunctional := true, body := body } /-- Append a free postcondition to a procedure's body postconditions. For Opaque and Abstract bodies, the free condition is appended to the @@ -126,46 +148,31 @@ private def addFreePostcondition (proc : Procedure) (freePost : StmtExprMd) : Pr { proc with body := .Opaque [freeCond] (some body) [] } | _ => proc -/-- -Transparency pass: translate a Laurel program to the UnorderedCoreWithLaurelTypes IR. - -For each procedure: -- Generate a function with the same signature, named `foo$asFunction` -- If transparent, the function gets a functional body (assertions erased, calls to functional versions) -- If the function has a body, add a free postcondition equating the procedure output to the function --/ -def transparencyPass (program : Program) : UnorderedCoreWithLaurelTypes := - let (skipped, notSkipped) := program.staticProcedures.partition (fun p => p.body.isExternal || - -- Skip functions until we introduce a contract pass, - -- which enables lifting procedure calls from contracts - p.isFunctional) - let asFunctionNames : Std.HashSet String := notSkipped.foldl (fun s p => s.insert p.name.text) {} - let asFunctions := notSkipped.map (mkFunctionCopy asFunctionNames) - - -- External procedures get a plain function copy (they have no $asFunction version) - let (skippedFunctions, skippedProcedures) := skipped.partition (fun p => p.isFunctional) - let functions := skippedFunctions ++ asFunctions - let coreProcedures := notSkipped.map fun p => - let freePostcondition := mkFreePostcondition p - let p := addFreePostcondition p freePostcondition - { p with isFunctional := false } +def createFunctionsForTransparentBodies (program : Program) : UnorderedCoreWithLaurelTypes := + let (toUpdate, _) := program.staticProcedures.partition (fun p => !p.body.isExternal && !p.isFunctional) + let toUpdateNames : Std.HashSet String := toUpdate.foldl (fun s p => s.insert p.name.text) {} + -- $asFunction copies for non-external procedures + let functions := program.staticProcedures.map (mkFunctionCopy toUpdateNames) + let coreProcedures := toUpdate.map fun proc => + let freePostcondition := mkFreePostcondition proc + let proc := { proc with isFunctional := false, axioms := proc.axioms.map (rewriteCallsToFunctional toUpdateNames) } + let proc := rewriteQuantifierBodiesInProc toUpdateNames proc + addFreePostcondition proc freePostcondition let datatypes := program.types.filterMap fun td => match td with | .Datatype dt => some dt | _ => none - let procs: List Procedure := skippedProcedures ++ coreProcedures - { functions, coreProcedures := procs, datatypes, constants := program.constants } + { functions, coreProcedures, datatypes, constants := program.constants } -open Std (Format ToFormat) - -def formatUnorderedCoreWithLaurelTypes (p : UnorderedCoreWithLaurelTypes) : Format := - let datatypeFmts := p.datatypes.map ToFormat.format - let constantFmts := p.constants.map ToFormat.format - let functionFmts := p.functions.map ToFormat.format - let procFmts := p.coreProcedures.map ToFormat.format - Format.joinSep (datatypeFmts ++ constantFmts ++ functionFmts ++ procFmts) "\n\n" - -instance : ToFormat UnorderedCoreWithLaurelTypes where - format := formatUnorderedCoreWithLaurelTypes +public def transparencyPass : LaurelPass Laurel.Program UnorderedCoreWithLaurelTypes where + name := "TransparencyPass" + comesBefore := [⟨ orderingPass.meta, "Transparency pass creates functions that are not ordered" ⟩] + documentation := "Translate a Laurel program to the UnorderedCoreWithLaurelTypes IR. +For each procedure: + - Generate a function with the same signature, named `foo$asFunction` + - If transparent, the function gets a functional body (assertions erased, calls to functional versions) + - If the function has a body, add a free postcondition equating the procedure output to the function" + run := fun p _ _ => + (createFunctionsForTransparentBodies p, [], {}) end -- public section end Strata.Laurel diff --git a/Strata/Languages/Laurel/TypeAliasElim.lean b/Strata/Languages/Laurel/TypeAliasElim.lean index 36c5426613..65a3a462e2 100644 --- a/Strata/Languages/Laurel/TypeAliasElim.lean +++ b/Strata/Languages/Laurel/TypeAliasElim.lean @@ -61,10 +61,10 @@ public def typeAliasElim (_model : SemanticModel) (program : Program) : Program { program with types := program.types.filter fun | .Alias _ => false | _ => true } /-- Pipeline pass: type alias elimination. -/ -public def typeAliasElimPass : LaurelPass where +public def typeAliasElimPass : LoweringPass where name := "TypeAliasElim" documentation := "Eliminates type aliases by replacing all UserDefined references to alias names with their resolved target types. Chained aliases are resolved transitively. Alias entries are removed from the type list." needsResolves := true - run := fun p m => (typeAliasElim m p, [], {}) + run := fun p m _ => (typeAliasElim m p, [], {}) end Strata.Laurel diff --git a/Strata/Languages/Laurel/TypeHierarchy.lean b/Strata/Languages/Laurel/TypeHierarchy.lean index 7ada70ac30..0414298e6a 100644 --- a/Strata/Languages/Laurel/TypeHierarchy.lean +++ b/Strata/Languages/Laurel/TypeHierarchy.lean @@ -10,6 +10,7 @@ import Strata.Util.Tactics public import Strata.Languages.Laurel.LaurelPass public import Strata.Languages.Laurel.Resolution import Std.Tactic.BVDecide.Normalize.Prop +import Strata.Languages.Laurel.HeapParameterization import Strata.Languages.Laurel.LaurelTypes import Strata.Languages.Laurel.MapStmtExpr @@ -187,11 +188,12 @@ def typeHierarchyTransform (model: SemanticModel) (program : Program) : Program mapProgramHighTypes (compositeRefToComposite compositeSet) transformed /-- Pipeline pass: type hierarchy transform. -/ -public def typeHierarchyTransformPass : LaurelPass where +public def typeHierarchyTransformPass : LoweringPass where name := "TypeHierarchyTransform" documentation := "Encodes the object-oriented type hierarchy (inheritance, dynamic dispatch, type tests, and casts) into explicit operations on a flat representation. Composite types with parents are flattened, and dynamic dispatch is resolved through type-test chains." needsResolves := false -- Only resolve again after completing HeapParam, ModifiesClauses and TypeHierarchy. These are logically one pass. - run := fun p m => + comesAfter := [⟨ heapParameterizationPass.meta, "the type hierarchy pass modifies the 'Composite' datatype that is introduced by this pass."⟩] + run := fun p m _ => (typeHierarchyTransform m p, [], {}) end Strata.Laurel diff --git a/Strata/Languages/Laurel/UnorderedCore.lean b/Strata/Languages/Laurel/UnorderedCore.lean new file mode 100644 index 0000000000..97c070902b --- /dev/null +++ b/Strata/Languages/Laurel/UnorderedCore.lean @@ -0,0 +1,34 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +module +public import Strata.Languages.Laurel.LaurelAST +import Strata.Languages.Laurel.Grammar.AbstractToConcreteTreeTranslator + +namespace Strata.Laurel + +open Std (Format ToFormat) +/-- +An intermediate representation produced by the transparency pass. +Functions are pure computational procedures (suffixed `$asFunction`); +coreProcedures are the original procedures with any free postconditions +embedded in their `Body.Opaque` postcondition lists. +-/ +public structure UnorderedCoreWithLaurelTypes where + functions : List Procedure + coreProcedures : List Procedure + datatypes : List DatatypeDefinition + constants : List Constant + +public def formatUnorderedCoreWithLaurelTypes (p : UnorderedCoreWithLaurelTypes) : Format := + let datatypeFmts := p.datatypes.map ToFormat.format + let constantFmts := p.constants.map ToFormat.format + let functionFmts := p.functions.map ToFormat.format + let procFmts := p.coreProcedures.map ToFormat.format + Format.joinSep (datatypeFmts ++ constantFmts ++ functionFmts ++ procFmts) "\n\n" + +public instance : ToFormat UnorderedCoreWithLaurelTypes where + format := formatUnorderedCoreWithLaurelTypes diff --git a/StrataPython/StrataPython/PySpecPipeline.lean b/StrataPython/StrataPython/PySpecPipeline.lean index 13e376c2ab..02322f079b 100644 --- a/StrataPython/StrataPython/PySpecPipeline.lean +++ b/StrataPython/StrataPython/PySpecPipeline.lean @@ -404,9 +404,9 @@ public def translateCombinedLaurelWithLowered (combined : Laurel.Program) /-- Translate a combined Laurel program to Core and prepend the full runtime prelude. -/ -public def translateCombinedLaurel (combined : Laurel.Program) +public def translateCombinedLaurel (combined : Laurel.Program) (keepAllFilesPrefix : Option String := none) : IO (Option Core.Program × List DiagnosticModel) := do - let (coreOption, errors, _, _) ← translateCombinedLaurelWithLowered combined + let (coreOption, errors, _, _) ← translateCombinedLaurelWithLowered combined keepAllFilesPrefix return (coreOption, errors) /-- Run the pyAnalyzeLaurel pipeline: read a Python Ion program, diff --git a/StrataPython/StrataPython/PythonToLaurel.lean b/StrataPython/StrataPython/PythonToLaurel.lean index 199963fa40..c01919b418 100644 --- a/StrataPython/StrataPython/PythonToLaurel.lean +++ b/StrataPython/StrataPython/PythonToLaurel.lean @@ -2063,7 +2063,7 @@ partial def translateStmt (ctx : TranslationContext) (s : stmt SourceRange) let assumeInRange := mkStmtExprMdWithLoc (.Assume inRangeExpr) md pure [assumeTypeInt, assumeInRange] | _ => - let targetInIter := mkStmtExprMd (.StaticCall "PIn" [targetVar, iterExpr]) + let targetInIter := mkStmtExprMdWithLoc (.StaticCall "PIn" [targetVar, iterExpr]) md let assumeInStmt := mkStmtExprMdWithLoc (.Assume (Any_to_bool targetInIter)) md pure [assumeInStmt] | _ => pure [] diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_empty.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_empty.expected index aab04f3a0d..7f98693936 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_empty.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_empty.expected @@ -1,4 +1,4 @@ -test_class_empty.py(5, 4): ✅ pass - callElimAssert_requires_4 +test_class_empty.py(5, 4): ✅ pass - precondition test_class_empty.py(6, 4): ✅ pass - empty class instantiated DETAIL: 2 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_field_init.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_field_init.expected index 7cb1e2fc89..4757a64ef8 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_field_init.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_field_init.expected @@ -1,2 +1,4 @@ -DETAIL: 0 passed, 0 failed, 0 inconclusive +test_class_field_init.py(20, 4): ✔️ always true if reached - (CircularBuffer@__init__ requires) Type constraint of size +test_class_field_init.py(20, 4): ✔️ always true if reached - (CircularBuffer@__init__ requires) Type constraint of name +DETAIL: 2 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_field_use.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_field_use.expected index 0caaf75c9f..29a689ea2c 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_field_use.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_field_use.expected @@ -1,5 +1,6 @@ +test_class_field_use.py(13, 4): ✔️ always true if reached - (CircularBuffer@__init__ requires) Type constraint of n test_class_field_use.py(14, 4): ✔️ always true if reached - Check PMul exception test_class_field_use.py(14, 4): ✔️ always true if reached - assert(302) test_class_field_use.py(15, 4): ✔️ always true if reached - Doubling of buffer did not work -DETAIL: 3 passed, 0 failed, 0 inconclusive +DETAIL: 4 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_methods.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_methods.expected index 36c53a8361..f0601e776e 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_methods.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_methods.expected @@ -1,11 +1,17 @@ -test_class_methods.py(34, 4): ✔️ always true if reached - main_assert(471)_13 +test_class_methods.py(34, 4): ✔️ always true if reached - (Account@__init__ requires) Type constraint of owner +test_class_methods.py(34, 4): ✔️ always true if reached - (Account@__init__ requires) Type constraint of balance +test_class_methods.py(34, 4): ✔️ always true if reached - main_assert(471)_32 test_class_methods.py(34, 4): ✔️ always true if reached - get_owner should return Alice -test_class_methods.py(34, 4): ✔️ always true if reached - main_assert(564)_15 +test_class_methods.py(34, 4): ✔️ always true if reached - main_assert(564)_35 test_class_methods.py(34, 4): ✔️ always true if reached - get_balance should return 100 -test_class_methods.py(34, 4): ✔️ always true if reached - main_assert(678)_17 +test_class_methods.py(34, 4): ✔️ always true if reached - (Account@set_balance requires) Type constraint of amount +test_class_methods.py(34, 4): ✔️ always true if reached - main_assert(678)_40 test_class_methods.py(34, 4): ✔️ always true if reached - set_balance should update balance +test_class_methods.py(34, 4): ✔️ always true if reached - (Origin_test_helper_procedure_Requires)req_name_is_foo +test_class_methods.py(34, 4): ✔️ always true if reached - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_str +test_class_methods.py(34, 4): ✔️ always true if reached - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_bar test_class_methods.py(31, 4): ✔️ always true if reached - assert_name_is_foo test_class_methods.py(31, 4): ✔️ always true if reached - assert_opt_name_none_or_str test_class_methods.py(31, 4): ✔️ always true if reached - assert_opt_name_none_or_bar -DETAIL: 9 passed, 0 failed, 0 inconclusive +DETAIL: 15 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_mixed_init.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_mixed_init.expected index e0d77a61a6..1966e6aa7b 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_mixed_init.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_mixed_init.expected @@ -1,4 +1,6 @@ +test_class_mixed_init.py(19, 0): ✔️ always true if reached - (WithInit@__init__ requires) Type constraint of x test_class_mixed_init.py(19, 0): ✔️ always true if reached - class with init +test_class_mixed_init.py(19, 0): ✔️ always true if reached - precondition test_class_mixed_init.py(19, 0): ✔️ always true if reached - class with init -DETAIL: 2 passed, 0 failed, 0 inconclusive +DETAIL: 4 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init.expected index 7228247375..a55c76cfe4 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init.expected @@ -1,4 +1,4 @@ -test_class_no_init.py(5, 4): ✅ pass - callElimAssert_requires_4 +test_class_no_init.py(5, 4): ✅ pass - precondition test_class_no_init.py(6, 4): ❓ unknown - class without __init__ DETAIL: 1 passed, 0 failed, 1 inconclusive RESULT: Inconclusive diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_multi_field.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_multi_field.expected index 3dbe40b3b6..13ba7f459b 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_multi_field.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_multi_field.expected @@ -1,4 +1,4 @@ -test_class_no_init_multi_field.py(7, 4): ✅ pass - callElimAssert_requires_4 +test_class_no_init_multi_field.py(7, 4): ✅ pass - precondition test_class_no_init_multi_field.py(8, 4): ✅ pass - class with multiple annotated fields no init DETAIL: 2 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_with_method.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_with_method.expected index 29b6682a29..93f5036c2d 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_with_method.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_no_init_with_method.expected @@ -1,5 +1,5 @@ test_class_no_init_with_method.py(4, 23): ❓ unknown - (WithMethod@get_x ensures) Return type constraint -test_class_no_init_with_method.py(8, 4): ✅ pass - callElimAssert_requires_4 +test_class_no_init_with_method.py(8, 4): ✅ pass - precondition test_class_no_init_with_method.py(9, 4): ✅ pass - class with method but no __init__ DETAIL: 2 passed, 0 failed, 1 inconclusive RESULT: Inconclusive diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_class_with_methods.expected b/StrataPython/StrataPythonTest/expected_laurel/test_class_with_methods.expected index 1085e02e58..3f5ddaf665 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_class_with_methods.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_class_with_methods.expected @@ -1,9 +1,15 @@ -test_class_with_methods.py(32, 4): ✔️ always true if reached - main_assert(484)_12 +test_class_with_methods.py(32, 4): ✔️ always true if reached - (DataStore@__init__ requires) Type constraint of name +test_class_with_methods.py(32, 4): ✔️ always true if reached - (DataStore@add requires) Type constraint of amount +test_class_with_methods.py(32, 4): ✔️ always true if reached - (DataStore@add requires) Type constraint of amount +test_class_with_methods.py(32, 4): ✔️ always true if reached - main_assert(484)_34 test_class_with_methods.py(32, 4): ✔️ always true if reached - get_count should return 30 -test_class_with_methods.py(32, 4): ✔️ always true if reached - main_assert(569)_14 +test_class_with_methods.py(32, 4): ✔️ always true if reached - main_assert(569)_37 test_class_with_methods.py(32, 4): ✔️ always true if reached - get_name should return mystore +test_class_with_methods.py(32, 4): ✔️ always true if reached - (Origin_test_helper_procedure_Requires)req_name_is_foo +test_class_with_methods.py(32, 4): ✔️ always true if reached - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_str +test_class_with_methods.py(32, 4): ✔️ always true if reached - (Origin_test_helper_procedure_Requires)req_opt_name_none_or_bar test_class_with_methods.py(29, 4): ✔️ always true if reached - assert_name_is_foo test_class_with_methods.py(29, 4): ✔️ always true if reached - assert_opt_name_none_or_str test_class_with_methods.py(29, 4): ✔️ always true if reached - assert_opt_name_none_or_bar -DETAIL: 7 passed, 0 failed, 0 inconclusive +DETAIL: 13 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_deep_inline.expected b/StrataPython/StrataPythonTest/expected_laurel/test_deep_inline.expected index eac560309d..bc3395d77d 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_deep_inline.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_deep_inline.expected @@ -1,11 +1,15 @@ +test_deep_inline.py(21, 4): ✔️ always true if reached - (triple_apply requires) Type constraint of x +test_deep_inline.py(15, 4): ✔️ always true if reached - (double_inc requires) Type constraint of x +test_deep_inline.py(10, 4): ✔️ always true if reached - (inc requires) Type constraint of x test_deep_inline.py(6, 4): ✔️ always true if reached - Check PAdd exception -test_deep_inline.py(10, 4): ✔️ always true if reached - double_inc_assert(135)_35 +test_deep_inline.py(10, 4): ✔️ always true if reached - double_inc_assert(135)_54 test_deep_inline.py(10, 4): ✔️ always true if reached - Check PMul exception -test_deep_inline.py(15, 4): ✔️ always true if reached - triple_apply_assert(206)_17 +test_deep_inline.py(15, 4): ✔️ always true if reached - triple_apply_assert(206)_27 +test_deep_inline.py(15, 4): ✔️ always true if reached - (inc requires) Type constraint of x test_deep_inline.py(11, 4): ✔️ always true if reached - Check PAdd exception -test_deep_inline.py(15, 4): ✔️ always true if reached - triple_apply_assert(233)_18 -test_deep_inline.py(21, 4): ✔️ always true if reached - main_assert(279)_5 +test_deep_inline.py(15, 4): ✔️ always true if reached - triple_apply_assert(233)_30 +test_deep_inline.py(21, 4): ✔️ always true if reached - main_assert(279)_9 test_deep_inline.py(21, 4): ✔️ always true if reached - triple_apply(3) should be 9 test_deep_inline.py(21, 4): ✖️ always false if reached - triple_apply(3) should not be 10 -DETAIL: 8 passed, 1 failed, 0 inconclusive +DETAIL: 12 passed, 1 failed, 0 inconclusive RESULT: Failures found diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_havoc_callee_after_hole_call.expected b/StrataPython/StrataPythonTest/expected_laurel/test_havoc_callee_after_hole_call.expected index 9a320c707c..85efc6288d 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_havoc_callee_after_hole_call.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_havoc_callee_after_hole_call.expected @@ -2,9 +2,10 @@ test_havoc_callee_after_hole_call.py(8, 0): ❓ unknown - expected unknown becau 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 - 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(22, 0): ✔️ always true if reached - (MyClass@__init__ requires) Type constraint of n 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 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 +DETAIL: 5 passed, 0 failed, 4 inconclusive RESULT: Inconclusive diff --git a/StrataPython/StrataPythonTest/expected_laurel/test_with_statement.expected b/StrataPython/StrataPythonTest/expected_laurel/test_with_statement.expected index c73e76d8cb..8948ea9e4e 100644 --- a/StrataPython/StrataPythonTest/expected_laurel/test_with_statement.expected +++ b/StrataPython/StrataPythonTest/expected_laurel/test_with_statement.expected @@ -1,9 +1,13 @@ +test_with_statement.py(16, 4): ✔️ always true if reached - (Resource@__init__ requires) Type constraint of n test_with_statement.py(17, 4): ✔️ always true if reached - assert(364) test_with_statement.py(20, 4): ✔️ always true if reached - assert(426) +test_with_statement.py(23, 4): ✔️ always true if reached - (Resource@__init__ requires) Type constraint of n test_with_statement.py(25, 8): ✔️ always true if reached - assert(525) test_with_statement.py(26, 8): ✔️ always true if reached - assert(558) +test_with_statement.py(29, 4): ✔️ always true if reached - (Resource@__init__ requires) Type constraint of n +test_with_statement.py(30, 4): ✔️ always true if reached - (Resource@__init__ requires) Type constraint of n test_with_statement.py(32, 21): ✔️ always true if reached - Check PAdd exception test_with_statement.py(32, 8): ✔️ always true if reached - assert(697) test_with_statement.py(33, 8): ✔️ always true if reached - assert(724) -DETAIL: 7 passed, 0 failed, 0 inconclusive +DETAIL: 11 passed, 0 failed, 0 inconclusive RESULT: Analysis success diff --git a/StrataPython/StrataPythonTestExtra/AnalyzeLaurelTest.lean b/StrataPython/StrataPythonTestExtra/AnalyzeLaurelTest.lean index c6bc50370d..898d069c8d 100644 --- a/StrataPython/StrataPythonTestExtra/AnalyzeLaurelTest.lean +++ b/StrataPython/StrataPythonTestExtra/AnalyzeLaurelTest.lean @@ -370,10 +370,9 @@ Without the attribute, the regex VC would be ❓ unknown. -/ | .error msg => throw <| IO.userError s!"Pipeline failed: {msg}" | .ok vcResults => for r in vcResults do - if r.obligation.label.startsWith "servicelib_Storage_" then - if !r.isSuccess then - throw <| IO.userError - s!"Expected all Storage preconditions to pass but got: {r.formatOutcome}" + if !r.isSuccess then + throw <| IO.userError + s!"Expected all Storage preconditions to pass but got: {r.formatOutcome}" /-! ## Resolution error test after FilterPrelude diff --git a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean index 4456562e35..cfc0740d51 100644 --- a/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean +++ b/StrataTest/Languages/Laurel/AbstractToConcreteTreeTranslatorTest.lean @@ -121,7 +121,9 @@ composite Point { info: procedure test(x: int): int opaque { - if x > 0 then x else 0 - x + if x > 0 + then x + else 0 - x }; -/ #guard_msgs in @@ -332,7 +334,8 @@ procedure test(): int info: procedure earlyExit(b: bool) opaque { - if b then { + if b + then { return }; assert true diff --git a/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean index 855f316187..9580ece5d6 100644 --- a/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean +++ b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean @@ -30,9 +30,7 @@ private def printElim (program : StrataDDM.Program) : IO Unit := do /-- info: function nat$constraint(x: int): bool -{ - x >= 0 -}; +x >= 0; procedure test(n: int) returns (r: int) requires nat$constraint(n) @@ -66,13 +64,12 @@ procedure test(n: nat) returns (r: nat) opaque { -- Scope management: constrained variable in if-branch must not leak into sibling block /-- info: function pos$constraint(v: int): bool -{ - v > 0 -}; +v > 0; procedure test(b: bool) opaque { - if b then { + if b + then { var x: int := 1; assert pos$constraint(x) }; @@ -108,9 +105,7 @@ procedure test(b: bool) opaque { -- The variable has no known value, only the type constraint is assumed. /-- info: function posint$constraint(x: int): bool -{ - x > 0 -}; +x > 0; procedure f() opaque { diff --git a/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean b/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean index 64517c93fe..548073bb0a 100644 --- a/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean +++ b/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean @@ -59,7 +59,8 @@ info: procedure basic() { x := x + 1 }; - if !(x < 3) then exit $dowhile_exit_0 + if !(x < 3) + then exit $dowhile_exit_0 } }$dowhile_exit_0; assert x == 3 @@ -106,12 +107,14 @@ info: procedure nested() { y := y + 1 }; - if !(y < 3) then exit $dowhile_exit_0 + if !(y < 3) + then exit $dowhile_exit_0 } }$dowhile_exit_0; x := x + 1 }; - if !(x < 3) then exit $dowhile_exit_1 + if !(x < 3) + then exit $dowhile_exit_1 } }$dowhile_exit_1; assert x == 3 diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean index ca26f5f73e..0d15885bc9 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean @@ -31,7 +31,7 @@ procedure outputValid(): nat // Output constraint — invalid return fails procedure outputInvalid(): nat -// ^^^ error: assertion does not hold +// ^^^ error: postcondition does not hold opaque { return -1 @@ -169,9 +169,7 @@ procedure uninitNotWitness() // Quantifier constraint injection — forall // n + 1 > 0 is only provable with n >= 0 injected; false for all int -procedure forallNat() - opaque -{ +procedure forallNat() opaque { var b: bool := forall(n: nat) => n + 1 > 0; assert b }; @@ -179,9 +177,7 @@ procedure forallNat() // 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() - opaque -{ +procedure existsNat() opaque { var b: bool := exists(n: nat) => n == 42; assert b }; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean index 2e5e628620..eb7562fe68 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T15_ShortCircuit.lean @@ -24,7 +24,6 @@ procedure mustNotCallProc(): int }; // Pure path: function with requires false - procedure testAndThenFunc() opaque { diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveFunction.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean similarity index 76% rename from StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveFunction.lean rename to StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean index 4b56c8266e..9a97632738 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveFunction.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T18_RecursiveProcedure.lean @@ -22,8 +22,9 @@ datatype IntList { Cons(head: int, tail: IntList) } -function listLen(xs: IntList): int { - if IntList..isNil(xs) then 0 +procedure listLen(xs: IntList): int +{ + return if IntList..isNil(xs) then 0 else 1 + listLen(IntList..tail!(xs)) }; @@ -35,13 +36,15 @@ procedure testListLen() }; // Mutual recursion -function listLenEven(xs: IntList): bool { - if IntList..isNil(xs) then true +procedure listLenEven(xs: IntList): bool +{ + return if IntList..isNil(xs) then true else listLenOdd(IntList..tail!(xs)) }; -function listLenOdd(xs: IntList): bool { - if IntList..isNil(xs) then false +procedure listLenOdd(xs: IntList): bool +{ + return if IntList..isNil(xs) then false else listLenEven(IntList..tail!(xs)) }; diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean index 2389fa10c4..32efb65c0c 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T19_InvokeOn.lean @@ -69,7 +69,7 @@ procedure badPostcondition(x: int) invokeOn R(x) opaque ensures R(x) -// ^^^^ error: assertion could not be proved +// ^^^^ error: postcondition could not be proved { }; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean index c9c1a2cc35..1b3ae3aa75 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBody.lean @@ -15,11 +15,11 @@ program Laurel; procedure transparentBody(): int { assert true; - 3 + return 3 }; procedure tranparentCaller(): int { - transparentBody() + return transparentBody() }; procedure transparentCallerCaller() opaque { diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBodyError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBodyError.lean index b9f59037bb..806c97af00 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBodyError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T20_TransparentBodyError.lean @@ -16,13 +16,15 @@ procedure transparentBodyMultipleOuts() returns (q: int, r: int) { assert true; q := 3; -//^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts r := 2 +//^^^^^^ error: ending a transparent body with a Assign statement is not supported }; procedure transparentBodyNoOuts() { - assert true + assert true; + 3 +//^ error: ending a transparent body with a LiteralInt statement is not supported }; procedure transparentProcedureCaller() opaque { diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean index bf4535c2aa..23885ec665 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean @@ -9,6 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -111,9 +112,9 @@ procedure imperativeCallInConditionalExpression(b: bool) } }; -function add(x: int, y: int): int +procedure add(x: int, y: int): int { - x + y + return x + y }; procedure repeatedBlockExpressions() @@ -137,9 +138,58 @@ procedure addProcCaller(): int { var x: int := 0; var y: int := addProc({x := 1; x}, {x := x + 10; x}); - assert y == 11 // TOOD should be 12. Fix in other PR + assert y == 12 + // The next statement is not translated correctly. + // I think it's a bug in the handling of StaticCall + // Where a reference is substituted when it should not be // var z: int := addProc({x := 1; x}, {x := x + 10; x}) + (x := 3); // assert z == 15 }; + +procedure assertInsideConditionalExpression(a: int): int + return if a > 2 + then 4 + else { + assert a <= 2; + assert a < 2; +// ^^^^^^^^^^^^ error: assertion does not hold + 5 + }; + +procedure assertInBlockExpr() +opaque { + var x: int := 0; + var y: int := { assert x == 0; x := 1; x }; + assert y == 1 +}; + +procedure transparentProc(x: int) returns (r: int) +{ + return x + 1 +}; + +procedure assignmentInExpressionAfterProcCall() +opaque { + var x: int := 0; + var y: int := transparentProc(x) + (x := 2); + assert y == 3 +}; + +procedure liftInsideAssignmentInExpression() +opaque { + var x: int := 0; + var y: int := ((x := 1) + transparentProc(x)); + assert y == 3 +}; + +procedure hasMultipleOutputs() returns (x: int, y: int) opaque { + x := 1; + y := 2 +}; + +procedure liftWithMultipleOutputs() opaque { + var x: int := { assign var y: int, var z: int := hasMultipleOutputs() ; y + z } +}; + #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean index d1e67ff6f6..5862e9b93f 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressionsError.lean @@ -41,12 +41,10 @@ function functionWithWhile(x: int): int function functionCallingHasMutationAssignment(x: int): int { hasMutatingAssignment() -//^^^^^^^^^^^^^^^^^^^^^^^ error: calls to procedures are not supported in functions or contracts }; -procedure impureContractIsNotLegal1(x: int) +procedure impureContractIsLegal1(x: int) requires x == hasMutatingAssignment() -// ^^^^^^^^^^^^^^^^^^^^^^^ error: calls to procedures are not supported in functions or contracts opaque { assert hasMutatingAssignment() == 1 @@ -58,6 +56,5 @@ procedure impureContractIsNotLegal2(x: int) opaque { assert (x := 2) == 2 -// ^^^^^^ error: destructive assignments are not supported in transparent bodies or contracts }; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean index ed39235e88..83f3a702a2 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlow.lean @@ -12,7 +12,7 @@ open Strata #eval testLaurel <| #strata program Laurel; -function returnAtEnd(x: int) returns (r: int) { +procedure returnAtEnd(x: int) returns (r: int) { if x > 0 then { if x == 1 then { return 1 @@ -24,11 +24,25 @@ function returnAtEnd(x: int) returns (r: int) { } }; -function elseWithCall(): int { +function elseWithCall(): int +{ if true then 3 else returnAtEnd(3) }; -function guardInFunction(x: int) returns (r: int) { +procedure testFunctions() + opaque +{ + assert returnAtEnd(1) == 1; + assert returnAtEnd(1) == 2; +//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved + + assert guardInFunction(1) == 1; + assert guardInFunction(1) == 2 +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved +}; + +procedure guardInFunction(x: int) returns (r: int) +{ if x > 0 then { if x == 1 then { return 1 @@ -40,20 +54,7 @@ function guardInFunction(x: int) returns (r: int) { return 3 }; -procedure testFunctions() - opaque -{ - assert returnAtEnd(1) == 1; - assert returnAtEnd(1) == 2; -//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion does not hold - - assert guardInFunction(1) == 1; - assert guardInFunction(1) == 2 -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion does not hold -}; - procedure guards(a: int) returns (r: int) - opaque { var b: int := a + 2; if b > 2 then { diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean index 9438d322fe..a1d8f03ad7 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError.lean @@ -38,10 +38,4 @@ function localVariableWithoutInitializer(): int { //^^^^^^^^^^ error: local variables in functions must have initializers 3 }; - -function deadCodeAfterIfElse(x: int) returns (r: int) { - if x > 0 then { return 1 } else { return 2 }; -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: if-then-else only supported as the last statement in a block - return 3 -}; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean new file mode 100644 index 0000000000..656009ffca --- /dev/null +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T3_ControlFlowError2.lean @@ -0,0 +1,21 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +import StrataTest.Util.TestLaurel + +open StrataTest.Util +open Strata + +#eval testLaurel <| +#strata +program Laurel; + +procedure deadCodeAfterIfElse(x: int) returns (r: int) { + if x > 0 then { return 1 } else { return 2 }; +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: in a transparent body, if-then-else is only supported as the last statement in a block + return 3 +}; +#end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean index f23b6d81c7..7f68e3caf6 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T5_ProcedureCalls.lean @@ -27,7 +27,7 @@ procedure fooSingleAssign(): int var x: int := 0; var x2: int := x + 1; var x3: int := x2 + 1; - x3 + return x3 }; procedure fooProof() diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T7_Decreases.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T7_Decreases.lean index 8be8a22a65..61175cc1e2 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T7_Decreases.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T7_Decreases.lean @@ -21,23 +21,29 @@ procedure noDecreases(x: int): boolean; procedure caller(x: int) requires noDecreases(x) // ^ error: noDecreases can not be called from a pure context, because it is not proven to terminate + opaque ; procedure noCyclicCalls() + opaque decreases [] { leaf(); }; -procedure leaf() decreases [1] { }; +procedure leaf() decreases [1] + opaque +{ }; procedure mutualRecursionA(x: nat) + opaque decreases [x, 1] { mutualRecursionB(x); }; procedure mutualRecursionB(x: nat) + opaque decreases [x, 0] { if x != 0 { mutualRecursionA(x-1); } diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean index abf1d7e6c2..08e32bff17 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8_Postconditions.lean @@ -30,9 +30,24 @@ procedure callerOfOpaqueProcedure() }; procedure invalidPostcondition(x: int) + returns (r: int) // TODO, removing this returns triggers a latent bug opaque ensures false -// ^^^^^ error: assertion does not hold +// ^^^^^ error: postcondition does not hold { }; + +procedure bar(x: int) returns (r: int) + requires x > 0 + opaque + ensures r > 0 +{ + r := x + 1 +}; + +procedure caller() returns (out: int) + opaque +{ + out := bar(bar(5)) +}; #end diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean index 5c0ee06eb2..66a71def1c 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T8b_EarlyReturnPostconditions.lean @@ -33,7 +33,7 @@ program Laurel; procedure earlyReturnBuggy(x: int) returns (r: int) opaque ensures r >= 0 -// ^^^^^^ error: assertion does not hold +// ^^^^^^ error: postcondition does not hold { if x < 0 then { return x diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T9_Nondeterministic.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T9_Nondeterministic.lean index d77667fb3d..1331db2c27 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T9_Nondeterministic.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T9_Nondeterministic.lean @@ -20,6 +20,7 @@ nondet procedure nonDeterministic(x: int): (r: int) }; procedure caller() + opaque { var x = nonDeterministic(1) assert x > 0; @@ -29,11 +30,13 @@ procedure caller() }; nondet procedure nonDeterminsticTransparant(x: int): (r: int) + opaque { nonDeterministic(x + 1) }; procedure nonDeterministicCaller(x: int): int + opaque { nonDeterministic(x) }; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean b/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean index 7c84f64a55..d60053efef 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T10_CompositeBvField.lean @@ -44,7 +44,7 @@ procedure writeLiteral(r: Register) procedure writeWrongLiteral(r: Register) opaque ensures r#value == 100 bv 16 -// ^^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved +// ^^^^^^^^^^^^^^^^^^^^ error: postcondition could not be proved modifies r { r#value := 200 bv 16 diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean b/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean index 8213eb22de..4bca875744 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T1_MutableFields.lean @@ -9,7 +9,7 @@ import StrataTest.Util.TestLaurel open StrataTest.Util open Strata -#eval testLaurel +#eval testLaurelKeepIntermediates #strata program Laurel; composite Container { @@ -66,9 +66,7 @@ procedure updatesAndAliasing() assert dAlias#intValue == d#intValue }; -procedure subsequentHeapMutations() - opaque -{ +procedure subsequentHeapMutations() opaque { var c: Container := new Container; // The additional parenthesis on the next line are needed to let the parser succeed. Joe, any idea why this is needed? @@ -124,9 +122,7 @@ composite Pixel { var color: Color } -procedure datatypeField() - opaque -{ +procedure datatypeField() opaque { var p: Pixel := new Pixel; p#color := Red(); assert Color..isRed(p#color); diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8d_HeapMutatingValueReturn.lean b/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean similarity index 92% rename from StrataTest/Languages/Laurel/Examples/Fundamentals/T8d_HeapMutatingValueReturn.lean rename to StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean index 83b315e977..1a18972604 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T8d_HeapMutatingValueReturn.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T1b_HeapMutatingValueReturn.lean @@ -40,7 +40,7 @@ composite Container { procedure setAndReturnBuggy(c: Container, x: int) returns (r: int) opaque ensures r == x + 1 -// ^^^^^^^^^^ error: assertion does not hold +// ^^^^^^^^^^ error: postcondition does not hold modifies c { c#value := x; diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T3_ReadsClauses.lr.st b/StrataTest/Languages/Laurel/Examples/Objects/T3_ReadsClauses.lr.st index 5732c7d40c..210a95c155 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T3_ReadsClauses.lr.st +++ b/StrataTest/Languages/Laurel/Examples/Objects/T3_ReadsClauses.lr.st @@ -46,7 +46,8 @@ type Composite; type Field; val value: Field; -function opaqueProcedure_ensures(heap: Heap, c: Container, r: int): boolean { +function opaqueProcedure_ensures(heap: Heap, c: Container, r: int): boolean +{ true } diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean b/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean index 99ef1565c1..896474404b 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T6_Datatypes.lean @@ -18,17 +18,13 @@ datatype IntList { } // Construction and destructor access -procedure testConstruction() - opaque -{ +procedure testConstruction() opaque { var xs: IntList := Cons(42, Nil()); assert IntList..head(xs) == 42 }; // Constructor testing -procedure testConstructorTest() - opaque -{ +procedure testConstructorTest() opaque { var xs: IntList := Cons(1, Nil()); assert IntList..isCons(xs); assert !IntList..isNil(xs); @@ -39,9 +35,7 @@ procedure testConstructorTest() }; // Nested construction and deconstruction -procedure testNested() - opaque -{ +procedure testNested() opaque { var xs: IntList := Cons(1, Cons(2, Nil())); assert IntList..isCons(xs); assert IntList..head(xs) == 1; @@ -50,9 +44,7 @@ procedure testNested() assert IntList..isNil(IntList..tail(IntList..tail(xs))) }; -procedure unsafeDestructor() - opaque -{ +procedure unsafeDestructor() opaque { var nil: IntList := Nil(); var noError: int := IntList..head!(nil); var error: int := IntList..head(nil) @@ -66,18 +58,14 @@ function listHead(xs: IntList): int IntList..head(xs) }; -procedure testFunction() - opaque -{ +procedure testFunction() opaque { var xs: IntList := Cons(10, Nil()); var h: int := listHead(xs); assert h == 10 }; // Failing assertion -procedure testFailing() - opaque -{ +procedure testFailing() opaque { var xs: IntList := Nil(); assert IntList..isCons(xs) //^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion does not hold @@ -93,9 +81,7 @@ datatype OddList { OCons(head: int, tail: EvenList) } -procedure testMutualConstruction() - opaque -{ +procedure testMutualConstruction() opaque { var even: EvenList := ENil(); assert EvenList..isENil(even); var odd: OddList := OCons(1, ENil()); diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T9_OldHeapTwoState.lean b/StrataTest/Languages/Laurel/Examples/Objects/T9_OldHeapTwoState.lean index 6cc0fc85ff..89681f2234 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T9_OldHeapTwoState.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T9_OldHeapTwoState.lean @@ -59,7 +59,7 @@ procedure bumpCell(c: Cell) procedure bumpCellWrong(c: Cell) opaque ensures c#value == old(c#value) + 1 -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: postcondition could not be proved modifies c { c#value := c#value + 2 @@ -210,7 +210,7 @@ procedure bumpUnderWildcard(c: Cell) procedure wrongOldRelation(c: Cell) opaque ensures c#value == old(c#value) - 1 -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: postcondition could not be proved modifies c { c#value := c#value + 1 diff --git a/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Allocation.lr.st b/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Allocation.lr.st index dd02787340..3dc57127ed 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Allocation.lr.st +++ b/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Allocation.lr.st @@ -13,6 +13,7 @@ composite Immutable { invariant x + y >= 5 procedure construct() + opaque constructor requires contructing == {this} ensures constructing == {} @@ -50,6 +51,7 @@ composite ImmutableChainOfTwo { // only used privately procedure allocate() + opaque constructor ensures constructing = {this} { // empty body diff --git a/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Constructors.lr.st b/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Constructors.lr.st index 2d9a5afaa4..6d33df854c 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Constructors.lr.st +++ b/StrataTest/Languages/Laurel/Examples/Objects/WIP/5. Constructors.lr.st @@ -18,6 +18,7 @@ composite Immutable { // fields of Immutable are considered mutable inside this procedure // and invariants of Immutable are not visible // can only call procedures that are also constructing Immutable + opaque constructs Immutable modifies this { diff --git a/StrataTest/Languages/Laurel/Examples/Objects/WIP/7. InstanceCallables.lr.st b/StrataTest/Languages/Laurel/Examples/Objects/WIP/7. InstanceCallables.lr.st index fe4c5756d6..95cf829196 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/WIP/7. InstanceCallables.lr.st +++ b/StrataTest/Languages/Laurel/Examples/Objects/WIP/7. InstanceCallables.lr.st @@ -5,12 +5,14 @@ */ composite Base { procedure foo(): int + opaque ensures result > 3 { abstract }; } composite Extender1 extends Base { procedure foo(): int + opaque ensures result > 4 // ^^^^^^^ error: could not prove ensures clause guarantees that of extended method 'Base.foo' { abstract }; @@ -19,6 +21,7 @@ composite Extender1 extends Base { composite Extender2 extends Base { value: int procedure foo(): int + opaque ensures result > 2 { this.value + 2 // 'this' is an implicit variable inside instance callables diff --git a/StrataTest/Languages/Laurel/Examples/Objects/WIP/9. Closures.lr.st b/StrataTest/Languages/Laurel/Examples/Objects/WIP/9. Closures.lr.st index 212df76ab8..3449eb9246 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/WIP/9. Closures.lr.st +++ b/StrataTest/Languages/Laurel/Examples/Objects/WIP/9. Closures.lr.st @@ -87,6 +87,7 @@ so in affect there no longer are any variables captured by a closure. // Option A: first class procedures procedure hasClosure() returns (r: int) + opaque ensures r == 7 { var x = 3; diff --git a/StrataTest/Languages/Laurel/IncrDecrLiftTest.lean b/StrataTest/Languages/Laurel/IncrDecrLiftTest.lean index b9d00c16cd..44731ab35d 100644 --- a/StrataTest/Languages/Laurel/IncrDecrLiftTest.lean +++ b/StrataTest/Languages/Laurel/IncrDecrLiftTest.lean @@ -15,7 +15,6 @@ expected output. meta import StrataDDM.Elab meta import StrataDDM.BuiltinDialects.Init meta import Strata.Languages.Laurel.Grammar -meta import Strata.Languages.Laurel.LaurelToCoreTranslator meta import Strata.Languages.Laurel.EliminateIncrDecr meta import Strata.Languages.Laurel.LiftImperativeExpressions @@ -42,7 +41,7 @@ def parseLowerIncrDecr (input : String) : IO Program := do -- Step 2: resolve so liftExpressionAssignments has a valid SemanticModel let result := resolve program let (program, model) := (result.program, result.model) - pure (liftExpressionAssignments model program) + pure (liftExpressionAssignments program model []) /-- Statement form: `x++;` and `--x` as statements. Prefix (`--x`) produces a clean assignment. Postfix (`x++`) emits the same assignment-based form as diff --git a/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean b/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean index 4754a126aa..814fabe6cf 100644 --- a/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean +++ b/StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean @@ -11,7 +11,7 @@ by comparing the lifted Laurel against expected output. -/ import StrataTest.Util.TestLaurel -import Strata.Languages.Laurel.LaurelToCoreTranslator +import Strata.Languages.Laurel.LaurelToCoreSchemaPass import Strata.Languages.Laurel.Resolution open Strata @@ -22,7 +22,7 @@ namespace Strata.Laurel private def parseLaurelAndLift (program : StrataDDM.Program) : IO Program := do let laurelProgram ← translateLaurel program let result := resolve laurelProgram - pure (liftExpressionAssignments result.model result.program) + pure (liftExpressionAssignments result.program result.model []) private def printLifted (program : StrataDDM.Program) : IO Unit := do let lifted ← parseLaurelAndLift program diff --git a/StrataTest/Languages/Laurel/LiftHolesTest.lean b/StrataTest/Languages/Laurel/LiftHolesTest.lean index 46fc19a24f..a7d9879b9d 100644 --- a/StrataTest/Languages/Laurel/LiftHolesTest.lean +++ b/StrataTest/Languages/Laurel/LiftHolesTest.lean @@ -138,7 +138,8 @@ info: function $hole_0() procedure test() opaque { - if $hole_0() then { + if $hole_0() + then { assert true } }; @@ -160,7 +161,9 @@ info: function $hole_0() procedure test() opaque { - var x: int := if true then $hole_0() else 0 + var x: int := if true + then $hole_0() + else 0 }; -/ #guard_msgs in @@ -336,7 +339,8 @@ info: function $hole_0() procedure test() opaque { - if 1 + $hole_0() > 0 then { + if 1 + $hole_0() > 0 + then { assert true } }; @@ -424,7 +428,6 @@ info: function $hole_0(x: int) returns ($result: int) opaque; function test(x: int): int - opaque { $hole_0(x) }; @@ -434,7 +437,6 @@ function test(x: int): int #strata program Laurel; function test(x: int): int - opaque { }; #end @@ -530,7 +532,7 @@ info: function $hole_0() opaque; procedure test() { - assert IntList..isCons($hole_0()) + assert IntList..head($hole_0()) }; -/ #guard_msgs in @@ -538,7 +540,7 @@ procedure test() #strata program Laurel; datatype IntList { Nil(), Cons(head: int, tail: IntList) } -procedure test() { assert IntList..isCons() }; +procedure test() { assert IntList..head() }; #end end Laurel diff --git a/StrataTest/Languages/Laurel/LiftImperativeCallsInAssertTest.lean b/StrataTest/Languages/Laurel/LiftImperativeCallsInAssertTest.lean index 24ff37523b..dcc3135105 100644 --- a/StrataTest/Languages/Laurel/LiftImperativeCallsInAssertTest.lean +++ b/StrataTest/Languages/Laurel/LiftImperativeCallsInAssertTest.lean @@ -11,7 +11,7 @@ out of assert and assume conditions, while leaving assignments untouched -/ import StrataTest.Util.TestLaurel -import Strata.Languages.Laurel.LaurelToCoreTranslator +import Strata.Languages.Laurel.LaurelToCoreSchemaPass import Strata.Languages.Laurel.Resolution open Strata @@ -22,7 +22,7 @@ namespace Strata.Laurel private def parseLaurelAndLift (program : StrataDDM.Program) : IO Program := do let laurelProgram ← translateLaurel program let result := resolve laurelProgram - pure (liftExpressionAssignments result.model result.program) + pure (liftExpressionAssignments result.program result.model ["impure", "multi_out"]) private def printLifted (program : StrataDDM.Program) : IO Unit := do let lifted ← parseLaurelAndLift program @@ -40,9 +40,8 @@ info: procedure impure(): int }; procedure test() { - var $c_0: int; - $c_0 := impure(); - assert $c_0 == 1 + var $cndtn_0: int := impure(); + assert $cndtn_0 == 1 }; -/ #guard_msgs in @@ -65,7 +64,9 @@ procedure test() { info: procedure test() { var x: int := 0; - assert (x := 2) == 2 + var $x_0: int := x; + x := 2; + assert x == 2 }; -/ #guard_msgs in @@ -89,9 +90,8 @@ info: procedure impure(): int }; procedure test() { - var $c_0: int; - $c_0 := impure(); - assume $c_0 == 1 + var $cndtn_0: int := impure(); + assume $cndtn_0 == 1 }; -/ #guard_msgs in @@ -121,9 +121,8 @@ info: procedure multi_out(x: int) }; procedure test() { - var $c_0: BUG_MultiValuedExpr; - $c_0 := multi_out(5); - assert $c_0 == 6 + var $cndtn_0: BUG_MultiValuedExpr := multi_out(5); + assert $cndtn_0 == 6 }; -/ #guard_msgs in diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index eedf40093d..4de8a96ba4 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -6,7 +6,7 @@ import VersoManual -import Strata.Languages.Laurel +import Strata.Languages.Laurel.LaurelAST import Strata.Languages.Laurel.LaurelTypes import Strata.Languages.Laurel.LaurelCompilationPipeline import Strata.Languages.Laurel.HeapParameterization @@ -24,34 +24,39 @@ open Verso.Genre.Manual.InlineLean set_option pp.rawOnError true -/-- Block command that generates documentation for all Laurel pipeline passes. - Usage inside a `#doc` block: `{laurelPipelineDocs}` -/ -@[block_command] -def laurelPipelineDocs : Verso.Doc.Elab.BlockCommandOf Unit := fun () => do - let entries := laurelPipeline.map fun pass => +/-- Markdown documentation for all Laurel passes, including their + `comesBefore`/`comesAfter` ordering rationales. Note: pass + `documentation`/`reason` strings are rendered as Markdown, so avoid raw + `` text (it is treated as inline HTML and crashes Verso's + converter); use backticks for inline code instead. -/ +def laurelPipelineDocsMarkdown : String := + let entries := allPasses.map fun pass => let base := s!"- **{pass.name}**: {pass.documentation}" - if pass.comesBefore.isEmpty then base - else - let deps := pass.comesBefore.map fun cb => - s!" - Comes before **{cb.pass.name}** because: {cb.reason}" - base ++ "\n" ++ "\n".intercalate deps - - let md := "\n".intercalate entries.toList - let some ast := MD4Lean.parse md - | Lean.throwError "Failed to parse laurelPipelineDocumentation as Markdown" - let blocks ← ast.blocks.mapM (Markdown.blockFromMarkdown · (handleHeaders := Markdown.strongEmphHeaders)) - `(Verso.Doc.Block.concat #[$blocks,*]) - -/-- Block command that generates a dependency graph for the Laurel pipeline passes - based on the `comesBefore` property. - Usage inside a `#doc` block: `{laurelPipelineDependencyGraph}` -/ -@[block_command] -def laurelPipelineDependencyGraph : Verso.Doc.Elab.BlockCommandOf Unit := fun () => do + let beforeDeps := pass.comesBefore.map fun cb => + s!" - Comes before **{cb.pass.name}** because: {cb.reason}" + let afterDeps := pass.comesAfter.map fun ca => + s!" - Comes after **{ca.pass.name}** because: {ca.reason}" + let deps := beforeDeps ++ afterDeps + if deps.isEmpty then base + else base ++ "\n" ++ "\n".intercalate deps + "\n".intercalate entries.toList + +/-- Markdown dependency graph for the Laurel passes, derived from the + `comesBefore`/`comesAfter` properties. -/ +def laurelPipelineDependencyGraphMarkdown : String := Id.run do -- Collect all edges: (source, target, reason) where source comesBefore target let mut edges : List (String × String × String) := [] - for pass in laurelPipeline do + for pass in allPasses do + -- `pass.comesBefore` declares: pass must run before cb.pass, i.e. pass → cb.pass for cb in pass.comesBefore do edges := edges ++ [(pass.name, cb.pass.name, cb.reason)] + -- `pass.comesAfter` declares: pass must run after ca.pass, i.e. ca.pass → pass + for ca in pass.comesAfter do + edges := edges ++ [(ca.pass.name, pass.name, ca.reason)] + + -- Deduplicate edges with the same (source, target), keeping the first reason. + edges := edges.foldl (init := []) fun acc e => + if acc.any (fun a => a.1 == e.1 && a.2.1 == e.2.1) then acc else acc ++ [e] -- Build the graph as a markdown list showing dependencies let mut md := "**Dependency edges** (A → B means A must run before B):\n\n" @@ -62,16 +67,35 @@ def laurelPipelineDependencyGraph : Verso.Doc.Elab.BlockCommandOf Unit := fun () md := md ++ s!"- **{src}** → **{tgt}**\n - *{reason}*\n" -- Add a textual rendering of the pipeline order with dependency annotations - md := md ++ "\n**Pipeline execution order:**\n\n" + md := md ++ "\n**Pipeline execution order** (→ X: must run before X; ← X: must run after X):\n\n" md := md ++ "```\n" let mut idx := 1 - for pass in laurelPipeline do - let deps := pass.comesBefore.map (s!" → {·.pass.name}") + for pass in allPasses do + let beforeDeps := pass.comesBefore.map (s!" → {·.pass.name}") + let afterDeps := pass.comesAfter.map (s!" ← {·.pass.name}") + let deps := beforeDeps ++ afterDeps let depStr := if deps.isEmpty then "" else String.join deps md := md ++ s!"{idx}. {pass.name}{depStr}\n" idx := idx + 1 md := md ++ "```\n" + return md + +/-- Block command that generates documentation for all Laurel pipeline passes. + Usage inside a `#doc` block: `{laurelPipelineDocs}` -/ +@[block_command] +def laurelPipelineDocs : Verso.Doc.Elab.BlockCommandOf Unit := fun () => do + let md := laurelPipelineDocsMarkdown + let some ast := MD4Lean.parse md + | Lean.throwError "Failed to parse laurelPipelineDocumentation as Markdown" + let blocks ← ast.blocks.mapM (Markdown.blockFromMarkdown · (handleHeaders := Markdown.strongEmphHeaders)) + `(Verso.Doc.Block.concat #[$blocks,*]) +/-- Block command that generates a dependency graph for the Laurel pipeline passes + based on the `comesBefore` and `comesAfter` properties. + Usage inside a `#doc` block: `{laurelPipelineDependencyGraph}` -/ +@[block_command] +def laurelPipelineDependencyGraph : Verso.Doc.Elab.BlockCommandOf Unit := fun () => do + let md := laurelPipelineDependencyGraphMarkdown let some ast := MD4Lean.parse md | Lean.throwError "Failed to parse laurelPipelineDependencyGraph as Markdown" let blocks ← ast.blocks.mapM (Markdown.blockFromMarkdown · (handleHeaders := Markdown.strongEmphHeaders)) @@ -940,17 +964,20 @@ If new references or definitions are created during compilation, `resolve` must ## Translation Pipeline -Laurel programs are verified by translating them to Strata Core and then invoking the Core -verification pipeline. The Laurel compilation pipeline consists of three parts: -- Lowering, consisting of many phases. Maps Laurel to Laurel -- Ordering, consisting of a single pass. Maps Laurel to OrderedLaurel -- Translation, consisting of a single pass. Maps OrderedLaurel to Core. +The Laurel to Core translation pipeline uses these IRs: +- Laurel +- UnorderedCoreWithLaurelTypes +- CoreWithLaurelTypes +- Core -Ideally the translation pass only translates between types but does not change the structure of the program. +Most of the passes are in the Laurel IR. +The transparency pass goes from `Laurel` to `UnorderedCoreWithLaurelTypes`. +The CoreGroupingAndOrdering goes from `UnorderedCoreWithLaurelTypes` to `CoreWithLaurelTypes` +And the LaurelToCoreSchemaPass goes from `CoreWithLaurelTypes` to `Core`. -## Lowering Passes +## Passes -The following passes are part of the lowering group: +The following passes making up the compilation of Laurel to Core: {laurelPipelineDocs} From 04da456d018ed8a6eed7f7ca4ca48386cb028f53 Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 24 Jun 2026 11:40:16 +0000 Subject: [PATCH 40/42] Fixes --- .../Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean | 4 ++-- Strata/Languages/Laurel/InlineLocalVariables.lean | 4 ++-- Strata/Languages/Laurel/LaurelAST.lean | 2 +- Strata/Languages/Laurel/LiftImperativeExpressions.lean | 4 ++-- Strata/Languages/Laurel/MergeAndLiftReturns.lean | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean index 0524c2c390..78673dfeaa 100644 --- a/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean +++ b/Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean @@ -353,7 +353,7 @@ partial def translateStmtExpr (arg : Arg) : TransM StmtExprMd := do let cond ← translateStmtExpr condArg let invariants ← translateInvariantClauses translateStmtExpr invSeqArg let body ← translateStmtExpr bodyArg - return mkStmtExprMd (.While cond invariants none body) src + return mkStmtExprMd (.While cond invariants none body false) src | q`Laurel.forLoop, #[initArg, condArg, stepArg, invSeqArg, bodyArg] => let init ← translateStmtExpr initArg let cond ← translateStmtExpr condArg @@ -361,7 +361,7 @@ partial def translateStmtExpr (arg : Arg) : TransM StmtExprMd := do let invariants ← translateInvariantClauses translateStmtExpr invSeqArg let body ← translateStmtExpr bodyArg let whileBody := mkStmtExprMd (.Block [body, step] none) src - let whileStmt := mkStmtExprMd (.While cond invariants none whileBody) src + let whileStmt := mkStmtExprMd (.While cond invariants none whileBody false) src return mkStmtExprMd (.Block [init, whileStmt] none) src | q`Laurel.doWhile, #[bodyArg, condArg, invSeqArg] => -- A `do … while` is a post-test `While`. The `EliminateDoWhile` pass diff --git a/Strata/Languages/Laurel/InlineLocalVariables.lean b/Strata/Languages/Laurel/InlineLocalVariables.lean index 68b2e855e0..6fc5a5ed5e 100644 --- a/Strata/Languages/Laurel/InlineLocalVariables.lean +++ b/Strata/Languages/Laurel/InlineLocalVariables.lean @@ -91,12 +91,12 @@ private partial def inlineExpr (subst : InlineSubst) (expr : StmtExprMd) : Inlin let el' ← el.mapM (inlineExpr subst) return ⟨.IfThenElse cond' th' el', source⟩ - | .While cond invs dec body => + | .While cond invs dec body postTest => let cond' ← inlineExpr subst cond let invs' ← invs.mapM (inlineExpr subst) let dec' ← dec.mapM (inlineExpr subst) let body' ← inlineExpr subst body - return ⟨.While cond' invs' dec' body', source⟩ + return ⟨.While cond' invs' dec' body' postTest, source⟩ | .Return value => return ⟨.Return (← value.mapM (inlineExpr subst)), source⟩ diff --git a/Strata/Languages/Laurel/LaurelAST.lean b/Strata/Languages/Laurel/LaurelAST.lean index dd59dbc2c2..8b22a19cfe 100644 --- a/Strata/Languages/Laurel/LaurelAST.lean +++ b/Strata/Languages/Laurel/LaurelAST.lean @@ -313,7 +313,7 @@ inductive StmtExpr : Type where | While (cond : AstNode StmtExpr) (invariants : List (AstNode StmtExpr)) (decreases : Option (AstNode StmtExpr)) (body : AstNode StmtExpr) - (postTest : Bool := false) + (postTest : Bool) /-- Exit a labelled block. Models `break` and `continue` statements. -/ | Exit (target : String) /-- Return from the enclosing procedure with an optional value. -/ diff --git a/Strata/Languages/Laurel/LiftImperativeExpressions.lean b/Strata/Languages/Laurel/LiftImperativeExpressions.lean index 25eed8d0b4..f3dcf5e050 100644 --- a/Strata/Languages/Laurel/LiftImperativeExpressions.lean +++ b/Strata/Languages/Laurel/LiftImperativeExpressions.lean @@ -430,14 +430,14 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do let seqRet ← transformExpr retExpr return ⟨.Return (some seqRet), source⟩ - | .While cond invs dec body => + | .While cond invs dec body postTest => let seqCond ← transformExpr cond let seqInvs ← invs.mapM transformExpr let seqDec ← match dec with | some d => pure (some (← transformExpr d)) | none => pure none let seqBody ← transformExpr body - return ⟨.While seqCond seqInvs seqDec seqBody, source⟩ + return ⟨.While seqCond seqInvs seqDec seqBody postTest, source⟩ | .PureFieldUpdate target fieldName newValue => let seqTarget ← transformExpr target diff --git a/Strata/Languages/Laurel/MergeAndLiftReturns.lean b/Strata/Languages/Laurel/MergeAndLiftReturns.lean index 49c01bab4c..c0f4b7c614 100644 --- a/Strata/Languages/Laurel/MergeAndLiftReturns.lean +++ b/Strata/Languages/Laurel/MergeAndLiftReturns.lean @@ -43,7 +43,7 @@ def removeReturns (stmt : StmtExprMd) : Except DiagnosticModel StmtExprMd := | .Assert _ => .ok passThrough | .Block _ _ => .ok passThrough | .IfThenElse _ _ (some _) => .error (diagnosticFromSource head.source "in a transparent body, if-then-else is only supported as the last statement in a block") - | .While _ _ _ _ => .error $ diagnosticFromSource head.source $ "loops are not supported in transparent bodies or contracts" + | .While _ _ _ _ _ => .error $ diagnosticFromSource head.source $ "loops are not supported in transparent bodies or contracts" | .Var (.Declare _) => .error $ diagnosticFromSource head.source $ "local variables must have initializers in transparent bodies or contracts" | _ => .error (diagnosticFromSource head.source s!"unsupported statement {head.val.constructorName} in block head") From 01f0f753a417549e9f8918d2869a7036e215447e Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 24 Jun 2026 11:41:56 +0000 Subject: [PATCH 41/42] Fixes --- .../Languages/Laurel/ConstrainedTypeElimTest.lean | 12 ++++++------ .../Languages/Laurel/EliminateDoWhileTest.lean | 6 +++--- .../Examples/Fundamentals/T10_ConstrainedTypes.lean | 2 +- StrataTest/Util/TestLaurel.lean | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean index ac1c884582..396761d550 100644 --- a/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean +++ b/StrataTest/Languages/Laurel/ConstrainedTypeElimTest.lean @@ -29,8 +29,8 @@ private def printElim (program : StrataDDM.Program) : IO Unit := do IO.println (toString (Std.Format.pretty (Std.ToFormat.format proc))) /-- -info: function nat$constraint(x: int): bool -x >= 0; +info: procedure nat$constraint(x: int): bool +return x >= 0; procedure test(n: int) returns (r: int) requires nat$constraint(n) @@ -63,8 +63,8 @@ procedure test(n: nat) returns (r: nat) opaque { -- Scope management: constrained variable in if-branch must not leak into sibling block /-- -info: function pos$constraint(v: int): bool -v > 0; +info: procedure pos$constraint(v: int): bool +return v > 0; procedure test(b: bool) opaque { @@ -104,8 +104,8 @@ procedure test(b: bool) opaque { -- Uninitialized constrained variable: havoc + assume constraint. -- The variable has no known value, only the type constraint is assumed. /-- -info: function posint$constraint(x: int): bool -x > 0; +info: procedure posint$constraint(x: int): bool +return x > 0; procedure f() opaque { diff --git a/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean b/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean index 548073bb0a..136ad79209 100644 --- a/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean +++ b/StrataTest/Languages/Laurel/EliminateDoWhileTest.lean @@ -60,7 +60,7 @@ info: procedure basic() x := x + 1 }; if !(x < 3) - then exit $dowhile_exit_0 + then exit $dowhile_exit_0 } }$dowhile_exit_0; assert x == 3 @@ -108,13 +108,13 @@ info: procedure nested() y := y + 1 }; if !(y < 3) - then exit $dowhile_exit_0 + then exit $dowhile_exit_0 } }$dowhile_exit_0; x := x + 1 }; if !(x < 3) - then exit $dowhile_exit_1 + then exit $dowhile_exit_1 } }$dowhile_exit_1; assert x == 3 diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean index 7a8f69c2e3..e6ffbd6dce 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T10_ConstrainedTypes.lean @@ -145,7 +145,7 @@ procedure sideEffect() var x : nat; var y : int; y := (x := -1) + 1; -// ^^^^^^^ error: assertion does not hold +// ^^^^^^^ error: assertion could not be proved assert x==-1; assert y==0 }; diff --git a/StrataTest/Util/TestLaurel.lean b/StrataTest/Util/TestLaurel.lean index d9241e4ed6..bc36b7eb8b 100644 --- a/StrataTest/Util/TestLaurel.lean +++ b/StrataTest/Util/TestLaurel.lean @@ -344,7 +344,7 @@ private def runAndCheck (block : SourcedProgram) file-relative `line:col` range (so a `#guard_msgs` golden can pin the localization), and `showSnippet := true` to also append the snippet-relative range. (Failure reports always use the file-relative format regardless.) -/ -def testLaurel (block : SourcedProgram) +public def testLaurel (block : SourcedProgram) (options : LaurelVerifyOptions := defaultLaurelTestOptions) (showLocations : Bool := false) (showSnippet : Bool := false) : IO Unit := runAndCheck block (runLaurelPipelineRaw · options) showLocations showSnippet @@ -367,7 +367,7 @@ public def testLaurelKeepIntermediates (block : SourcedProgram) : IO Unit := do As with `testLaurel`, succeeds silently by default; `showLocations := true` echoes each diagnostic's file-relative `line:col` range and `showSnippet := true` appends the snippet-relative range. -/ -def testLaurelResolution (block : SourcedProgram) +public def testLaurelResolution (block : SourcedProgram) (showLocations : Bool := false) (showSnippet : Bool := false) : IO Unit := runAndCheck block runLaurelResolutionRaw showLocations showSnippet From 5270ab1baf77d988a9474a2e484357a6925b2ebf Mon Sep 17 00:00:00 2001 From: Remy Willems Date: Wed, 24 Jun 2026 11:43:36 +0000 Subject: [PATCH 42/42] Mute overly verbose test --- .../Objects/T9_ValueReturningInstanceMethods.lean | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T9_ValueReturningInstanceMethods.lean b/StrataTest/Languages/Laurel/Examples/Objects/T9_ValueReturningInstanceMethods.lean index fb7249ddb7..d9ebc7d6d5 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T9_ValueReturningInstanceMethods.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T9_ValueReturningInstanceMethods.lean @@ -32,6 +32,7 @@ open Strata /-! ## 1. Basic: instance method body returns a field via `return expr`. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -56,6 +57,7 @@ procedure useGet() /-! ## 2. Return a computed expression. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -80,6 +82,7 @@ procedure useIncd() /-! ## 3. Return an expression that uses a (non-self) parameter. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -105,6 +108,7 @@ procedure useAddTo() /-! ## 4. Conditional / early returns: a valued `return` in each branch of an if-then-else inside the method body. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -133,6 +137,7 @@ procedure useClampPos() /-! ## 5. Method that mutates a field (modifies clause) and then returns. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -161,6 +166,7 @@ procedure useSetAndGet() /-! ## 6. Boolean return type. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -186,6 +192,7 @@ procedure useIsPos() /-! ## 7. Two composites sharing a method name, both with value-returning bodies. Confirms lifting + value-return elimination keep them distinct. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -224,6 +231,7 @@ procedure useBoth() /-! ## 8. Value-returning method invoked through a field-selected receiver: `o#inner#getX()`. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -252,6 +260,7 @@ procedure useOuter() /-! ## 9. Local variable in the body before a valued return. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -278,6 +287,7 @@ procedure useDoubleV() /-! ## 10. Negative: valued return in an instance method with NO output parameter is rejected by `EliminateValueInReturns`. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel; @@ -295,6 +305,7 @@ composite C { /-! ## 11. Negative: valued return in an instance method with MULTIPLE output parameters is rejected by `EliminateValueInReturns`. -/ +#guard_msgs (drop info) in #eval testLaurel <| #strata program Laurel;