From d5d5cb85cfb396fc89c37f1b600cda71ff68145a Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 18:21:08 +0000 Subject: [PATCH 001/128] Add type checking to Laurel resolution pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change resolveStmtExpr to return (StmtExprMd × HighTypeMd) - Add type checks for: - Boolean conditions in if/while/assert/assume - Numeric operands in arithmetic/comparison operations - Boolean operands in logical operations - Argument types matching parameter types in static calls - Argument types matching parameter types in instance calls - Assignment value type matching target type - Function body type matching declared output type - Report type mismatches as diagnostics (compilation continues) - Handle cascading errors: Unknown types are compatible with everything, UserDefined types skip strict checking (subtype relationships not tracked), void types skip assignment checks (statements don't produce values) Closes #1120 --- Strata/Languages/Laurel/Resolution.lean | 345 +++++++++++++++++------- 1 file changed, 253 insertions(+), 92 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 16bcf1333f..287382d3f9 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -326,7 +326,14 @@ def resolveHighType (ty : HighTypeMd) : ResolveM HighTypeMd := do | .UserDefined ref => let ref' ← resolveRef ref ty.source (expected := #[.compositeType, .constrainedType, .datatypeDefinition, .typeAlias]) - pure (.UserDefined ref') + -- If the reference resolved to the wrong kind, treat the type as Unknown to avoid cascading errors + let s ← get + let kindOk : Bool := match s.scope.get? ref.text with + | some (_, node) => node.kind == .unresolved || + (#[ResolvedNodeKind.compositeType, .constrainedType, .datatypeDefinition, .typeAlias].contains node.kind) + | none => true -- unresolved references already reported + if kindOk then pure (HighType.UserDefined ref') + else pure HighType.Unknown | .TTypedField vt => let vt' ← resolveHighType vt pure (.TTypedField vt') @@ -353,40 +360,119 @@ def resolveHighType (ty : HighTypeMd) : ResolveM HighTypeMd := do | other => pure other return { val := val', source := ty.source } -def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM StmtExprMd := do +/-- Emit a type mismatch diagnostic. -/ +private def typeMismatch (source : Option FileRange) (expected : String) (actual : HighTypeMd) : ResolveM Unit := do + let actualStr := toString (formatHighTypeVal actual.val) + let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" + modify fun s => { s with errors := s.errors.push diag } + +/-- Check that a type is boolean, emitting a diagnostic if not. -/ +private def checkBool (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do + match ty.val with + | .TBool | .Unknown => pure () + | .UserDefined _ => pure () -- constrained types may wrap bool + | _ => typeMismatch source "bool" ty + +/-- Check that a type is numeric (int, real, or float64), emitting a diagnostic if not. -/ +private def checkNumeric (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do + match ty.val with + | .TInt | .TReal | .TFloat64 | .Unknown => pure () + | .UserDefined _ => pure () -- constrained types may wrap numeric types + | _ => typeMismatch source "a numeric type" ty + +/-- Check that two types are compatible, emitting a diagnostic if not. + UserDefined types are always considered compatible with each other since + subtype relationships (inheritance) are not tracked during resolution. -/ +private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do + match expected.val, actual.val with + | .Unknown, _ => pure () + | _, .Unknown => pure () + | _, .MultiValuedExpr _ => pure () -- arity mismatch already reported separately + | .UserDefined _, _ => pure () -- subtype relationships not tracked here + | _, .UserDefined _ => pure () -- subtype relationships not tracked here + | _, _ => + if !highEq expected actual then + let expectedStr := toString (formatHighTypeVal expected.val) + let actualStr := toString (formatHighTypeVal actual.val) + let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" + modify fun s => { s with errors := s.errors.push diag } + +/-- Get the type of a resolved variable reference from scope. -/ +private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do + let s ← get + match s.scope.get? ref.text with + | some (_, node) => pure node.getType + | none => pure { val := .Unknown, source := ref.source } + +/-- Get the call return type and parameter types for a callee from scope. -/ +private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List HighTypeMd) := do + let s ← get + match s.scope.get? callee.text with + | some (_, .staticProcedure proc) => + let retTy := match proc.outputs with + | [singleOutput] => singleOutput.type + | outputs => { val := .MultiValuedExpr (outputs.map (·.type)), source := none } + pure (retTy, proc.inputs.map (·.type)) + | some (_, .instanceProcedure _ proc) => + let retTy := match proc.outputs with + | [singleOutput] => singleOutput.type + | outputs => { val := .MultiValuedExpr (outputs.map (·.type)), source := none } + pure (retTy, proc.inputs.map (·.type)) + | some (_, .datatypeConstructor t _) => + -- Testers (e.g. "Color..isRed") return Bool; constructors return the type + if (callee.text.splitOn "..is").length > 1 then + pure ({ val := .TBool, source := callee.source }, []) + else + pure ({ val := .UserDefined t, source := callee.source }, []) + | some (_, .parameter p) => pure (p.type, []) + | some (_, .constant c) => pure (c.type, []) + | _ => pure ({ val := .Unknown, source := callee.source }, []) + +def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match _: exprMd with | AstNode.mk expr source => - let val' ← match _: expr with + let (val', ty) ← match _: expr with | .IfThenElse cond thenBr elseBr => - let cond' ← resolveStmtExpr cond - let thenBr' ← resolveStmtExpr thenBr - let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - pure (.IfThenElse cond' thenBr' elseBr') + let (cond', condTy) ← resolveStmtExpr cond + checkBool cond'.source condTy + let (thenBr', thenTy) ← resolveStmtExpr thenBr + let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + pure (.IfThenElse cond' thenBr' elseBr', thenTy) | .Block stmts label => withScope do - let stmts' ← stmts.mapM resolveStmtExpr - pure (.Block stmts' label) + let results ← stmts.mapM resolveStmtExpr + let stmts' := results.map (·.1) + let lastTy := match results.getLast? with + | some (_, ty) => ty + | none => { val := .TVoid, source := source } + pure (.Block stmts' label, lastTy) | .While cond invs dec body => - let cond' ← resolveStmtExpr cond - let invs' ← invs.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - let dec' ← dec.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - let body' ← resolveStmtExpr body - pure (.While cond' invs' dec' body') - | .Exit target => pure (.Exit target) + let (cond', condTy) ← resolveStmtExpr cond + checkBool cond'.source condTy + let invs' ← invs.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + let dec' ← dec.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + let (body', _) ← resolveStmtExpr body + pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) + | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) | .Return val => do - let val' ← val.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - pure (.Return val') - | .LiteralInt v => pure (.LiteralInt v) - | .LiteralBool v => pure (.LiteralBool v) - | .LiteralString v => pure (.LiteralString v) - | .LiteralDecimal v => pure (.LiteralDecimal v) + let val' ← val.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + pure (.Return val', { val := .TVoid, source := source }) + | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) + | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) + | .LiteralString v => pure (.LiteralString v, { val := .TString, source := source }) + | .LiteralDecimal v => pure (.LiteralDecimal v, { val := .TReal, source := source }) | .Var (.Local ref) => let ref' ← resolveRef ref source - pure (.Var (.Local ref')) + let ty ← getVarType ref + pure (.Var (.Local ref'), ty) | .Var (.Declare param) => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (.Var (.Declare ⟨name', ty'⟩)) + pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) | .Assign targets value => let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do let ⟨vv, vs⟩ := v @@ -395,14 +481,14 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM StmtExprMd := do let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let value' ← resolveStmtExpr value + let (value', valueTy) ← resolveStmtExpr value -- Check that LHS target count matches the number of outputs from the RHS. -- This fires for procedure calls (which can have multiple outputs). -- Functions always have exactly 1 output in the model, so single-target function calls pass trivially. @@ -424,84 +510,144 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM StmtExprMd := do let diag := diagnosticFromSource source s!"Assignment target count mismatch: {targets'.length} targets but right-hand side produces {expectedOutputCount} values" modify fun s => { s with errors := s.errors.push diag } - pure (.Assign targets' value') + -- Type check: for single-target assignments, check value type matches target type + -- Skip when value type is void (RHS is a statement like while/return that doesn't produce a value) + if targets'.length == 1 && valueTy.val != HighType.TVoid then + if let some target := targets'.head? then + let targetTy := match target.val with + | .Local ref => do + let s ← get + match s.scope.get? ref.text with + | some (_, node) => pure node.getType + | none => pure { val := HighType.Unknown, source := ref.source : HighTypeMd } + | .Declare param => pure param.type + | .Field _ fieldName => do + let s ← get + match s.scope.get? fieldName.text with + | some (_, node) => pure node.getType + | none => pure { val := HighType.Unknown, source := fieldName.source : HighTypeMd } + let tTy ← targetTy + checkAssignable source tTy valueTy + pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - pure (.Var (.Field target' fieldName')) + let ty ← getVarType fieldName + pure (.Var (.Field target' fieldName'), ty) | .PureFieldUpdate target fieldName newVal => - let target' ← resolveStmtExpr target + let (target', targetTy) ← resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let newVal' ← resolveStmtExpr newVal - pure (.PureFieldUpdate target' fieldName' newVal') + let (newVal', _) ← resolveStmtExpr newVal + pure (.PureFieldUpdate target' fieldName' newVal', targetTy) | .StaticCall callee args => let callee' ← resolveRef callee source (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let args' ← args.mapM resolveStmtExpr - pure (.StaticCall callee' args') + let results ← args.mapM resolveStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + -- Check argument types match parameter types + for (argTy, paramTy) in argTypes.zip paramTypes do + checkAssignable source paramTy argTy + pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => - let args' ← args.mapM resolveStmtExpr - pure (.PrimitiveOp op args') + let results ← args.mapM resolveStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let resultTy := match op with + | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies + | .Lt | .Leq | .Gt | .Geq => HighType.TBool + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT => + match argTypes.head? with + | some headTy => headTy.val + | none => HighType.TInt + | .StrConcat => HighType.TString + -- Type check operands + match op with + | .And | .Or | .AndThen | .OrElse | .Not | .Implies => + for aTy in argTypes do checkBool source aTy + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => + for aTy in argTypes do checkNumeric source aTy + | .Eq | .Neq | .StrConcat => pure () + pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source (expected := #[.compositeType, .datatypeDefinition]) - pure (.New ref') - | .This => pure .This + -- If the reference resolved to the wrong kind, use Unknown type to avoid cascading errors + let s ← get + let kindOk : Bool := match s.scope.get? ref.text with + | some (_, node) => node.kind == .unresolved || + (#[ResolvedNodeKind.compositeType, .datatypeDefinition].contains node.kind) + | none => true + let ty := if kindOk then { val := HighType.UserDefined ref', source := source } + else { val := HighType.Unknown, source := source } + pure (.New ref', ty) + | .This => pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => - let lhs' ← resolveStmtExpr lhs - let rhs' ← resolveStmtExpr rhs - pure (.ReferenceEquals lhs' rhs') + let (lhs', _) ← resolveStmtExpr lhs + let (rhs', _) ← resolveStmtExpr rhs + pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let ty' ← resolveHighType ty - pure (.AsType target' ty') + pure (.AsType target' ty', ty') | .IsType target ty => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let ty' ← resolveHighType ty - pure (.IsType target' ty') + pure (.IsType target' ty', { val := .TBool, source := source }) | .InstanceCall target callee args => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let callee' ← resolveRef callee source (expected := #[.instanceProcedure, .staticProcedure]) - let args' ← args.mapM resolveStmtExpr - pure (.InstanceCall target' callee' args') + let results ← args.mapM resolveStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + -- Check argument types match parameter types (skip first param which is 'self') + let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] + for (argTy, paramTy) in argTypes.zip callParamTypes do + checkAssignable source paramTy argTy + pure (.InstanceCall target' callee' args', retTy) | .Quantifier mode param trigger body => withScope do let paramTy' ← resolveHighType param.type let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') - let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; resolveStmtExpr pv.val) - let body' ← resolveStmtExpr body - pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body') + let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do + let (e', _) ← resolveStmtExpr pv.val; pure e') + let (body', _) ← resolveStmtExpr body + pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) | .Assigned name => - let name' ← resolveStmtExpr name - pure (.Assigned name') + let (name', _) ← resolveStmtExpr name + pure (.Assigned name', { val := .TBool, source := source }) | .Old val => - let val' ← resolveStmtExpr val - pure (.Old val') + let (val', valTy) ← resolveStmtExpr val + pure (.Old val', valTy) | .Fresh val => - let val' ← resolveStmtExpr val - pure (.Fresh val') + let (val', _) ← resolveStmtExpr val + pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => - let cond' ← resolveStmtExpr condExpr - pure (.Assert { condition := cond', summary }) + let (cond', condTy) ← resolveStmtExpr condExpr + checkBool cond'.source condTy + pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) | .Assume cond => - let cond' ← resolveStmtExpr cond - pure (.Assume cond') + let (cond', condTy) ← resolveStmtExpr cond + checkBool cond'.source condTy + pure (.Assume cond', { val := .TVoid, source := source }) | .ProveBy val proof => - let val' ← resolveStmtExpr val - let proof' ← resolveStmtExpr proof - pure (.ProveBy val' proof') + let (val', valTy) ← resolveStmtExpr val + let (proof', _) ← resolveStmtExpr proof + pure (.ProveBy val' proof', valTy) | .ContractOf ty fn => - let fn' ← resolveStmtExpr fn - pure (.ContractOf ty fn') - | .Abstract => pure .Abstract - | .All => pure .All + let (fn', _) ← resolveStmtExpr fn + pure (.ContractOf ty fn', { val := .Unknown, source := source }) + | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) + | .All => pure (.All, { val := .Unknown, source := source }) | .Hole det type => match type with | some ty => let ty' ← resolveHighType ty - pure (.Hole det ty') - | none => pure (.Hole det none) - return { val := val', source := source } + pure (.Hole det ty', ty') + | none => pure (.Hole det none, { val := .Unknown, source := source }) + return ({ val := val', source := source }, ty) termination_by exprMd decreasing_by all_goals term_by_mem @@ -511,21 +657,21 @@ def resolveParameter (param : Parameter) : ResolveM Parameter := do let name' ← defineNameCheckDup param.name (.parameter ⟨param.name, ty'⟩) return ⟨name', ty'⟩ -/-- Resolve a procedure body. -/ -def resolveBody (body : Body) : ResolveM Body := do +/-- Resolve a procedure body. Returns the resolved body and its type. -/ +def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do match body with | .Transparent b => - let b' ← resolveStmtExpr b - return .Transparent b' + let (b', ty) ← resolveStmtExpr b + return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM resolveStmtExpr) - let impl' ← impl.mapM resolveStmtExpr - let mods' ← mods.mapM resolveStmtExpr - return .Opaque posts' impl' mods' + let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let impl' ← impl.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let mods' ← mods.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM resolveStmtExpr) - return .Abstract posts' - | .External => return .External + let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + return (.Abstract posts', { val := .TVoid, source := none }) + | .External => return (.External, { val := .TVoid, source := none }) /-- Resolve a procedure: resolve its name, then resolve params, contracts, and body in a new scope. -/ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do @@ -533,14 +679,22 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) - let dec' ← proc.decreases.mapM resolveStmtExpr - let body' ← resolveBody proc.body + let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" modify fun s => { s with errors := s.errors.push diag } - let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr + -- Check body type matches declared output type for functional procedures with transparent bodies + if proc.isFunctional && body'.isTransparent then + match proc.outputs with + | [singleOutput] => + -- Only check when body produces a value (not void from return/while/assign) + if bodyTy.val != HighType.TVoid then + checkAssignable proc.name.source singleOutput.type bodyTy + | _ => pure () + let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -566,14 +720,21 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) - let dec' ← proc.decreases.mapM resolveStmtExpr - let body' ← resolveBody proc.body + let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" modify fun s => { s with errors := s.errors.push diag } - let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr + -- Check body type matches declared output type for functional procedures with transparent bodies + if proc.isFunctional && body'.isTransparent then + match proc.outputs with + | [singleOutput] => + if bodyTy.val != HighType.TVoid then + checkAssignable proc.name.source singleOutput.type bodyTy + | _ => pure () + let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, @@ -615,8 +776,8 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do -- in scope when resolving the constraint and witness expressions. let (valueName', constraint', witness') ← withScope do let valueName' ← defineNameCheckDup ct.valueName (.quantifierVar ct.valueName base') - let constraint' ← resolveStmtExpr ct.constraint - let witness' ← resolveStmtExpr ct.witness + let (constraint', _) ← resolveStmtExpr ct.constraint + let (witness', _) ← resolveStmtExpr ct.witness return (valueName', constraint', witness') return .Constrained { name := ctName', base := base', valueName := valueName', constraint := constraint', witness := witness' } @@ -642,7 +803,7 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM resolveStmtExpr + let init' ← c.initializer.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } From f65de03b28af37b7c24f534c2bcbb8ba25f912b4 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 18:58:54 +0000 Subject: [PATCH 002/128] Fix type checking: skip TCore types in assignability check TCore is a pass-through type from Core that should not be checked during Laurel resolution. Without this, two identical TCore types (e.g. 'Core Any') would fail highEq (which has no TCore case) and produce spurious 'Type mismatch' diagnostics. --- Strata/Languages/Laurel/Resolution.lean | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 287382d3f9..43d8866d0d 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -390,6 +390,8 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) | _, .MultiValuedExpr _ => pure () -- arity mismatch already reported separately | .UserDefined _, _ => pure () -- subtype relationships not tracked here | _, .UserDefined _ => pure () -- subtype relationships not tracked here + | .TCore _, _ => pure () -- pass-through Core types not checked during resolution + | _, .TCore _ => pure () -- pass-through Core types not checked during resolution | _, _ => if !highEq expected actual then let expectedStr := toString (formatHighTypeVal expected.val) From 7aaea818895c9b1e2b4f510a8281ea0d586b67bf Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:22:28 +0000 Subject: [PATCH 003/128] Simplify assignment arity check to use valueTy directly Derive expected output count from the RHS type (MultiValuedExpr gives the arity, otherwise 1) instead of re-looking up the procedure. This ensures LHS and RHS arity always match for assignments. --- Strata/Languages/Laurel/Resolution.lean | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 43d8866d0d..d87f97cd73 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -491,24 +491,11 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) let (value', valueTy) ← resolveStmtExpr value - -- Check that LHS target count matches the number of outputs from the RHS. - -- This fires for procedure calls (which can have multiple outputs). - -- Functions always have exactly 1 output in the model, so single-target function calls pass trivially. - let expectedOutputCount ← match value'.val with - | .StaticCall callee _ => do - let s ← get - match s.scope.get? callee.text with - | some (_, .staticProcedure proc) => pure proc.outputs.length - | some (_, .instanceProcedure _ proc) => pure proc.outputs.length - | _ => pure 1 - | .InstanceCall _ callee _ => do - let s ← get - match s.scope.get? callee.text with - | some (_, .instanceProcedure _ proc) => pure proc.outputs.length - | some (_, .staticProcedure proc) => pure proc.outputs.length - | _ => pure 1 - | _ => pure 1 - if targets'.length != expectedOutputCount then + -- Check that LHS target count matches the RHS arity (derived from the value type). + let expectedOutputCount := match valueTy.val with + | .MultiValuedExpr tys => tys.length + | _ => 1 + if valueTy.val != HighType.TVoid && targets'.length != expectedOutputCount then let diag := diagnosticFromSource source s!"Assignment target count mismatch: {targets'.length} targets but right-hand side produces {expectedOutputCount} values" modify fun s => { s with errors := s.errors.push diag } From 179d16dc811c006331ae0ae2e98e4c71869556b1 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:26:32 +0000 Subject: [PATCH 004/128] Add tests for type checking error diagnostics in resolution pass Tests confirm that the following type errors are reported: - Non-boolean condition in if/assert/assume/while - Non-boolean operand in logical operators (&&) - Non-numeric operand in comparisons (<) - Assignment type mismatch (int := bool) - Function return type mismatch - Static call argument type mismatch --- .../Laurel/ResolutionTypeCheckTests.lean | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean new file mode 100644 index 0000000000..01ccd40708 --- /dev/null +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -0,0 +1,149 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +/- +Tests that the resolution pass detects type checking errors — e.g. using an int +where a bool is expected, or passing the wrong type to a procedure. +-/ + +import StrataTest.Util.TestDiagnostics +import Strata.DDM.Elab +import Strata.DDM.BuiltinDialects.Init +import Strata.Languages.Laurel.Grammar.LaurelGrammar +import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator +import Strata.Languages.Laurel.Resolution + +open StrataTest.Util +open Strata +open Strata.Elab (parseStrataProgramFromDialect) + +namespace Strata.Laurel + +/-- Run only parsing + resolution and return diagnostics (no SMT verification). -/ +private def processResolution (input : Lean.Parser.InputContext) : IO (Array Diagnostic) := do + let dialects := Strata.Elab.LoadedDialects.ofDialects! #[initDialect, Laurel] + let strataProgram ← parseStrataProgramFromDialect dialects Laurel.name input + let uri := Strata.Uri.file input.fileName + match Laurel.TransM.run uri (Laurel.parseProgram strataProgram) with + | .error e => throw (IO.userError s!"Translation errors: {e}") + | .ok program => + let result := resolve program + let files := Map.insert Map.empty uri input.fileMap + return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray + +/-! ## Non-boolean condition in if-then-else -/ + +def ifCondNotBool := r" +function foo(x: int): int { + if x then 1 else 0 +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "IfCondNotBool" ifCondNotBool 39 processResolution + +/-! ## Non-boolean condition in assert -/ + +def assertCondNotBool := r" +procedure baz() opaque { + var x: int := 42; + assert x +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssertCondNotBool" assertCondNotBool 49 processResolution + +/-! ## Non-boolean condition in assume -/ + +def assumeCondNotBool := r" +procedure qux() opaque { + var x: int := 42; + assume x +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssumeCondNotBool" assumeCondNotBool 59 processResolution + +/-! ## Non-boolean operand in logical and -/ + +def logicalAndNotBool := r" +function foo(x: int, y: bool): bool { + x && y +//^^^^^^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "LogicalAndNotBool" logicalAndNotBool 69 processResolution + +/-! ## Assignment type mismatch -/ + +def assignTypeMismatch := r" +procedure foo() opaque { + var x: int := true +//^^^^^^^^^^^^^^^^^^ error: expected 'int', but got 'bool' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssignTypeMismatch" assignTypeMismatch 79 processResolution + +/-! ## Function return type mismatch -/ + +def returnTypeMismatch := r" +function foo(): int { +// ^^^ error: expected 'int', but got 'bool' + true +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "ReturnTypeMismatch" returnTypeMismatch 89 processResolution + +/-! ## Static call argument type mismatch -/ + +def callArgTypeMismatch := r" +function bar(x: int): int { x }; +function foo(): int { + bar(true) +//^^^^^^^^^ error: expected 'int', but got 'bool' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "CallArgTypeMismatch" callArgTypeMismatch 99 processResolution + +/-! ## Non-boolean condition in while loop -/ + +def whileCondNotBool := r" +procedure wh() opaque { + var x: int := 1; + while (x) { } +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "WhileCondNotBool" whileCondNotBool 109 processResolution + +/-! ## Non-numeric operand in comparison -/ + +def comparisonNotNumeric := r" +function cmp(x: string, y: int): bool { + x < y +//^^^^^ error: expected a numeric type, but got 'string' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "ComparisonNotNumeric" comparisonNotNumeric 121 processResolution + +end Laurel From 76ea8dfbf8ae711189974131c7d90988ed3b7e93 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:29:32 +0000 Subject: [PATCH 005/128] Add multi-output procedure in expression position check and test - Add checkSingleValued helper that detects MultiValuedExpr types used in expression position (e.g., as operands to PrimitiveOp) - Emit error: "Multi-output procedure '' used in expression position" - Add ResolutionTypeTests.lean with test for assert multi(1) == 1 --- Strata/Languages/Laurel/Resolution.lean | 17 +++++++ .../Languages/Laurel/ResolutionTypeTests.lean | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 StrataTest/Languages/Laurel/ResolutionTypeTests.lean diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index d87f97cd73..367259b9ac 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -399,6 +399,20 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } +/-- Check that an expression is single-valued (not a multi-output procedure call). + Emits an error if the expression has MultiValuedExpr type. -/ +private def checkSingleValued (expr : StmtExprMd) (ty : HighTypeMd) : ResolveM Unit := do + match ty.val with + | .MultiValuedExpr _ => + let calleeName := match expr.val with + | .StaticCall callee _ => callee.text + | .InstanceCall _ callee _ => callee.text + | _ => "expression" + let diag := diagnosticFromSource expr.source + s!"Multi-output procedure '{calleeName}' used in expression position" + modify fun s => { s with errors := s.errors.push diag } + | _ => pure () + /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -543,6 +557,9 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let results ← args.mapM resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) + -- Check that no argument is a multi-output procedure call + for (arg, argTy) in results do + checkSingleValued arg argTy let resultTy := match op with | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies | .Lt | .Leq | .Gt | .Geq => HighType.TBool diff --git a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean new file mode 100644 index 0000000000..b3d10b55f0 --- /dev/null +++ b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean @@ -0,0 +1,50 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +/- +Tests that the resolution pass detects type checking errors — e.g. using a +multi-output procedure in expression position. +-/ + +import StrataTest.Util.TestDiagnostics +import Strata.DDM.Elab +import Strata.DDM.BuiltinDialects.Init +import Strata.Languages.Laurel.Grammar.LaurelGrammar +import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator +import Strata.Languages.Laurel.Resolution + +open StrataTest.Util +open Strata +open Strata.Elab (parseStrataProgramFromDialect) + +namespace Strata.Laurel + +/-- Run only parsing + resolution and return diagnostics (no SMT verification). -/ +private def processResolution (input : Lean.Parser.InputContext) : IO (Array Diagnostic) := do + let dialects := Strata.Elab.LoadedDialects.ofDialects! #[initDialect, Laurel] + let strataProgram ← parseStrataProgramFromDialect dialects Laurel.name input + let uri := Strata.Uri.file input.fileName + match Laurel.TransM.run uri (Laurel.parseProgram strataProgram) with + | .error e => throw (IO.userError s!"Translation errors: {e}") + | .ok program => + let result := resolve program + let files := Map.insert Map.empty uri input.fileMap + return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray + +/-! ## Multi-output procedure used in expression position -/ + +def multiOutputInExpr := r" +procedure multi(x: int) returns (a: int, b: int) opaque; +procedure test() opaque { + assert multi(1) == 1 +// ^^^^^^^^ error: Multi-output procedure 'multi' used in expression position +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "MultiOutputInExpr" multiOutputInExpr 42 processResolution + +end Laurel From 0a26f1d6eb180cc153f9116a9bb83ae9913f06cb Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:48:41 +0000 Subject: [PATCH 006/128] Remove checkSingleValued; let type checks report multi-output errors naturally Instead of a dedicated 'Multi-output procedure used in expression position' error, multi-output calls in expression position now produce standard type mismatch errors like 'expected int, but got (int, int)'. - Remove checkSingleValued function and its call in PrimitiveOp - Remove MultiValuedExpr skip in checkAssignable - Add Eq/Neq operand compatibility check - Add formatType helper for nice MultiValuedExpr formatting - Skip assignment type check when arity already mismatches --- Strata/Languages/Laurel/Resolution.lean | 42 +++++++++---------- .../Languages/Laurel/ResolutionTypeTests.lean | 2 +- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 367259b9ac..cbedf8f53c 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -360,9 +360,17 @@ def resolveHighType (ty : HighTypeMd) : ResolveM HighTypeMd := do | other => pure other return { val := val', source := ty.source } +/-- Format a type for use in diagnostics. -/ +private def formatType (ty : HighTypeMd) : String := + match ty.val with + | .MultiValuedExpr tys => + let parts := tys.map (fun t => toString (formatHighTypeVal t.val)) + "(" ++ ", ".intercalate parts ++ ")" + | other => toString (formatHighTypeVal other) + /-- Emit a type mismatch diagnostic. -/ private def typeMismatch (source : Option FileRange) (expected : String) (actual : HighTypeMd) : ResolveM Unit := do - let actualStr := toString (formatHighTypeVal actual.val) + let actualStr := formatType actual let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } @@ -387,32 +395,17 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) match expected.val, actual.val with | .Unknown, _ => pure () | _, .Unknown => pure () - | _, .MultiValuedExpr _ => pure () -- arity mismatch already reported separately | .UserDefined _, _ => pure () -- subtype relationships not tracked here | _, .UserDefined _ => pure () -- subtype relationships not tracked here | .TCore _, _ => pure () -- pass-through Core types not checked during resolution | _, .TCore _ => pure () -- pass-through Core types not checked during resolution | _, _ => if !highEq expected actual then - let expectedStr := toString (formatHighTypeVal expected.val) - let actualStr := toString (formatHighTypeVal actual.val) + let expectedStr := formatType expected + let actualStr := formatType actual let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } -/-- Check that an expression is single-valued (not a multi-output procedure call). - Emits an error if the expression has MultiValuedExpr type. -/ -private def checkSingleValued (expr : StmtExprMd) (ty : HighTypeMd) : ResolveM Unit := do - match ty.val with - | .MultiValuedExpr _ => - let calleeName := match expr.val with - | .StaticCall callee _ => callee.text - | .InstanceCall _ callee _ => callee.text - | _ => "expression" - let diag := diagnosticFromSource expr.source - s!"Multi-output procedure '{calleeName}' used in expression position" - modify fun s => { s with errors := s.errors.push diag } - | _ => pure () - /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -515,7 +508,8 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) modify fun s => { s with errors := s.errors.push diag } -- Type check: for single-target assignments, check value type matches target type -- Skip when value type is void (RHS is a statement like while/return that doesn't produce a value) - if targets'.length == 1 && valueTy.val != HighType.TVoid then + -- Skip when there's an arity mismatch (already reported above) + if targets'.length == 1 && targets'.length == expectedOutputCount && valueTy.val != HighType.TVoid then if let some target := targets'.head? then let targetTy := match target.val with | .Local ref => do @@ -557,9 +551,6 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let results ← args.mapM resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) - -- Check that no argument is a multi-output procedure call - for (arg, argTy) in results do - checkSingleValued arg argTy let resultTy := match op with | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies | .Lt | .Leq | .Gt | .Geq => HighType.TBool @@ -574,7 +565,12 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) for aTy in argTypes do checkBool source aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => for aTy in argTypes do checkNumeric source aTy - | .Eq | .Neq | .StrConcat => pure () + | .Eq | .Neq => + -- Check that operands are compatible with each other + match argTypes with + | [lhsTy, rhsTy] => checkAssignable source rhsTy lhsTy + | _ => pure () + | .StrConcat => pure () pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source diff --git a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean index b3d10b55f0..89ac1a162c 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean @@ -40,7 +40,7 @@ def multiOutputInExpr := r" procedure multi(x: int) returns (a: int, b: int) opaque; procedure test() opaque { assert multi(1) == 1 -// ^^^^^^^^ error: Multi-output procedure 'multi' used in expression position +// ^^^^^^^^^^^^^ error: expected 'int', but got '(int, int)' }; " From 2acc758f5b79f3ca8281301602187296227850db Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Wed, 6 May 2026 17:07:52 +0000 Subject: [PATCH 007/128] Address review feedback: symmetric Eq/Neq errors, extract helper, consolidate tests - Add checkComparable helper for symmetric Eq/Neq error messages ("Operands of '==' have incompatible types 'X' and 'Y'") - Extract resolveStmtExprExpr helper to reduce repeated pattern - Add constant initializer type check in resolveConstant - Merge ResolutionTypeTests.lean into ResolutionTypeCheckTests.lean - Add tests: equality type mismatch, assignment target count mismatch, UserDefined pass-through (documents known limitation) - Update checkAssignable doc comment to mention TCore types --- Strata/Languages/Laurel/Resolution.lean | 54 ++++++--- .../Laurel/ResolutionTypeCheckTests.lean | 107 +++++++++++++----- .../Languages/Laurel/ResolutionTypeTests.lean | 50 -------- 3 files changed, 118 insertions(+), 93 deletions(-) delete mode 100644 StrataTest/Languages/Laurel/ResolutionTypeTests.lean diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index cbedf8f53c..4bfa2d39dc 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -390,7 +390,8 @@ private def checkNumeric (source : Option FileRange) (ty : HighTypeMd) : Resolve /-- Check that two types are compatible, emitting a diagnostic if not. UserDefined types are always considered compatible with each other since - subtype relationships (inheritance) are not tracked during resolution. -/ + subtype relationships (inheritance) are not tracked during resolution. + TCore types are not checked since they are pass-through types from the Core language. -/ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do match expected.val, actual.val with | .Unknown, _ => pure () @@ -406,6 +407,22 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } +/-- Check that two types are comparable (for == and !=), emitting a symmetric diagnostic if not. -/ +private def checkComparable (source : Option FileRange) (lhsTy : HighTypeMd) (rhsTy : HighTypeMd) : ResolveM Unit := do + match lhsTy.val, rhsTy.val with + | .Unknown, _ => pure () + | _, .Unknown => pure () + | .UserDefined _, _ => pure () + | _, .UserDefined _ => pure () + | .TCore _, _ => pure () + | _, .TCore _ => pure () + | _, _ => + if !highEq lhsTy rhsTy then + let lhsStr := formatType lhsTy + let rhsStr := formatType rhsTy + let diag := diagnosticFromSource source s!"Operands of '==' have incompatible types '{lhsStr}' and '{rhsStr}'" + modify fun s => { s with errors := s.errors.push diag } + /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -566,9 +583,9 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => for aTy in argTypes do checkNumeric source aTy | .Eq | .Neq => - -- Check that operands are compatible with each other + -- Check that operands are compatible with each other (symmetric check) match argTypes with - | [lhsTy, rhsTy] => checkAssignable source rhsTy lhsTy + | [lhsTy, rhsTy] => checkComparable source lhsTy rhsTy | _ => pure () | .StrConcat => pure () pure (.PrimitiveOp op args', { val := resultTy, source := source }) @@ -653,6 +670,11 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) termination_by exprMd decreasing_by all_goals term_by_mem +/-- Resolve a statement expression, discarding the synthesized type. + Use when only the resolved expression is needed (invariants, decreases, etc.). -/ +private def resolveStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do + let (e', _) ← resolveStmtExpr e; pure e' + /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do let ty' ← resolveHighType param.type @@ -666,12 +688,12 @@ def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do let (b', ty) ← resolveStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') - let impl' ← impl.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' - let mods' ← mods.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) + let impl' ← impl.mapM resolveStmtExprExpr + let mods' ← mods.mapM resolveStmtExprExpr return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) return (.Abstract posts', { val := .TVoid, source := none }) | .External => return (.External, { val := .TVoid, source := none }) @@ -681,8 +703,8 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') - let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) + let dec' ← proc.decreases.mapM resolveStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -696,7 +718,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -722,8 +744,8 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') - let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) + let dec' ← proc.decreases.mapM resolveStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -736,7 +758,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, @@ -805,7 +827,11 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let init' ← c.initializer.mapM fun e => do + let (e', eTy) ← resolveStmtExpr e + if eTy.val != HighType.TVoid then + checkAssignable e'.source ty' eTy + pure e' let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 01ccd40708..3a9fa8f174 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -34,7 +34,7 @@ private def processResolution (input : Lean.Parser.InputContext) : IO (Array Dia let files := Map.insert Map.empty uri input.fileMap return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray -/-! ## Non-boolean condition in if-then-else -/ +/-! ## Non-boolean conditions -/ def ifCondNotBool := r" function foo(x: int): int { @@ -44,9 +44,7 @@ function foo(x: int): int { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "IfCondNotBool" ifCondNotBool 39 processResolution - -/-! ## Non-boolean condition in assert -/ +#eval testInputWithOffset "IfCondNotBool" ifCondNotBool 44 processResolution def assertCondNotBool := r" procedure baz() opaque { @@ -57,9 +55,7 @@ procedure baz() opaque { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "AssertCondNotBool" assertCondNotBool 49 processResolution - -/-! ## Non-boolean condition in assume -/ +#eval testInputWithOffset "AssertCondNotBool" assertCondNotBool 54 processResolution def assumeCondNotBool := r" procedure qux() opaque { @@ -70,9 +66,20 @@ procedure qux() opaque { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "AssumeCondNotBool" assumeCondNotBool 59 processResolution +#eval testInputWithOffset "AssumeCondNotBool" assumeCondNotBool 64 processResolution + +def whileCondNotBool := r" +procedure wh() opaque { + var x: int := 1; + while (x) { } +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "WhileCondNotBool" whileCondNotBool 74 processResolution -/-! ## Non-boolean operand in logical and -/ +/-! ## Logical operator type checks -/ def logicalAndNotBool := r" function foo(x: int, y: bool): bool { @@ -82,9 +89,21 @@ function foo(x: int, y: bool): bool { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "LogicalAndNotBool" logicalAndNotBool 69 processResolution +#eval testInputWithOffset "LogicalAndNotBool" logicalAndNotBool 84 processResolution + +/-! ## Numeric operator type checks -/ + +def comparisonNotNumeric := r" +function cmp(x: string, y: int): bool { + x < y +//^^^^^ error: expected a numeric type, but got 'string' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "ComparisonNotNumeric" comparisonNotNumeric 94 processResolution -/-! ## Assignment type mismatch -/ +/-! ## Assignment type checks -/ def assignTypeMismatch := r" procedure foo() opaque { @@ -94,9 +113,9 @@ procedure foo() opaque { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "AssignTypeMismatch" assignTypeMismatch 79 processResolution +#eval testInputWithOffset "AssignTypeMismatch" assignTypeMismatch 104 processResolution -/-! ## Function return type mismatch -/ +/-! ## Function return type checks -/ def returnTypeMismatch := r" function foo(): int { @@ -106,9 +125,9 @@ function foo(): int { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "ReturnTypeMismatch" returnTypeMismatch 89 processResolution +#eval testInputWithOffset "ReturnTypeMismatch" returnTypeMismatch 114 processResolution -/-! ## Static call argument type mismatch -/ +/-! ## Call argument type checks -/ def callArgTypeMismatch := r" function bar(x: int): int { x }; @@ -119,31 +138,61 @@ function foo(): int { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "CallArgTypeMismatch" callArgTypeMismatch 99 processResolution +#eval testInputWithOffset "CallArgTypeMismatch" callArgTypeMismatch 124 processResolution -/-! ## Non-boolean condition in while loop -/ +/-! ## Equality operator type checks -/ -def whileCondNotBool := r" -procedure wh() opaque { - var x: int := 1; - while (x) { } -// ^ error: expected bool, but got 'int' +def equalityTypeMismatch := r" +function cmp(x: int, y: string): bool { + x == y +//^^^^^^ error: Operands of '==' have incompatible types 'int' and 'string' }; " #guard_msgs (error, drop all) in -#eval testInputWithOffset "WhileCondNotBool" whileCondNotBool 109 processResolution +#eval testInputWithOffset "EqualityTypeMismatch" equalityTypeMismatch 134 processResolution -/-! ## Non-numeric operand in comparison -/ +/-! ## Multi-output procedures -/ -def comparisonNotNumeric := r" -function cmp(x: string, y: int): bool { - x < y -//^^^^^ error: expected a numeric type, but got 'string' +def multiOutputInExpr := r" +procedure multi(x: int) returns (a: int, b: int) opaque; +procedure test() opaque { + assert multi(1) == 1 +// ^^^^^^^^^^^^^ error: Operands of '==' have incompatible types '(int, int)' and 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "MultiOutputInExpr" multiOutputInExpr 146 processResolution + +def assignTargetCountMismatch := r" +procedure multi() returns (a: int, b: int) opaque; +procedure test() opaque { + var x: int := multi() +//^^^^^^^^^^^^^^^^^^^^^ error: Assignment target count mismatch:1 targets but right-hand side produces 2 values +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssignTargetCountMismatch" assignTargetCountMismatch 156 processResolution + +/-! ## UserDefined type pass-through (known limitation) + +UserDefined types skip strict assignability checks because subtype/inheritance +relationships are not tracked during resolution. This test documents that +cross-type assignments are silently accepted today. When hierarchy tracking +lands, this test should be updated to expect a rejection. -/ + +def userDefinedPassThrough := r" +composite Dog { } +composite Cat { } +procedure test() opaque { + var x: Dog := new Cat }; " +-- This should produce NO diagnostics (UserDefined types are not checked against each other) #guard_msgs (error, drop all) in -#eval testInputWithOffset "ComparisonNotNumeric" comparisonNotNumeric 121 processResolution +#eval testInputWithOffset "UserDefinedPassThrough" userDefinedPassThrough 170 processResolution end Laurel diff --git a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean deleted file mode 100644 index 89ac1a162c..0000000000 --- a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean +++ /dev/null @@ -1,50 +0,0 @@ -/- - Copyright Strata Contributors - - SPDX-License-Identifier: Apache-2.0 OR MIT --/ - -/- -Tests that the resolution pass detects type checking errors — e.g. using a -multi-output procedure in expression position. --/ - -import StrataTest.Util.TestDiagnostics -import Strata.DDM.Elab -import Strata.DDM.BuiltinDialects.Init -import Strata.Languages.Laurel.Grammar.LaurelGrammar -import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator -import Strata.Languages.Laurel.Resolution - -open StrataTest.Util -open Strata -open Strata.Elab (parseStrataProgramFromDialect) - -namespace Strata.Laurel - -/-- Run only parsing + resolution and return diagnostics (no SMT verification). -/ -private def processResolution (input : Lean.Parser.InputContext) : IO (Array Diagnostic) := do - let dialects := Strata.Elab.LoadedDialects.ofDialects! #[initDialect, Laurel] - let strataProgram ← parseStrataProgramFromDialect dialects Laurel.name input - let uri := Strata.Uri.file input.fileName - match Laurel.TransM.run uri (Laurel.parseProgram strataProgram) with - | .error e => throw (IO.userError s!"Translation errors: {e}") - | .ok program => - let result := resolve program - let files := Map.insert Map.empty uri input.fileMap - return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray - -/-! ## Multi-output procedure used in expression position -/ - -def multiOutputInExpr := r" -procedure multi(x: int) returns (a: int, b: int) opaque; -procedure test() opaque { - assert multi(1) == 1 -// ^^^^^^^^^^^^^ error: expected 'int', but got '(int, int)' -}; -" - -#guard_msgs (error, drop all) in -#eval testInputWithOffset "MultiOutputInExpr" multiOutputInExpr 42 processResolution - -end Laurel From e243dbdcf67dbd69a55820a06a7b81c2ad8d1164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 09:44:39 -0400 Subject: [PATCH 008/128] add explanations about the typechecking routine added --- Strata/Languages/Laurel/Resolution.lean | 77 +++++++-- docs/verso/LaurelDoc.lean | 215 ++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 14 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 4bfa2d39dc..e7155a7ca8 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -13,24 +13,73 @@ import Strata.Languages.Python.PythonLaurelCorePrelude /-! # Name Resolution Pass -Assigns a unique numeric ID to every definition and reference node in a -Laurel program, then resolves references to their definitions. +Turns a freshly parsed Laurel `Program` (where every `Identifier` has +`uniqueId := none`) into a program where every definition has a fresh numeric +ID and every reference points to the ID of the definition it names. The pass +also synthesizes a `HighType` for every `StmtExpr` and emits diagnostics for +unresolved names, duplicate definitions, kind mismatches (e.g. using a +constant where a type is expected), and type mismatches. + +The entry point is `resolve`. It returns a `ResolutionResult` containing the +resolved program, a `SemanticModel` (the `refToDef` map and ID counters), and +the accumulated diagnostics. ## Design -The resolution pass operates in two phases: +The resolution pass operates in two phases. ### Phase 1: ID Assignment and Reference Resolution -Walks the AST, assigning fresh unique IDs to all definition nodes and -resolving references by looking up names in the current lexical scope. -After this phase, every definition and reference node has its `id` field -filled in. + +Walks the AST under `ResolveM`, a state monad over `ResolveState`. Phase 1: +- assigns fresh unique IDs to all definition nodes via `defineNameCheckDup`, +- resolves references by looking up names in the current lexical scope via + `resolveRef` (and `resolveFieldRef` for fields, which uses the target's + declared type to build a qualified lookup key), +- opens fresh nested scopes via `withScope` for blocks, quantifiers, + procedure bodies, and constrained-type constraint/witness expressions, +- synthesizes a `HighType` for every `StmtExpr` and runs the type-checking + helpers (`checkBool`, `checkNumeric`, `checkAssignable`, `checkComparable`) + on assignments, call arguments, condition positions, functional bodies, and + constant initializers. + +Before any bodies are walked, `preRegisterTopLevel` registers every top-level +name (types and their constructors / testers / destructors / instance +procedures / fields, constants, static procedures) into scope with a +placeholder `ResolvedNode`. The placeholders are overwritten with real nodes +as each definition is fully resolved. This is what allows declaration order to +not matter inside a Laurel program. + +When a reference fails to resolve, or a `UserDefined` type reference resolves +to the wrong kind, Phase 1 records the name as `ResolvedNode.unresolved` (or +the type as `HighType.Unknown`) and continues. Both are treated as wildcards +by the type checker, so subsequent uses do not produce cascading errors. + +After this phase, every definition and reference node has its `uniqueId` +field filled in. ### Phase 2: Build refToDef Map + Walks the *resolved* AST (where all definitions already have their UUIDs) -and builds a map from each definition's ID to its `ResolvedNode`. Because this -happens after Phase 1, the `ResolvedNode` values in the map contain the fully -resolved sub-trees (e.g. a procedure's parameters already have their IDs). +and builds a map from each definition's ID to its `ResolvedNode`. Because +this happens after Phase 1, the `ResolvedNode` values in the map contain the +fully resolved sub-trees (e.g. a procedure's parameters already have their +IDs). + +### Scopes + +Three forms of scope are maintained on `ResolveState`: +- `scope` — the current lexical scope, mapping name → `(uniqueId, ResolvedNode)`, + saved and restored by `withScope`. +- `currentScopeNames` — names defined at the current nesting level only, used + by `defineNameCheckDup` to detect duplicates. +- `typeScopes` — per-composite-type scopes mapping field names to scope + entries. Built by `resolveTypeDefinition` *before* descending into instance + procedures (and inheriting from `extending` parents), so that field + references inside method bodies can be resolved. +- `instanceTypeName` — when resolving inside an instance procedure, the + owning composite type's name. Used by `resolveFieldRef` as a fallback so + that a bare `self.field` reference resolves through the type scope when + `self` has type `Any`. ### Definition nodes (introduce a name into scope) - `Variable.Declare` — local variable declaration (in `Assign` targets or `Var`) @@ -51,10 +100,10 @@ resolved sub-trees (e.g. a procedure's parameters already have their IDs). - `StmtExpr.Exit` — exit a labelled block - `HighType.UserDefined` — type reference -Each of these nodes carries an `id : Nat` field (defaulting to `0`). -The ID assignment pass fills in unique values. The resolution pass then -builds a map from reference IDs to `ResolvedNode` values describing the -definition each reference resolves to. +Each of these nodes carries a `uniqueId : Option Nat` field (defaulting to +`none`). Phase 1 fills in unique values; Phase 2 then builds a map from +reference IDs to `ResolvedNode` values describing the definition each +reference resolves to. -/ namespace Strata.Laurel diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4d153eb439..ef6014580d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -146,6 +146,221 @@ A Laurel program consists of procedures, global variables, type definitions, and {docstring Strata.Laurel.Program} +# Type checking + +Type checking runs as part of the resolution pass, in `resolveStmtExpr`. Resolution +synthesizes a {name Strata.Laurel.HighType}`HighType` for every {name Strata.Laurel.StmtExpr}`StmtExpr` +bottom-up and emits diagnostics when the synthesized type clashes with what its context +requires. + +## Type system at a glance + +The checker is *synthesis-only* (no inference, no subtyping) over a flat type lattice, with +three _wildcard_ types that disable checking: + +- {name Strata.Laurel.HighType.Unknown}`Unknown` — synthesized when a name fails to resolve, + when a {name Strata.Laurel.HighType.UserDefined}`UserDefined` reference resolves to the + wrong kind, or for constructs whose result type isn't tracked + ({name Strata.Laurel.StmtExpr.This}`This`, + {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, + {name Strata.Laurel.StmtExpr.All}`All`, + {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf`, untyped + {name Strata.Laurel.StmtExpr.Hole}`Hole`). It is compatible with everything in both + directions (acts like _any_). +- {name Strata.Laurel.HighType.UserDefined}`UserDefined _` — also treated bivariantly. + Subtype/inheritance relationships aren't tracked here, and a + {name Strata.Laurel.HighType.UserDefined}`UserDefined` may be a constrained type wrapping a + primitive, so it's accepted wherever a primitive is expected. +- {name Strata.Laurel.HighType.TCore}`TCore _` — pass-through types from the Core language; + never checked. + +Everything else ({name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`, +{name Strata.Laurel.HighType.TBool}`TBool`, +{name Strata.Laurel.HighType.TString}`TString`, +{name Strata.Laurel.HighType.TVoid}`TVoid`, +{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [..]`) is compared by +*structural equality* via {name Strata.Laurel.highEq}`highEq`. There is no implicit numeric +promotion: {name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, and +{name Strata.Laurel.HighType.TFloat64}`TFloat64` are siblings, not a chain. + +{name Strata.Laurel.HighType.TVoid}`TVoid` marks expressions that produce no value +({name Strata.Laurel.StmtExpr.Return}`Return`, +{name Strata.Laurel.StmtExpr.Exit}`Exit`, +{name Strata.Laurel.StmtExpr.While}`While`, +{name Strata.Laurel.StmtExpr.Assert}`Assert`, +{name Strata.Laurel.StmtExpr.Assume}`Assume`, +{name Strata.Laurel.Variable.Declare}`Var Declare`, opaque/abstract/external bodies). +{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr tys` models the result of a +procedure call with multiple outputs. + +## Checking judgments + +Four helper checks fire from context positions: + +- `checkBool` — accepts {name Strata.Laurel.HighType.TBool}`TBool`, + {name Strata.Laurel.HighType.Unknown}`Unknown`, or any + {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by + {name Strata.Laurel.StmtExpr.IfThenElse}`if`/{name Strata.Laurel.StmtExpr.While}`while` + conditions, logical primitive ops, + {name Strata.Laurel.StmtExpr.Assert}`Assert`, and + {name Strata.Laurel.StmtExpr.Assume}`Assume`. +- `checkNumeric` — accepts {name Strata.Laurel.HighType.TInt}`TInt`, + {name Strata.Laurel.HighType.TReal}`TReal`, + {name Strata.Laurel.HighType.TFloat64}`TFloat64`, + {name Strata.Laurel.HighType.Unknown}`Unknown`, or any + {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by arithmetic and ordering + primitive ops. +- `checkAssignable expected actual` — accepts equality under + {name Strata.Laurel.highEq}`highEq`, *or* either side being + {name Strata.Laurel.HighType.Unknown}`Unknown` / + {name Strata.Laurel.HighType.UserDefined}`UserDefined` / + {name Strata.Laurel.HighType.TCore}`TCore`. Used by assignment, call arguments, functional + body vs. declared output, and constant initializers. +- `checkComparable` — same wildcards as `checkAssignable`, but with a symmetric error message. + Used for the operands of {name Strata.Laurel.Operation.Eq}`==` and + {name Strata.Laurel.Operation.Neq}`!=`. + +The {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out in `checkBool` and +`checkNumeric` is conservative on purpose: a constrained type might wrap a +{name Strata.Laurel.HighType.TBool}`bool` or a numeric type. + +## Synthesis rules + +Literals synthesize their obvious primitive types: integers give +{name Strata.Laurel.HighType.TInt}`TInt`, booleans +{name Strata.Laurel.HighType.TBool}`TBool`, strings +{name Strata.Laurel.HighType.TString}`TString`, decimals +{name Strata.Laurel.HighType.TReal}`TReal`. Variable and field references take their type +from scope; a {name Strata.Laurel.Variable.Declare}`Var (.Declare p)` synthesizes +{name Strata.Laurel.HighType.TVoid}`TVoid` because it is a declaration statement. + +Control flow: +- {name Strata.Laurel.StmtExpr.IfThenElse}`if c then t else e_1; …; e_n` — `c` is checked + against bool; the result type is the _then_-branch type. Else-branch types are discarded. +- {name Strata.Laurel.StmtExpr.Block}`Block [s_1; …; s_n]` — the type is the last + statement's type, or {name Strata.Laurel.HighType.TVoid}`TVoid` if empty. This is what makes + a transparent functional body usable as a value. +- {name Strata.Laurel.StmtExpr.While}`While`, + {name Strata.Laurel.StmtExpr.Exit}`Exit`, + {name Strata.Laurel.StmtExpr.Return}`Return _`, + {name Strata.Laurel.StmtExpr.Assert}`Assert`, + {name Strata.Laurel.StmtExpr.Assume}`Assume` — all synthesize + {name Strata.Laurel.HighType.TVoid}`TVoid`. The condition positions of + {name Strata.Laurel.StmtExpr.While}`While`, + {name Strata.Laurel.StmtExpr.Assert}`Assert`, and + {name Strata.Laurel.StmtExpr.Assume}`Assume` enforce `checkBool`. + +Calls ({name Strata.Laurel.StmtExpr.StaticCall}`StaticCall`, +{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall`) synthesize each argument, then apply +`checkAssignable param arg` pairwise. +{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall` drops the first parameter (the +implicit `self`). The return type is determined as follows: +- procedure with one output → that output's type +- procedure with `n ≠ 1` outputs → + {name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [t_1, …, t_n]` +- datatype constructor whose name contains `..is` → + {name Strata.Laurel.HighType.TBool}`TBool` (testers) +- other datatype constructors → {name Strata.Laurel.HighType.UserDefined}`UserDefined T` +- parameters or constants in callee position → their declared type +- anything else → {name Strata.Laurel.HighType.Unknown}`Unknown` + +Primitive ops (see {name Strata.Laurel.Operation}`Operation`): +- {name Strata.Laurel.Operation.And}`And`, + {name Strata.Laurel.Operation.Or}`Or`, + {name Strata.Laurel.Operation.AndThen}`AndThen`, + {name Strata.Laurel.Operation.OrElse}`OrElse`, + {name Strata.Laurel.Operation.Not}`Not`, + {name Strata.Laurel.Operation.Implies}`Implies` — operands `checkBool`; result + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.Operation.Lt}`Lt`, + {name Strata.Laurel.Operation.Leq}`Leq`, + {name Strata.Laurel.Operation.Gt}`Gt`, + {name Strata.Laurel.Operation.Geq}`Geq` — operands `checkNumeric`; result + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.Operation.Eq}`Eq`, + {name Strata.Laurel.Operation.Neq}`Neq` — `checkComparable lhs rhs` (binary only); result + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.Operation.Neg}`Neg`, + {name Strata.Laurel.Operation.Add}`Add`, + {name Strata.Laurel.Operation.Sub}`Sub`, + {name Strata.Laurel.Operation.Mul}`Mul`, + {name Strata.Laurel.Operation.Div}`Div`, + {name Strata.Laurel.Operation.Mod}`Mod`, + {name Strata.Laurel.Operation.DivT}`DivT`, + {name Strata.Laurel.Operation.ModT}`ModT` — operands `checkNumeric`; result is the type of + the first argument. +- {name Strata.Laurel.Operation.StrConcat}`StrConcat` — no operand check; result + {name Strata.Laurel.HighType.TString}`TString`. + +The _result is the type of the first argument_ rule is how arithmetic handles +{name Strata.Laurel.HighType.TInt}`TInt` / {name Strata.Laurel.HighType.TReal}`TReal` / +{name Strata.Laurel.HighType.TFloat64}`TFloat64` without a unification step. A consequence: +`int + real` will not be flagged, since each operand individually passes `checkNumeric`. + +Other forms: +- {name Strata.Laurel.StmtExpr.New}`New T` synthesizes + {name Strata.Laurel.HighType.UserDefined}`UserDefined T`, falling back to + {name Strata.Laurel.HighType.Unknown}`Unknown` if `T` resolved to the wrong kind. +- {name Strata.Laurel.StmtExpr.AsType}`AsType e T` synthesizes `T`. + {name Strata.Laurel.StmtExpr.IsType}`IsType _ _` and + {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals` synthesize + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`, + {name Strata.Laurel.StmtExpr.Assigned}`Assigned`, + {name Strata.Laurel.StmtExpr.Fresh}`Fresh` synthesize + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.StmtExpr.Old}`Old e` and + {name Strata.Laurel.StmtExpr.ProveBy}`ProveBy val proof` propagate the type of their first + sub-expression. {name Strata.Laurel.StmtExpr.PureFieldUpdate}`PureFieldUpdate target …` + propagates the type of `target`. +- {name Strata.Laurel.StmtExpr.Hole}`Hole _ (some T)` synthesizes `T`. + {name Strata.Laurel.StmtExpr.Hole}`Hole _ none`, + {name Strata.Laurel.StmtExpr.This}`This`, + {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, + {name Strata.Laurel.StmtExpr.All}`All`, and + {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf` synthesize + {name Strata.Laurel.HighType.Unknown}`Unknown`. + +## Checking positions + +There is no separate checking mode — checking happens by synthesizing and then invoking one of +the four helpers above. The places that check: + +1. *Assignment.* Target count must equal RHS arity + ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr` length, else 1), suppressed + when RHS is {name Strata.Laurel.HighType.TVoid}`TVoid`. When single-target and arities + match, `checkAssignable target_ty value_ty` runs. +2. *Call arguments.* `checkAssignable param_ty arg_ty` for each pair (instance calls skip + `self`). +3. *Functional procedure body.* When a {name Strata.Laurel.Procedure}`Procedure` is + `isFunctional`, has a transparent body, exactly one output, and the body type is not + {name Strata.Laurel.HighType.TVoid}`TVoid`, `checkAssignable output_ty body_ty` runs. +4. *Constant initializer.* `checkAssignable declared_ty init_ty`, skipped when the + initializer is {name Strata.Laurel.HighType.TVoid}`TVoid`. + +## Summary + +In type-system terms, the checker is: + +- *monomorphic, structurally-equal, no-subtyping* over primitive types, +- with a *gradual / dynamic escape hatch* — {name Strata.Laurel.HighType.Unknown}`Unknown`, + {name Strata.Laurel.HighType.UserDefined}`UserDefined`, and + {name Strata.Laurel.HighType.TCore}`TCore` are bivariantly compatible with everything, so + unresolved names, user-defined types, and Core types never produce spurious mismatches, +- in *synthesis-only direction* (no contextual checking flowing into expressions), +- with *arity tracking via tuple types* + ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`) for multi-output + procedures, +- and *side-effecting expressions modeled as* + {name Strata.Laurel.HighType.TVoid}`TVoid` so blocks, returns, and loops compose cleanly. + +The wildcard carve-outs are the dominant design choice: the checker's behavior on +user-defined and unresolved-kind code is essentially _anything goes_, and strict checking +applies only between the built-in primitive types. + # Translation Pipeline Laurel programs are verified by translating them to Strata Core and then invoking the Core From 8cb4ab2979e78f653550a19854fbd965915a9a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 11:54:47 -0400 Subject: [PATCH 009/128] bidirectional type checking first implementation : blocks --- Strata/Languages/Laurel/Resolution.lean | 155 ++++++++++++++++-------- 1 file changed, 102 insertions(+), 53 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index e7155a7ca8..b423e09304 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -423,6 +423,19 @@ private def typeMismatch (source : Option FileRange) (expected : String) (actual let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } +/-- Subtyping. Stub: structural equality via `highEq`. + TODO: To be replaced with a real check that walks `extending` chains for composites, unfolds aliases, and unwraps constrained types to their base. -/ +private def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup + +/-- Gradual consistency-subtyping (Siek–Taha style): `Unknown` is the dynamic + type and is consistent with everything in either direction. `TCore` is a + migration escape hatch and is bivariantly compatible for now. -/ +private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := + match sub.val, sup.val with + | .Unknown, _ | _, .Unknown => true + | .TCore _, _ | _, .TCore _ => true + | _, _ => isSubtype sub sup + /-- Check that a type is boolean, emitting a diagnostic if not. -/ private def checkBool (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do match ty.val with @@ -503,38 +516,41 @@ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List Hig | some (_, .constant c) => pure (c.type, []) | _ => pure ({ val := .Unknown, source := callee.source }, []) -def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do +def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match _: exprMd with | AstNode.mk expr source => let (val', ty) ← match _: expr with | .IfThenElse cond thenBr elseBr => - let (cond', condTy) ← resolveStmtExpr cond + let (cond', condTy) ← synthStmtExpr cond checkBool cond'.source condTy - let (thenBr', thenTy) ← resolveStmtExpr thenBr + let (thenBr', thenTy) ← synthStmtExpr thenBr let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') + let (e', _) ← synthStmtExpr a.val; pure e') pure (.IfThenElse cond' thenBr' elseBr', thenTy) | .Block stmts label => + -- Synth-mode block: non-last statements have their synthesized type discarded + -- (lax rule, matches Java/Python/JS expression-statement semantics). + -- The last statement's synthesized type becomes the block's type. withScope do - let results ← stmts.mapM resolveStmtExpr + let results ← stmts.mapM synthStmtExpr let stmts' := results.map (·.1) let lastTy := match results.getLast? with | some (_, ty) => ty | none => { val := .TVoid, source := source } pure (.Block stmts' label, lastTy) | .While cond invs dec body => - let (cond', condTy) ← resolveStmtExpr cond + let (cond', condTy) ← synthStmtExpr cond checkBool cond'.source condTy let invs' ← invs.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') + let (e', _) ← synthStmtExpr a.val; pure e') let dec' ← dec.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') - let (body', _) ← resolveStmtExpr body + let (e', _) ← synthStmtExpr a.val; pure e') + let (body', _) ← synthStmtExpr body pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) | .Return val => do let val' ← val.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') + let (e', _) ← synthStmtExpr a.val; pure e') pure (.Return val', { val := .TVoid, source := source }) | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) @@ -556,14 +572,14 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← resolveStmtExpr value + let (value', valueTy) ← synthStmtExpr value -- Check that LHS target count matches the RHS arity (derived from the value type). let expectedOutputCount := match valueTy.val with | .MultiValuedExpr tys => tys.length @@ -593,19 +609,19 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) checkAssignable source tTy valueTy pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source let ty ← getVarType fieldName pure (.Var (.Field target' fieldName'), ty) | .PureFieldUpdate target fieldName newVal => - let (target', targetTy) ← resolveStmtExpr target + let (target', targetTy) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let (newVal', _) ← resolveStmtExpr newVal + let (newVal', _) ← synthStmtExpr newVal pure (.PureFieldUpdate target' fieldName' newVal', targetTy) | .StaticCall callee args => let callee' ← resolveRef callee source (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let results ← args.mapM resolveStmtExpr + let results ← args.mapM synthStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -614,7 +630,7 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) checkAssignable source paramTy argTy pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => - let results ← args.mapM resolveStmtExpr + let results ← args.mapM synthStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let resultTy := match op with @@ -652,22 +668,22 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) pure (.New ref', ty) | .This => pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => - let (lhs', _) ← resolveStmtExpr lhs - let (rhs', _) ← resolveStmtExpr rhs + let (lhs', _) ← synthStmtExpr lhs + let (rhs', _) ← synthStmtExpr rhs pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let ty' ← resolveHighType ty pure (.AsType target' ty', ty') | .IsType target ty => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let ty' ← resolveHighType ty pure (.IsType target' ty', { val := .TBool, source := source }) | .InstanceCall target callee args => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let callee' ← resolveRef callee source (expected := #[.instanceProcedure, .staticProcedure]) - let results ← args.mapM resolveStmtExpr + let results ← args.mapM synthStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -681,32 +697,32 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let paramTy' ← resolveHighType param.type let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do - let (e', _) ← resolveStmtExpr pv.val; pure e') - let (body', _) ← resolveStmtExpr body + let (e', _) ← synthStmtExpr pv.val; pure e') + let (body', _) ← synthStmtExpr body pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) | .Assigned name => - let (name', _) ← resolveStmtExpr name + let (name', _) ← synthStmtExpr name pure (.Assigned name', { val := .TBool, source := source }) | .Old val => - let (val', valTy) ← resolveStmtExpr val + let (val', valTy) ← synthStmtExpr val pure (.Old val', valTy) | .Fresh val => - let (val', _) ← resolveStmtExpr val + let (val', _) ← synthStmtExpr val pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => - let (cond', condTy) ← resolveStmtExpr condExpr + let (cond', condTy) ← synthStmtExpr condExpr checkBool cond'.source condTy pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) | .Assume cond => - let (cond', condTy) ← resolveStmtExpr cond + let (cond', condTy) ← synthStmtExpr cond checkBool cond'.source condTy pure (.Assume cond', { val := .TVoid, source := source }) | .ProveBy val proof => - let (val', valTy) ← resolveStmtExpr val - let (proof', _) ← resolveStmtExpr proof + let (val', valTy) ← synthStmtExpr val + let (proof', _) ← synthStmtExpr proof pure (.ProveBy val' proof', valTy) | .ContractOf ty fn => - let (fn', _) ← resolveStmtExpr fn + let (fn', _) ← synthStmtExpr fn pure (.ContractOf ty fn', { val := .Unknown, source := source }) | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) | .All => pure (.All, { val := .Unknown, source := source }) @@ -721,8 +737,45 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) /-- Resolve a statement expression, discarding the synthesized type. Use when only the resolved expression is needed (invariants, decreases, etc.). -/ -private def resolveStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do - let (e', _) ← resolveStmtExpr e; pure e' +private def synthStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do + let (e', _) ← synthStmtExpr e; pure e' + +/-- Check-mode resolution: resolve `e` and verify its type is a consistent + subtype of `expected`. Bidirectional rules for individual constructs push + `expected` into subexpressions; everything else falls back to subsumption + (synth, then `isConsistentSubtype actual expected`). -/ +def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do + match _: exprMd with + | AstNode.mk expr source => + match _: expr with + | .Block stmts label => + -- Bespoke check rule: discard non-last statement types (lax), push + -- `expected` into the last statement. Empty block reduces to subsumption + -- of TVoid against `expected`. + -- The init traversal calls `synthStmtExpr`, a different function, so it + -- needs no termination proof; only the recursive `checkStmtExpr last` + -- call needs `last ∈ stmts`, supplied by `List.mem_of_getLast?`. + withScope do + let init' ← stmts.dropLast.mapM (fun s => do + let (s', _) ← synthStmtExpr s; pure s') + match _lastResult: stmts.getLast? with + | none => + let tvoid : HighTypeMd := { val := .TVoid, source := source } + unless isConsistentSubtype tvoid expected do + typeMismatch source (formatType expected) tvoid + pure { val := .Block init' label, source := source } + | some last => + have := List.mem_of_getLast? _lastResult + let last' ← checkStmtExpr last expected + pure { val := .Block (init' ++ [last']) label, source := source } + | _ => + -- Subsumption fallback: synth then check `actual <: expected`. + let (e', actual) ← synthStmtExpr exprMd + unless isConsistentSubtype actual expected do + typeMismatch source (formatType expected) actual + pure e' + termination_by exprMd + decreasing_by all_goals term_by_mem /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do @@ -734,15 +787,15 @@ def resolveParameter (param : Parameter) : ResolveM Parameter := do def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do match body with | .Transparent b => - let (b', ty) ← resolveStmtExpr b + let (b', ty) ← synthStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) - let impl' ← impl.mapM resolveStmtExprExpr - let mods' ← mods.mapM resolveStmtExprExpr + let posts' ← posts.mapM (·.mapM synthStmtExprExpr) + let impl' ← impl.mapM synthStmtExprExpr + let mods' ← mods.mapM synthStmtExprExpr return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) + let posts' ← posts.mapM (·.mapM synthStmtExprExpr) return (.Abstract posts', { val := .TVoid, source := none }) | .External => return (.External, { val := .TVoid, source := none }) @@ -752,8 +805,8 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) - let dec' ← proc.decreases.mapM resolveStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) + let dec' ← proc.decreases.mapM synthStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -767,7 +820,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -793,8 +846,8 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) - let dec' ← proc.decreases.mapM resolveStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) + let dec' ← proc.decreases.mapM synthStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -807,7 +860,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, @@ -849,8 +902,8 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do -- in scope when resolving the constraint and witness expressions. let (valueName', constraint', witness') ← withScope do let valueName' ← defineNameCheckDup ct.valueName (.quantifierVar ct.valueName base') - let (constraint', _) ← resolveStmtExpr ct.constraint - let (witness', _) ← resolveStmtExpr ct.witness + let (constraint', _) ← synthStmtExpr ct.constraint + let (witness', _) ← synthStmtExpr ct.witness return (valueName', constraint', witness') return .Constrained { name := ctName', base := base', valueName := valueName', constraint := constraint', witness := witness' } @@ -876,11 +929,7 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM fun e => do - let (e', eTy) ← resolveStmtExpr e - if eTy.val != HighType.TVoid then - checkAssignable e'.source ty' eTy - pure e' + let init' ← c.initializer.mapM (checkStmtExpr · ty') let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } From fbaa911c4970a5dace98ca8d6a7fd638b9d4846e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 11:57:13 -0400 Subject: [PATCH 010/128] add resolution-only function discards the type synthesized --- Strata/Languages/Laurel/Resolution.lean | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index b423e09304..bd49bd8376 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -737,7 +737,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := /-- Resolve a statement expression, discarding the synthesized type. Use when only the resolved expression is needed (invariants, decreases, etc.). -/ -private def synthStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do +private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do let (e', _) ← synthStmtExpr e; pure e' /-- Check-mode resolution: resolve `e` and verify its type is a consistent @@ -790,12 +790,12 @@ def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do let (b', ty) ← synthStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM synthStmtExprExpr) - let impl' ← impl.mapM synthStmtExprExpr - let mods' ← mods.mapM synthStmtExprExpr + let posts' ← posts.mapM (·.mapM resolveStmtExpr) + let impl' ← impl.mapM resolveStmtExpr + let mods' ← mods.mapM resolveStmtExpr return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM synthStmtExprExpr) + let posts' ← posts.mapM (·.mapM resolveStmtExpr) return (.Abstract posts', { val := .TVoid, source := none }) | .External => return (.External, { val := .TVoid, source := none }) @@ -805,8 +805,8 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) - let dec' ← proc.decreases.mapM synthStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) + let dec' ← proc.decreases.mapM resolveStmtExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -820,7 +820,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -846,8 +846,8 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) - let dec' ← proc.decreases.mapM synthStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) + let dec' ← proc.decreases.mapM resolveStmtExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -860,7 +860,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, From 39c8dd5f644fa34d84627763cebc961676089d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 12:10:32 -0400 Subject: [PATCH 011/128] document type system --- docs/verso/LaurelDoc.lean | 402 +++++++++++++++++++------------------- 1 file changed, 200 insertions(+), 202 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index ef6014580d..9f89926f4a 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -148,218 +148,216 @@ A Laurel program consists of procedures, global variables, type definitions, and # Type checking -Type checking runs as part of the resolution pass, in `resolveStmtExpr`. Resolution -synthesizes a {name Strata.Laurel.HighType}`HighType` for every {name Strata.Laurel.StmtExpr}`StmtExpr` -bottom-up and emits diagnostics when the synthesized type clashes with what its context -requires. - -## Type system at a glance - -The checker is *synthesis-only* (no inference, no subtyping) over a flat type lattice, with -three _wildcard_ types that disable checking: - -- {name Strata.Laurel.HighType.Unknown}`Unknown` — synthesized when a name fails to resolve, - when a {name Strata.Laurel.HighType.UserDefined}`UserDefined` reference resolves to the - wrong kind, or for constructs whose result type isn't tracked - ({name Strata.Laurel.StmtExpr.This}`This`, - {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, - {name Strata.Laurel.StmtExpr.All}`All`, - {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf`, untyped - {name Strata.Laurel.StmtExpr.Hole}`Hole`). It is compatible with everything in both - directions (acts like _any_). -- {name Strata.Laurel.HighType.UserDefined}`UserDefined _` — also treated bivariantly. - Subtype/inheritance relationships aren't tracked here, and a - {name Strata.Laurel.HighType.UserDefined}`UserDefined` may be a constrained type wrapping a - primitive, so it's accepted wherever a primitive is expected. -- {name Strata.Laurel.HighType.TCore}`TCore _` — pass-through types from the Core language; - never checked. - -Everything else ({name Strata.Laurel.HighType.TInt}`TInt`, -{name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`, -{name Strata.Laurel.HighType.TBool}`TBool`, -{name Strata.Laurel.HighType.TString}`TString`, -{name Strata.Laurel.HighType.TVoid}`TVoid`, -{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [..]`) is compared by -*structural equality* via {name Strata.Laurel.highEq}`highEq`. There is no implicit numeric -promotion: {name Strata.Laurel.HighType.TInt}`TInt`, -{name Strata.Laurel.HighType.TReal}`TReal`, and -{name Strata.Laurel.HighType.TFloat64}`TFloat64` are siblings, not a chain. - -{name Strata.Laurel.HighType.TVoid}`TVoid` marks expressions that produce no value -({name Strata.Laurel.StmtExpr.Return}`Return`, +Type checking is woven into the resolution pass: every +{name Strata.Laurel.StmtExpr}`StmtExpr` gets a {name Strata.Laurel.HighType}`HighType`, and +mismatches against the surrounding context become diagnostics. The design is +*bidirectional*: each construct is resolved either in *synthesis* mode — return a type +inferred from the expression — or in *checking* mode — verify that the expression has a +given expected type. The two are different functions on +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. + +This page describes the design choices behind the checker. The implementation is in +`Resolution.lean`. + +## The two judgments + +There are two operations on expressions, written here in standard bidirectional notation: + +``` +Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) +Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) +``` + +Each construct picks a mode based on whether its type is determined locally (synth) or by +context (check). Mode assignment is part of the design — see _Mode assignment per construct_ +below. + +The two judgments are connected by a single change-of-direction rule, *subsumption*: + +``` +Γ ⊢ e ⇒ A A <: B +───────────────────── (sub) + Γ ⊢ e ⇐ B +``` + +Subsumption is the *only* place the checker switches from check to synth mode. It fires as a +default fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct +without a bespoke check rule: synthesize the expression's type, then verify the result is a +subtype of the expected type. Bespoke check rules push the expected type *into* +subexpressions instead of bouncing through synthesis, which keeps error messages localized +and lets the expected type propagate through nested control flow. + +## Subtyping and gradual consistency + +The relation `<:` is implemented by two Lean functions — both currently stubs, both +intended to be sharpened: + +- `isSubtype` — pure subtyping. The stub is structural + equality via {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the + `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds + {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps + {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. +- `isConsistentSubtype` — gradual consistency, in + the Siek–Taha sense. {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type + `?` and is consistent with everything in either direction; otherwise the relation + delegates to `isSubtype`. {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly + consistent for now, as a clearly-labelled migration escape hatch from the Core language — + this carve-out is intentionally temporary. + +Subsumption (and every bespoke check rule) uses +`isConsistentSubtype`, never raw `isSubtype`. That +single choice is what makes the system *gradual*: an expression of type +{name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) +flows freely into any typed slot, and any expression flows freely into a slot of type +{name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between +fully-known types only. + +## What changed from the synth-only design + +A previous iteration was synth-only with three *bivariantly-compatible* wildcards: +{name Strata.Laurel.HighType.Unknown}`Unknown`, +{name Strata.Laurel.HighType.UserDefined}`UserDefined`, and +{name Strata.Laurel.HighType.TCore}`TCore`. The +{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was particularly +load-bearing: it meant that *no* assignment, call argument, or comparison involving a user +type was ever rejected, because subtyping wasn't tracked at all and constrained types +weren't unwrapped — we couldn't tell what was safe. + +The bidirectional design replaces that with two cleanly-separated concerns: + +- {name Strata.Laurel.HighType.Unknown}`Unknown` keeps wildcard semantics, but now as a + *real* semantic claim (gradual typing) rather than a workaround. +- {name Strata.Laurel.HighType.UserDefined}`UserDefined` becomes a regular type. Once + `isSubtype` is implemented properly, `Cat ≤ Animal` will + pass, `Cat ≤ Dog` will fail, and constrained types will be unwrappable to their base. The + current stub is conservative (structural equality only); it can be tightened + incrementally without changing any callers. + +## Block and `TVoid` + +Statement-position constructs that produce no value synthesize +{name Strata.Laurel.HighType.TVoid}`TVoid`: +{name Strata.Laurel.StmtExpr.Return}`Return`, {name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, {name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, -{name Strata.Laurel.Variable.Declare}`Var Declare`, opaque/abstract/external bodies). -{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr tys` models the result of a -procedure call with multiple outputs. - -## Checking judgments - -Four helper checks fire from context positions: - -- `checkBool` — accepts {name Strata.Laurel.HighType.TBool}`TBool`, - {name Strata.Laurel.HighType.Unknown}`Unknown`, or any - {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by - {name Strata.Laurel.StmtExpr.IfThenElse}`if`/{name Strata.Laurel.StmtExpr.While}`while` - conditions, logical primitive ops, - {name Strata.Laurel.StmtExpr.Assert}`Assert`, and - {name Strata.Laurel.StmtExpr.Assume}`Assume`. -- `checkNumeric` — accepts {name Strata.Laurel.HighType.TInt}`TInt`, - {name Strata.Laurel.HighType.TReal}`TReal`, - {name Strata.Laurel.HighType.TFloat64}`TFloat64`, - {name Strata.Laurel.HighType.Unknown}`Unknown`, or any - {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by arithmetic and ordering - primitive ops. -- `checkAssignable expected actual` — accepts equality under - {name Strata.Laurel.highEq}`highEq`, *or* either side being - {name Strata.Laurel.HighType.Unknown}`Unknown` / - {name Strata.Laurel.HighType.UserDefined}`UserDefined` / - {name Strata.Laurel.HighType.TCore}`TCore`. Used by assignment, call arguments, functional - body vs. declared output, and constant initializers. -- `checkComparable` — same wildcards as `checkAssignable`, but with a symmetric error message. - Used for the operands of {name Strata.Laurel.Operation.Eq}`==` and - {name Strata.Laurel.Operation.Neq}`!=`. - -The {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out in `checkBool` and -`checkNumeric` is conservative on purpose: a constrained type might wrap a -{name Strata.Laurel.HighType.TBool}`bool` or a numeric type. - -## Synthesis rules - -Literals synthesize their obvious primitive types: integers give -{name Strata.Laurel.HighType.TInt}`TInt`, booleans -{name Strata.Laurel.HighType.TBool}`TBool`, strings -{name Strata.Laurel.HighType.TString}`TString`, decimals -{name Strata.Laurel.HighType.TReal}`TReal`. Variable and field references take their type -from scope; a {name Strata.Laurel.Variable.Declare}`Var (.Declare p)` synthesizes -{name Strata.Laurel.HighType.TVoid}`TVoid` because it is a declaration statement. - -Control flow: -- {name Strata.Laurel.StmtExpr.IfThenElse}`if c then t else e_1; …; e_n` — `c` is checked - against bool; the result type is the _then_-branch type. Else-branch types are discarded. -- {name Strata.Laurel.StmtExpr.Block}`Block [s_1; …; s_n]` — the type is the last - statement's type, or {name Strata.Laurel.HighType.TVoid}`TVoid` if empty. This is what makes - a transparent functional body usable as a value. -- {name Strata.Laurel.StmtExpr.While}`While`, - {name Strata.Laurel.StmtExpr.Exit}`Exit`, - {name Strata.Laurel.StmtExpr.Return}`Return _`, - {name Strata.Laurel.StmtExpr.Assert}`Assert`, - {name Strata.Laurel.StmtExpr.Assume}`Assume` — all synthesize - {name Strata.Laurel.HighType.TVoid}`TVoid`. The condition positions of - {name Strata.Laurel.StmtExpr.While}`While`, - {name Strata.Laurel.StmtExpr.Assert}`Assert`, and - {name Strata.Laurel.StmtExpr.Assume}`Assume` enforce `checkBool`. - -Calls ({name Strata.Laurel.StmtExpr.StaticCall}`StaticCall`, -{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall`) synthesize each argument, then apply -`checkAssignable param arg` pairwise. -{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall` drops the first parameter (the -implicit `self`). The return type is determined as follows: -- procedure with one output → that output's type -- procedure with `n ≠ 1` outputs → - {name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [t_1, …, t_n]` -- datatype constructor whose name contains `..is` → - {name Strata.Laurel.HighType.TBool}`TBool` (testers) -- other datatype constructors → {name Strata.Laurel.HighType.UserDefined}`UserDefined T` -- parameters or constants in callee position → their declared type -- anything else → {name Strata.Laurel.HighType.Unknown}`Unknown` - -Primitive ops (see {name Strata.Laurel.Operation}`Operation`): -- {name Strata.Laurel.Operation.And}`And`, - {name Strata.Laurel.Operation.Or}`Or`, - {name Strata.Laurel.Operation.AndThen}`AndThen`, - {name Strata.Laurel.Operation.OrElse}`OrElse`, - {name Strata.Laurel.Operation.Not}`Not`, - {name Strata.Laurel.Operation.Implies}`Implies` — operands `checkBool`; result - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.Operation.Lt}`Lt`, - {name Strata.Laurel.Operation.Leq}`Leq`, - {name Strata.Laurel.Operation.Gt}`Gt`, - {name Strata.Laurel.Operation.Geq}`Geq` — operands `checkNumeric`; result - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.Operation.Eq}`Eq`, - {name Strata.Laurel.Operation.Neq}`Neq` — `checkComparable lhs rhs` (binary only); result - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.Operation.Neg}`Neg`, - {name Strata.Laurel.Operation.Add}`Add`, - {name Strata.Laurel.Operation.Sub}`Sub`, - {name Strata.Laurel.Operation.Mul}`Mul`, - {name Strata.Laurel.Operation.Div}`Div`, - {name Strata.Laurel.Operation.Mod}`Mod`, - {name Strata.Laurel.Operation.DivT}`DivT`, - {name Strata.Laurel.Operation.ModT}`ModT` — operands `checkNumeric`; result is the type of - the first argument. -- {name Strata.Laurel.Operation.StrConcat}`StrConcat` — no operand check; result - {name Strata.Laurel.HighType.TString}`TString`. - -The _result is the type of the first argument_ rule is how arithmetic handles -{name Strata.Laurel.HighType.TInt}`TInt` / {name Strata.Laurel.HighType.TReal}`TReal` / -{name Strata.Laurel.HighType.TFloat64}`TFloat64` without a unification step. A consequence: -`int + real` will not be flagged, since each operand individually passes `checkNumeric`. - -Other forms: -- {name Strata.Laurel.StmtExpr.New}`New T` synthesizes - {name Strata.Laurel.HighType.UserDefined}`UserDefined T`, falling back to - {name Strata.Laurel.HighType.Unknown}`Unknown` if `T` resolved to the wrong kind. -- {name Strata.Laurel.StmtExpr.AsType}`AsType e T` synthesizes `T`. - {name Strata.Laurel.StmtExpr.IsType}`IsType _ _` and - {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals` synthesize - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`, - {name Strata.Laurel.StmtExpr.Assigned}`Assigned`, - {name Strata.Laurel.StmtExpr.Fresh}`Fresh` synthesize - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.StmtExpr.Old}`Old e` and - {name Strata.Laurel.StmtExpr.ProveBy}`ProveBy val proof` propagate the type of their first - sub-expression. {name Strata.Laurel.StmtExpr.PureFieldUpdate}`PureFieldUpdate target …` - propagates the type of `target`. -- {name Strata.Laurel.StmtExpr.Hole}`Hole _ (some T)` synthesizes `T`. - {name Strata.Laurel.StmtExpr.Hole}`Hole _ none`, - {name Strata.Laurel.StmtExpr.This}`This`, - {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, - {name Strata.Laurel.StmtExpr.All}`All`, and - {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf` synthesize - {name Strata.Laurel.HighType.Unknown}`Unknown`. - -## Checking positions - -There is no separate checking mode — checking happens by synthesizing and then invoking one of -the four helpers above. The places that check: - -1. *Assignment.* Target count must equal RHS arity - ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr` length, else 1), suppressed - when RHS is {name Strata.Laurel.HighType.TVoid}`TVoid`. When single-target and arities - match, `checkAssignable target_ty value_ty` runs. -2. *Call arguments.* `checkAssignable param_ty arg_ty` for each pair (instance calls skip - `self`). -3. *Functional procedure body.* When a {name Strata.Laurel.Procedure}`Procedure` is - `isFunctional`, has a transparent body, exactly one output, and the body type is not - {name Strata.Laurel.HighType.TVoid}`TVoid`, `checkAssignable output_ty body_ty` runs. -4. *Constant initializer.* `checkAssignable declared_ty init_ty`, skipped when the - initializer is {name Strata.Laurel.HighType.TVoid}`TVoid`. - -## Summary - -In type-system terms, the checker is: - -- *monomorphic, structurally-equal, no-subtyping* over primitive types, -- with a *gradual / dynamic escape hatch* — {name Strata.Laurel.HighType.Unknown}`Unknown`, - {name Strata.Laurel.HighType.UserDefined}`UserDefined`, and - {name Strata.Laurel.HighType.TCore}`TCore` are bivariantly compatible with everything, so - unresolved names, user-defined types, and Core types never produce spurious mismatches, -- in *synthesis-only direction* (no contextual checking flowing into expressions), +{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies. +This makes blocks compose cleanly: control-flow statements don't pollute a block's +synthesized type. + +A {name Strata.Laurel.StmtExpr.Block}`Block` is statement chaining `{ s_1; …; s_n }`. The +checker treats it permissively in two ways: + +1. *Non-last statements are not required to be {name Strata.Laurel.HighType.TVoid}`TVoid`.* + In synth mode their types are computed and discarded; in check mode they are still + synthesized rather than checked against `void`. This matches Java/Python/JavaScript + expression-statement semantics: `f(x);` where `f` returns a value is normal idiomatic + code, and forcing an explicit discard would be hostile to the imperative style Laurel + targets. The cost is that `5;` (a literal in statement position) is silently accepted; if + we ever want to flag that, it should land as a lint, not a type error. + +2. *The last statement is the block's type.* Empty blocks have type + {name Strata.Laurel.HighType.TVoid}`TVoid`. This is what lets a transparent functional + procedure body be `{ … some statements …; expr }`. + +In check mode, the bespoke `Block` rule pushes the expected type into the *last* statement +rather than checking the block's synthesized type at the boundary. This buys two things: +errors fire at the actual offending sub-expression (e.g. inside a deeply nested +{name Strata.Laurel.StmtExpr.IfThenElse}`if`), and the expected type keeps propagating +through nested {name Strata.Laurel.StmtExpr.Block}`Block` / +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / +{name Strata.Laurel.StmtExpr.Hole}`Hole` / +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to subsumption of +{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. + +## Mode assignment per construct + +The intended mode for each construct (some are still being converted to bidirectional in +the implementation): + +| Construct | Mode | Notes | +|---|---|---| +| Literals, `Var .Local`, `Var .Field`, `New T`, `IsType`, `ReferenceEquals`, `Quantifier`, `Assigned`, `Fresh`, `Hole _ (some T)`, `StaticCall`, `InstanceCall` | synth | type is determined locally | +| `Var .Declare`, `Exit`, `Return`, `While`, `Assert`, `Assume`, `Assign` | synth ⇒ {name Strata.Laurel.HighType.TVoid}`TVoid` | side-effecting; condition operands checked inward | +| `IfThenElse cond t e_opt` | bespoke check | `cond ⇐ TBool`; `t ⇐ T`; `e ⇐ T` if present | +| `Block` | bespoke check | `s_1..s_{n-1}` synth, `s_n ⇐ T`; synth uses last's synthesized type | +| `Hole _ none` | bespoke check | check mode succeeds with `expected`; synth mode → `Unknown` | +| `AsType e T` | synth ⇒ `T` | the cast is the user's claim; no check on `e` | +| `Old`, `ProveBy v _`, `PureFieldUpdate t _ _` | propagate type of subexpr | unchanged | +| `This`, `Abstract`, `All`, `ContractOf` | synth ⇒ {name Strata.Laurel.HighType.Unknown}`Unknown` | type not tracked | + +{name Strata.Laurel.StmtExpr.PrimitiveOp}`PrimitiveOp` operands are checked inward against +the operator's expected operand type ({name Strata.Laurel.HighType.TBool}`TBool` for +logical, numeric for arithmetic and ordering, {name Strata.Laurel.HighType.TString}`TString` +for `StrConcat`). {name Strata.Laurel.Operation.Eq}`Eq`/{name Strata.Laurel.Operation.Neq}`Neq` +synthesize both operands and require consistency in either direction +(`isConsistentSubtype l r ∨ isConsistentSubtype r l`). + +Arithmetic ops `Neg`/`Add`/…/`ModT` synthesize *the type of the first argument*. This is how +the checker handles {name Strata.Laurel.HighType.TInt}`TInt` / +{name Strata.Laurel.HighType.TReal}`TReal` / {name Strata.Laurel.HighType.TFloat64}`TFloat64` +without a unification step. A consequence: `int + real` is not flagged today, since each +operand passes the numeric check individually. A real fix would be a numeric-promotion or +unification rule; for now this is a known relaxation. + +## Two helpers for resolution sites + +Some positions (procedure preconditions, decreases, invariants, postconditions, modifies +clauses, constrained-type witness, etc.) need resolution to run but the type of the +expression is either uninteresting or already known by another path. They use: + +- {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` — the full synth API, returning + `(StmtExprMd × HighTypeMd)`. +- {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` — the check API, returning the resolved + expression and verifying its type is a consistent subtype of the expected type. +- `resolveStmtExpr` — a thin wrapper that calls + `synthStmtExpr` and discards the synthesized type. Used at sites where typing is not + enforced (verification annotations, modifies/reads clauses). + +The right principle is: when the position has a known expected type +({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for `decreases`, the +declared output for a constant initializer or a functional body), use +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. When it doesn't, use +`resolveStmtExpr`. {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` +itself is mostly an internal interface used by other rules. + +## Returns and the expected return type + +`Return e` synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` (the construct itself +produces no value), but the *value being returned* should be checked against the enclosing +procedure's declared output type. The intended design: thread the expected return type +through {name Strata.Laurel.ResolveState}`ResolveState`, set it from `proc.outputs` in +{name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` before resolving the +body, and have the `Return` rule push the expected type into its value via +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. This closes a soundness gap in the +synth-only design where `return 0` in a `bool`-returning procedure was not caught (because +the body's overall synthesized type was {name Strata.Laurel.HighType.TVoid}`TVoid` and the +body-vs-output check was skipped on `TVoid`). + +## What this is, in type-system terms + +The checker is: + +- *bidirectional*, with a single subsumption rule at the synth↔check boundary, +- with a *gradual* relation (`isConsistentSubtype`) + rather than a strict one — {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic + type, justified by Laurel's targeting of dynamic source languages, +- over a *nominal-with-stubs* subtype relation + (`isSubtype`) — currently structural equality, intended to + walk inheritance chains and unwrap aliases / constrained types, - with *arity tracking via tuple types* ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`) for multi-output procedures, - and *side-effecting expressions modeled as* {name Strata.Laurel.HighType.TVoid}`TVoid` so blocks, returns, and loops compose cleanly. -The wildcard carve-outs are the dominant design choice: the checker's behavior on -user-defined and unresolved-kind code is essentially _anything goes_, and strict checking -applies only between the built-in primitive types. +The wildcard carve-out for {name Strata.Laurel.HighType.UserDefined}`UserDefined` from the +previous design is gone — user-defined types are no longer a backdoor through the checker. +The {name Strata.Laurel.HighType.TCore}`TCore` carve-out is preserved for now as a +migration aid and is expected to be removed. # Translation Pipeline From 14d27b4be0371c835496148ead2e653e0fb13007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:26:06 -0400 Subject: [PATCH 012/128] ifthenelse type checking --- Strata/Languages/Laurel/Resolution.lean | 43 ++++++++++++++++--------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index bd49bd8376..97f6556331 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -516,17 +516,25 @@ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List Hig | some (_, .constant c) => pure (c.type, []) | _ => pure ({ val := .Unknown, source := callee.source }, []) +mutual def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match _: exprMd with | AstNode.mk expr source => let (val', ty) ← match _: expr with | .IfThenElse cond thenBr elseBr => - let (cond', condTy) ← synthStmtExpr cond - checkBool cond'.source condTy + -- Condition is checked against TBool. The result type is TVoid when the + -- else branch is absent (statement form: the then-branch's value is + -- discarded), otherwise the then-branch's synthesized type. We don't + -- compare the two branches against each other since statement-position + -- ifs commonly mix a value branch with a TVoid branch (return/exit). + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } let (thenBr', thenTy) ← synthStmtExpr thenBr let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do let (e', _) ← synthStmtExpr a.val; pure e') - pure (.IfThenElse cond' thenBr' elseBr', thenTy) + let resultTy := match elseBr with + | none => { val := .TVoid, source := source } + | some _ => thenTy + pure (.IfThenElse cond' thenBr' elseBr', resultTy) | .Block stmts label => -- Synth-mode block: non-last statements have their synthesized type discarded -- (lax rule, matches Java/Python/JS expression-statement semantics). @@ -732,13 +740,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.Hole det ty', ty') | none => pure (.Hole det none, { val := .Unknown, source := source }) return ({ val := val', source := source }, ty) - termination_by exprMd - decreasing_by all_goals term_by_mem - -/-- Resolve a statement expression, discarding the synthesized type. - Use when only the resolved expression is needed (invariants, decreases, etc.). -/ -private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do - let (e', _) ← synthStmtExpr e; pure e' + termination_by (exprMd, 0) + decreasing_by all_goals first + | (apply Prod.Lex.left; term_by_mem) + | (apply Prod.Lex.right; decide) /-- Check-mode resolution: resolve `e` and verify its type is a consistent subtype of `expected`. Bidirectional rules for individual constructs push @@ -752,11 +757,9 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE -- Bespoke check rule: discard non-last statement types (lax), push -- `expected` into the last statement. Empty block reduces to subsumption -- of TVoid against `expected`. - -- The init traversal calls `synthStmtExpr`, a different function, so it - -- needs no termination proof; only the recursive `checkStmtExpr last` - -- call needs `last ∈ stmts`, supplied by `List.mem_of_getLast?`. withScope do - let init' ← stmts.dropLast.mapM (fun s => do + let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do + have : s ∈ stmts := List.dropLast_subset stmts hMem let (s', _) ← synthStmtExpr s; pure s') match _lastResult: stmts.getLast? with | none => @@ -774,8 +777,16 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE unless isConsistentSubtype actual expected do typeMismatch source (formatType expected) actual pure e' - termination_by exprMd - decreasing_by all_goals term_by_mem + termination_by (exprMd, 1) + decreasing_by all_goals first + | (apply Prod.Lex.left; term_by_mem) + | (try subst_eqs; apply Prod.Lex.right; decide) +end + +/-- Resolve a statement expression, discarding the synthesized type. + Use when only the resolved expression is needed (invariants, decreases, etc.). -/ +private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do + let (e', _) ← synthStmtExpr e; pure e' /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do From d3750b4390a332917bf9e14cfc817ccceb7cd29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:28:11 -0400 Subject: [PATCH 013/128] document ifthenelse type checking --- docs/verso/LaurelDoc.lean | 57 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 9f89926f4a..c3d9a314f2 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -272,6 +272,61 @@ through nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to subsumption of {name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. +## IfThenElse + +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse cond t e_opt` has been converted to +*partial* bidirectional form. Today the implementation has a synth rule but reaches check +mode only through the subsumption fallback; a bespoke check rule that pushes the expected +type into both branches is the planned next step. + +The synth rule: + +- *Condition.* `cond` is checked against {name Strata.Laurel.HighType.TBool}`TBool` via a + recursive `checkStmtExpr cond TBool` call. This replaces the previous synth-then-`checkBool` + pattern with the clean bidirectional one — the expected type is pushed inward, so a + literal `if 5 then …` flags the literal directly rather than the surrounding `if`. +- *Branches.* `thenBr` is synthesized; if present, `elseBr` is synthesized too. The two + branch types are *not* compared against each other. The reason is that in Laurel's + unified statement-expression model, statement-position `if`s commonly mix a value + branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch (early + {name Strata.Laurel.StmtExpr.Return}`return`, {name Strata.Laurel.StmtExpr.Exit}`exit`, an + {name Strata.Laurel.StmtExpr.Assert}`assert`, …), which a strict equality check on + branches would reject incorrectly. +- *Result type.* When `elseBr` is `none`, the result is + {name Strata.Laurel.HighType.TVoid}`TVoid` — the construct is in statement form and the + then-branch's value is discarded. When `elseBr` is `some _`, the result is the + then-branch's synthesized type. The arbitrary preference for the then-branch here is + harmless: the result is always consumed by an enclosing `checkAssignable` / + subsumption-fallback, which gives a one-sided check against the surrounding context's + expected type. + +The change to `none` → {name Strata.Laurel.HighType.TVoid}`TVoid` closes a soundness gap in +the previous design, where `if c then 5` synthesized {name Strata.Laurel.HighType.TInt}`TInt` +unconditionally — even though there is no value when `c` is false — so an assignment +`x: int := if c then 5` would have type-checked. With the new rule, the synthesized type is +{name Strata.Laurel.HighType.TVoid}`TVoid` and the assignment is correctly rejected. + +The planned bespoke check rule is straightforward: `cond ⇐ TBool`, `thenBr ⇐ expected`, and +`elseBr ⇐ expected` if present; if absent, fall back to subsumption of +{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. The benefit is the +same as for `Block`: errors fire at the offending sub-expression rather than the +surrounding `if`, and the expected type propagates through nested control flow. + +## Mutual recursion and termination + +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` are now mutually recursive: the synth rule +for {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for +the condition, and the check function falls back to synth via the subsumption rule. + +Termination uses a lexicographic measure `(exprMd, tag)` where the tag is `0` for +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and `1` for +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. Any descent into a strict subterm +decreases via `Prod.Lex.left` (first component shrinks); the subsumption rule +`check e → synth e` calls synth on the *same* expression, which decreases via +`Prod.Lex.right` (second component goes from 1 to 0). This is the standard well-founded +encoding for bidirectional systems where one direction calls the other on the same input. + ## Mode assignment per construct The intended mode for each construct (some are still being converted to bidirectional in @@ -281,7 +336,7 @@ the implementation): |---|---|---| | Literals, `Var .Local`, `Var .Field`, `New T`, `IsType`, `ReferenceEquals`, `Quantifier`, `Assigned`, `Fresh`, `Hole _ (some T)`, `StaticCall`, `InstanceCall` | synth | type is determined locally | | `Var .Declare`, `Exit`, `Return`, `While`, `Assert`, `Assume`, `Assign` | synth ⇒ {name Strata.Laurel.HighType.TVoid}`TVoid` | side-effecting; condition operands checked inward | -| `IfThenElse cond t e_opt` | bespoke check | `cond ⇐ TBool`; `t ⇐ T`; `e ⇐ T` if present | +| `IfThenElse cond t e_opt` | synth (`cond ⇐ TBool`); planned bespoke check | see below | | `Block` | bespoke check | `s_1..s_{n-1}` synth, `s_n ⇐ T`; synth uses last's synthesized type | | `Hole _ none` | bespoke check | check mode succeeds with `expected`; synth mode → `Unknown` | | `AsType e T` | synth ⇒ `T` | the cast is the user's claim; no check on `e` | From 37e6c359e6d62c7499702aa11b465d27dafd49d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:33:24 -0400 Subject: [PATCH 014/128] typechecking description refactor general design rules (one section per rule) --- docs/verso/LaurelDoc.lean | 392 +++++++++++++++++++++++++++----------- 1 file changed, 277 insertions(+), 115 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index c3d9a314f2..64dd119a59 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -234,128 +234,290 @@ The bidirectional design replaces that with two cleanly-separated concerns: current stub is conservative (structural equality only); it can be tightened incrementally without changing any callers. -## Block and `TVoid` - -Statement-position constructs that produce no value synthesize -{name Strata.Laurel.HighType.TVoid}`TVoid`: -{name Strata.Laurel.StmtExpr.Return}`Return`, -{name Strata.Laurel.StmtExpr.Exit}`Exit`, -{name Strata.Laurel.StmtExpr.While}`While`, -{name Strata.Laurel.StmtExpr.Assert}`Assert`, -{name Strata.Laurel.StmtExpr.Assume}`Assume`, -{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies. -This makes blocks compose cleanly: control-flow statements don't pollute a block's -synthesized type. - -A {name Strata.Laurel.StmtExpr.Block}`Block` is statement chaining `{ s_1; …; s_n }`. The -checker treats it permissively in two ways: - -1. *Non-last statements are not required to be {name Strata.Laurel.HighType.TVoid}`TVoid`.* - In synth mode their types are computed and discarded; in check mode they are still - synthesized rather than checked against `void`. This matches Java/Python/JavaScript - expression-statement semantics: `f(x);` where `f` returns a value is normal idiomatic - code, and forcing an explicit discard would be hostile to the imperative style Laurel - targets. The cost is that `5;` (a literal in statement position) is silently accepted; if - we ever want to flag that, it should land as a lint, not a type error. - -2. *The last statement is the block's type.* Empty blocks have type - {name Strata.Laurel.HighType.TVoid}`TVoid`. This is what lets a transparent functional - procedure body be `{ … some statements …; expr }`. - -In check mode, the bespoke `Block` rule pushes the expected type into the *last* statement -rather than checking the block's synthesized type at the boundary. This buys two things: -errors fire at the actual offending sub-expression (e.g. inside a deeply nested -{name Strata.Laurel.StmtExpr.IfThenElse}`if`), and the expected type keeps propagating -through nested {name Strata.Laurel.StmtExpr.Block}`Block` / +## Notation + +Typing rules are written in the standard derivation-tree form: premises above the line, +conclusion below, rule name on the right. + +``` +premise_1 premise_2 … premise_n +───────────────────────────────────── (Rule-Name) + conclusion +``` + +We use: + +- `e ⇒ T` — _e_ synthesizes _T_ (synth mode, `synthStmtExpr`). +- `e ⇐ T` — _e_ checks against _T_ (check mode, `checkStmtExpr`). +- `T <: U` — gradual consistency-subtyping, i.e. `isConsistentSubtype T U`. +- `Γ` for the lexical scope is left implicit — every rule threads it identically. + +Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This +includes {name Strata.Laurel.StmtExpr.Return}`Return`, +{name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, +{name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, +{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies +— they're recorded in the rules below. + +## Subsumption (the synth↔check boundary) + +``` +e ⇒ A A <: B +───────────────── (Sub) + e ⇐ B +``` + +Subsumption is the *only* place check switches to synth. It fires as the default fallback +in `checkStmtExpr` for every construct without a bespoke check rule. Bespoke check rules +push the expected type *into* subexpressions, which keeps errors localized. + +## Typing rules + +Below, each construct is given as a derivation. Rules marked with ✓ in the implementation +column are implemented today; rules marked ✗ are planned. The current implementation has +bespoke check rules for {name Strata.Laurel.StmtExpr.Block}`Block` only; everything else +reaches check mode through Sub. Where a synth rule pushes an expected type into a +subexpression (e.g. `cond ⇐ TBool` in {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), +that's listed as a premise. + +### Literals and references + +``` + (Lit-Int) ✓ +───────────── ──────────────── ───────────────── + LiteralInt n ⇒ TInt LiteralBool b ⇒ TBool LiteralString s ⇒ TString + +──────────────────────── Γ(x) = T + LiteralDecimal d ⇒ TReal ───────────────── (Var-Local) ✓ + Var (.Local x) ⇒ T + + e ⇒ _ Γ(f) = T_f Γ(x) ↦ T fresh +───────────────────────── (Var-Field) ✓ ───────────────────────── (Var-Declare) ✓ + Var (.Field e f) ⇒ T_f Var (.Declare ⟨x, T⟩) ⇒ TVoid +``` + +`Var (.Field e f)` resolves `f` against the type of `e` (or the enclosing instance type for +`self.f`); the typing rule is independent of which path resolution took. + +### IfThenElse + +``` +cond ⇐ TBool thenBr ⇒ T +───────────────────────────── (If-NoElse) ✓ + IfThenElse cond thenBr none ⇒ TVoid + +cond ⇐ TBool thenBr ⇒ T_t elseBr ⇒ T_e +───────────────────────────────────────────────── (If-Synth) ✓ + IfThenElse cond thenBr (some elseBr) ⇒ T_t + +cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T +───────────────────────────────────────────── (If-Check) ✗ (planned) + IfThenElse cond thenBr (some elseBr) ⇐ T +``` + +If-Synth picks the then-branch type by convention; the result is always consumed by an +enclosing `checkAssignable` or by Sub, which provides a one-sided check against the +surrounding context. The two branches are deliberately not compared against each other: +statement-position `if`s commonly mix a value branch with a +{name Strata.Laurel.HighType.TVoid}`TVoid` branch (early `return`, `exit`, `assert`, …), +which a strict equality check would reject incorrectly. + +If-NoElse synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value +to give back when `cond` is false. This rejects `x : int := if c then 5` at the assignment. + +### Block + +``` + none of these statements has a typing premise + (their synthesized types are discarded — lax) + ─────────────────────────────────────────── + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T + ──────────────────────────────────────────────────────── (Block-Synth) ✓ + Block [s_1; …; s_n] label ⇒ T + +──────────────────── (Block-Synth-Empty) ✓ + Block [] label ⇒ TVoid + + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T +───────────────────────────────────────────── (Block-Check) ✓ + Block [s_1; …; s_n] label ⇐ T + + TVoid <: T +───────────────────── (Block-Check-Empty) ✓ + Block [] label ⇐ T +``` + +Block-Synth is lax: non-last statements are synthesized but their types are discarded. +This matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` +returns a value is normal idiomatic code. The cost is that `5;` (a literal in statement +position) is silently accepted; flagging it would belong to a lint, not the type checker. + +Block-Check pushes the expected type into the *last* statement rather than checking the +block's synthesized type at the boundary. Errors then fire at the offending subexpression +inside `s_n` rather than at the surrounding {name Strata.Laurel.StmtExpr.Block}`Block`, and +the expected type keeps propagating through nested +{name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to subsumption of -{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. - -## IfThenElse - -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse cond t e_opt` has been converted to -*partial* bidirectional form. Today the implementation has a synth rule but reaches check -mode only through the subsumption fallback; a bespoke check rule that pushes the expected -type into both branches is the planned next step. - -The synth rule: - -- *Condition.* `cond` is checked against {name Strata.Laurel.HighType.TBool}`TBool` via a - recursive `checkStmtExpr cond TBool` call. This replaces the previous synth-then-`checkBool` - pattern with the clean bidirectional one — the expected type is pushed inward, so a - literal `if 5 then …` flags the literal directly rather than the surrounding `if`. -- *Branches.* `thenBr` is synthesized; if present, `elseBr` is synthesized too. The two - branch types are *not* compared against each other. The reason is that in Laurel's - unified statement-expression model, statement-position `if`s commonly mix a value - branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch (early - {name Strata.Laurel.StmtExpr.Return}`return`, {name Strata.Laurel.StmtExpr.Exit}`exit`, an - {name Strata.Laurel.StmtExpr.Assert}`assert`, …), which a strict equality check on - branches would reject incorrectly. -- *Result type.* When `elseBr` is `none`, the result is - {name Strata.Laurel.HighType.TVoid}`TVoid` — the construct is in statement form and the - then-branch's value is discarded. When `elseBr` is `some _`, the result is the - then-branch's synthesized type. The arbitrary preference for the then-branch here is - harmless: the result is always consumed by an enclosing `checkAssignable` / - subsumption-fallback, which gives a one-sided check against the surrounding context's - expected type. - -The change to `none` → {name Strata.Laurel.HighType.TVoid}`TVoid` closes a soundness gap in -the previous design, where `if c then 5` synthesized {name Strata.Laurel.HighType.TInt}`TInt` -unconditionally — even though there is no value when `c` is false — so an assignment -`x: int := if c then 5` would have type-checked. With the new rule, the synthesized type is -{name Strata.Laurel.HighType.TVoid}`TVoid` and the assignment is correctly rejected. - -The planned bespoke check rule is straightforward: `cond ⇐ TBool`, `thenBr ⇐ expected`, and -`elseBr ⇐ expected` if present; if absent, fall back to subsumption of -{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. The benefit is the -same as for `Block`: errors fire at the offending sub-expression rather than the -surrounding `if`, and the expected type propagates through nested control flow. +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to a subsumption +check of {name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. + +### Statements that synthesize TVoid + +``` +───────────────── (Exit) ✓ cond ⇐ TBool invs ⇐ TBool dec ⇐ ? body ⇒ _ + Exit target ⇒ TVoid ──────────────────────────────────────────────────────────────── (While) ✓-ish + While cond invs dec body ⇒ TVoid + + +───────────────────────── (Return-None) ✓ e ⇒ _ + Return none ⇒ TVoid ───────────────────── (Return-Some) ✓ + Return (some e) ⇒ TVoid + + +cond ⇐ TBool cond ⇐ TBool +────────────────── (Assert) ✓-ish ────────────── (Assume) ✓-ish + Assert cond ⇒ TVoid Assume cond ⇒ TVoid + + + Γ(x) = T_x e ⇒ T_e T_e <: T_x targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i +───────────────────────────────────────── (Assign-Single) ✓-ish ─────────────────────────────────────────────────────────────────── (Assign-Multi) ✓-ish + Assign [x] e ⇒ TVoid Assign targets e ⇒ TVoid +``` + +✓-ish marks rules that are implemented but still call the legacy `checkBool` / +`checkAssignable` helpers rather than `checkStmtExpr cond TBool`. Functionally equivalent +under the gradual relation `<:` (since `checkBool` accepts the same types as +`isConsistentSubtype _ TBool` modulo the temporary {name Strata.Laurel.HighType.TCore}`TCore` +carve-out); slated to be migrated to `checkStmtExpr`. + +The {name Strata.Laurel.StmtExpr.Return}`Return`-with-value rule today only resolves `e` +without checking it against the enclosing procedure's declared output type. The intended +rule is: + +``` + Γ_proc.outputs = [T] e ⇐ T +───────────────────────────────── (Return-Some-Checked) ✗ (planned) + Return (some e) ⇒ TVoid +``` + +This requires threading the expected return type through `ResolveState`. Without it, +`return 0` in a `bool`-returning procedure goes uncaught. + +### Calls and primitive operations + +``` + callee resolves to procedure with inputs Ts and outputs [T] + args ⇒ Us U_i <: T_i (pairwise) +────────────────────────────────────────────────────────────── (Static-Call) ✓-ish + StaticCall callee args ⇒ T + + callee resolves to procedure with inputs Ts and outputs [T_1; …; T_n] (n ≠ 1) + args ⇒ Us U_i <: T_i (pairwise) +───────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi) ✓-ish + StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] + + target ⇒ _ callee resolves with inputs [self; Ts] and outputs [T] + args ⇒ Us U_i <: T_i (pairwise; self is dropped) +───────────────────────────────────────────────────────────────────────── (Instance-Call) ✓-ish + InstanceCall target callee args ⇒ T + + + args ⇐ TBool (each) +────────────────────────────── (Op-Bool) ✓-ish op ∈ {And, Or, AndThen, OrElse, Not, Implies} + PrimitiveOp op args ⇒ TBool + + + args ⇐ Numeric (each) +───────────────────────────── (Op-Cmp) ✓-ish op ∈ {Lt, Leq, Gt, Geq} + PrimitiveOp op args ⇒ TBool + + + lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l +────────────────────────────────────────────────────────── (Op-Eq) ✓-ish op ∈ {Eq, Neq} + PrimitiveOp op [lhs; rhs] ⇒ TBool + + + args ⇐ Numeric (each) args.head ⇒ T +────────────────────────────────────────── (Op-Arith) ✓-ish op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} + PrimitiveOp op args ⇒ T + + + args ⇐ TString (each) — current implementation: no operand check +───────────────────────────── (Op-Concat) ✓-ish + PrimitiveOp op args ⇒ TString +``` + +`Numeric` abbreviates "consistent with one of +{name Strata.Laurel.HighType.TInt}`TInt`, {name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` +rather than a `checkStmtExpr` chain; equivalent under the gradual relation. + +Op-Arith's "result is the type of the first argument" rule handles `int + int → int`, +`real + real → real`, etc. without a unification step. A consequence: `int + real` is *not* +flagged because each operand individually passes the numeric check. A real fix would be a +numeric-promotion or unification rule; for now this is a known relaxation. + +Op-Concat currently performs no operand check; the rule above describes the intended +behavior. + +### Object-related and verification forms + +``` + ref resolves to a composite or datatype T +───────────────────────────────────────────── (New-Ok) ✓ otherwise New ref ⇒ Unknown + New ref ⇒ UserDefined T + + +───────────────── (This) ✓ ──────────────────────────── (Abstract / All / ContractOf) ✓ + This ⇒ Unknown Abstract / All / ContractOf … ⇒ Unknown + + + lhs ⇒ _ rhs ⇒ _ +───────────────────────── (RefEq) ✓ target ⇒ _ + ReferenceEquals lhs rhs ⇒ TBool ────────────────── (AsType) ✓ + AsType target T ⇒ T + + + target ⇒ _ body ⇒ _ +───────────────── (IsType) ✓ ────────────────────────── (Quantifier) ✓ + IsType target T ⇒ TBool Quantifier mode ⟨x, T⟩ trig body ⇒ TBool + + + name ⇒ _ v ⇒ T v ⇒ _ +───────────────── (Assigned) ✓ ──────────── (Old) ✓ ────────────── (Fresh) ✓ + Assigned name ⇒ TBool Old v ⇒ T Fresh v ⇒ TBool + + + v ⇒ T proof ⇒ _ target ⇒ T_t newVal ⇒ _ +────────────────────── (ProveBy) ✓ ───────────────────────────────── (PureFieldUpdate) ✓ + ProveBy v proof ⇒ T PureFieldUpdate target f newVal ⇒ T_t +``` + +### Holes + +``` + Unknown <: T +───────────────────── (Hole-Some) ✓ ───────────────────── (Hole-None-Synth) ✓ ───────────────────── (Hole-None-Check) ✗ (planned) + Hole d (some T) ⇒ T Hole d none ⇒ Unknown Hole d none ⇐ T +``` + +In check mode, `Hole d none ⇐ T` reduces to subsumption today (`Unknown <: T`, which always +holds). The planned bespoke rule would record the inferred `T` on the hole node so +downstream passes can see it, instead of leaving `none` until the hole-inference pass. ## Mutual recursion and termination -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` are now mutually recursive: the synth rule -for {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for -the condition, and the check function falls back to synth via the subsumption rule. +`synthStmtExpr` and `checkStmtExpr` are mutually recursive: the synth rule for +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for the +condition, and the check function falls back to synth via Sub. Termination uses a lexicographic measure `(exprMd, tag)` where the tag is `0` for -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and `1` for -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. Any descent into a strict subterm -decreases via `Prod.Lex.left` (first component shrinks); the subsumption rule -`check e → synth e` calls synth on the *same* expression, which decreases via -`Prod.Lex.right` (second component goes from 1 to 0). This is the standard well-founded -encoding for bidirectional systems where one direction calls the other on the same input. - -## Mode assignment per construct - -The intended mode for each construct (some are still being converted to bidirectional in -the implementation): - -| Construct | Mode | Notes | -|---|---|---| -| Literals, `Var .Local`, `Var .Field`, `New T`, `IsType`, `ReferenceEquals`, `Quantifier`, `Assigned`, `Fresh`, `Hole _ (some T)`, `StaticCall`, `InstanceCall` | synth | type is determined locally | -| `Var .Declare`, `Exit`, `Return`, `While`, `Assert`, `Assume`, `Assign` | synth ⇒ {name Strata.Laurel.HighType.TVoid}`TVoid` | side-effecting; condition operands checked inward | -| `IfThenElse cond t e_opt` | synth (`cond ⇐ TBool`); planned bespoke check | see below | -| `Block` | bespoke check | `s_1..s_{n-1}` synth, `s_n ⇐ T`; synth uses last's synthesized type | -| `Hole _ none` | bespoke check | check mode succeeds with `expected`; synth mode → `Unknown` | -| `AsType e T` | synth ⇒ `T` | the cast is the user's claim; no check on `e` | -| `Old`, `ProveBy v _`, `PureFieldUpdate t _ _` | propagate type of subexpr | unchanged | -| `This`, `Abstract`, `All`, `ContractOf` | synth ⇒ {name Strata.Laurel.HighType.Unknown}`Unknown` | type not tracked | - -{name Strata.Laurel.StmtExpr.PrimitiveOp}`PrimitiveOp` operands are checked inward against -the operator's expected operand type ({name Strata.Laurel.HighType.TBool}`TBool` for -logical, numeric for arithmetic and ordering, {name Strata.Laurel.HighType.TString}`TString` -for `StrConcat`). {name Strata.Laurel.Operation.Eq}`Eq`/{name Strata.Laurel.Operation.Neq}`Neq` -synthesize both operands and require consistency in either direction -(`isConsistentSubtype l r ∨ isConsistentSubtype r l`). - -Arithmetic ops `Neg`/`Add`/…/`ModT` synthesize *the type of the first argument*. This is how -the checker handles {name Strata.Laurel.HighType.TInt}`TInt` / -{name Strata.Laurel.HighType.TReal}`TReal` / {name Strata.Laurel.HighType.TFloat64}`TFloat64` -without a unification step. A consequence: `int + real` is not flagged today, since each -operand passes the numeric check individually. A real fix would be a numeric-promotion or -unification rule; for now this is a known relaxation. +`synthStmtExpr` and `1` for `checkStmtExpr`. Any descent into a strict subterm decreases +via `Prod.Lex.left` (first component shrinks); Sub calls synth on the *same* expression, +which decreases via `Prod.Lex.right` (second component goes from 1 to 0). This is the +standard well-founded encoding for bidirectional systems where one direction calls the +other on the same input. ## Two helpers for resolution sites From 60822bbd1bbd6c6823971e1fe39ec0e24c361308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:44:13 -0400 Subject: [PATCH 015/128] reformat typechecking section --- docs/verso/LaurelDoc.lean | 632 +++++++++++++++++++++----------------- 1 file changed, 351 insertions(+), 281 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 64dd119a59..1577232261 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -150,432 +150,502 @@ A Laurel program consists of procedures, global variables, type definitions, and Type checking is woven into the resolution pass: every {name Strata.Laurel.StmtExpr}`StmtExpr` gets a {name Strata.Laurel.HighType}`HighType`, and -mismatches against the surrounding context become diagnostics. The design is -*bidirectional*: each construct is resolved either in *synthesis* mode — return a type -inferred from the expression — or in *checking* mode — verify that the expression has a -given expected type. The two are different functions on -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. - -This page describes the design choices behind the checker. The implementation is in +mismatches against the surrounding context become diagnostics. The implementation is in `Resolution.lean`. -## The two judgments +## Design + +### Bidirectional type checking There are two operations on expressions, written here in standard bidirectional notation: ``` -Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) -Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) +e ⇒ T -- "e synthesizes T" (synthStmtExpr) +e ⇐ T -- "e checks against T" (checkStmtExpr) ``` -Each construct picks a mode based on whether its type is determined locally (synth) or by -context (check). Mode assignment is part of the design — see _Mode assignment per construct_ -below. - -The two judgments are connected by a single change-of-direction rule, *subsumption*: +Synthesis returns a type inferred from the expression itself; checking verifies that the +expression has a given expected type. Each construct picks a mode based on whether its type +is determined locally (synth) or by context (check). The two judgments are connected by a +single change-of-direction rule, *subsumption*: ``` -Γ ⊢ e ⇒ A A <: B -───────────────────── (sub) - Γ ⊢ e ⇐ B +e ⇒ A A <: B +───────────────── (Sub) + e ⇐ B ``` -Subsumption is the *only* place the checker switches from check to synth mode. It fires as a -default fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct -without a bespoke check rule: synthesize the expression's type, then verify the result is a -subtype of the expected type. Bespoke check rules push the expected type *into* -subexpressions instead of bouncing through synthesis, which keeps error messages localized -and lets the expected type propagate through nested control flow. - -## Subtyping and gradual consistency +Subsumption is the *only* place the checker switches from check to synth mode. It fires as +the default fallback in +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct without a bespoke +check rule: synthesize the expression's type, then verify the result is a subtype of the +expected type. Bespoke check rules push the expected type *into* subexpressions instead of +bouncing through synthesis, which keeps error messages localized and lets the expected type +propagate through nested control flow. + +`synthStmtExpr` and `checkStmtExpr` are mutually recursive: synth rules invoke check on +subexpressions whose expected type is known (e.g. `cond ⇐ TBool` in +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), and `checkStmtExpr` falls back to +`synthStmtExpr` via Sub. Termination uses a lexicographic measure `(exprMd, tag)` where the +tag is `0` for synth and `1` for check; any descent into a strict subterm decreases via +`Prod.Lex.left`, while Sub calls synth on the *same* expression and decreases via +`Prod.Lex.right`. This is the standard well-founded encoding for bidirectional systems. + +There is also a thin `resolveStmtExpr` wrapper that calls `synthStmtExpr` and discards the +synthesized type. It's used at sites where typing is not enforced (verification annotations, +modifies/reads clauses). The right principle for new call sites is: when the position has a +known expected type ({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for +`decreases`, the declared output for a constant initializer or a functional body), use +`checkStmtExpr`. When it doesn't, use `resolveStmtExpr`. `synthStmtExpr` itself is mostly an +internal interface used by other rules. + +### Gradual typing The relation `<:` is implemented by two Lean functions — both currently stubs, both intended to be sharpened: -- `isSubtype` — pure subtyping. The stub is structural - equality via {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the - `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds +- `isSubtype` — pure subtyping. The stub is structural equality via + {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the `extending` + chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. -- `isConsistentSubtype` — gradual consistency, in - the Siek–Taha sense. {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type - `?` and is consistent with everything in either direction; otherwise the relation - delegates to `isSubtype`. {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly - consistent for now, as a clearly-labelled migration escape hatch from the Core language — - this carve-out is intentionally temporary. - -Subsumption (and every bespoke check rule) uses -`isConsistentSubtype`, never raw `isSubtype`. That -single choice is what makes the system *gradual*: an expression of type +- `isConsistentSubtype` — gradual consistency, in the Siek–Taha sense. + {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type `?` and is consistent + with everything in either direction; otherwise the relation delegates to `isSubtype`. + {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now, as a + clearly-labelled migration escape hatch from the Core language — this carve-out is + intentionally temporary. + +Subsumption (and every bespoke check rule) uses `isConsistentSubtype`, never raw +`isSubtype`. That single choice is what makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely into any typed slot, and any expression flows freely into a slot of type {name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between fully-known types only. -## What changed from the synth-only design - A previous iteration was synth-only with three *bivariantly-compatible* wildcards: {name Strata.Laurel.HighType.Unknown}`Unknown`, {name Strata.Laurel.HighType.UserDefined}`UserDefined`, and {name Strata.Laurel.HighType.TCore}`TCore`. The -{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was particularly -load-bearing: it meant that *no* assignment, call argument, or comparison involving a user -type was ever rejected, because subtyping wasn't tracked at all and constrained types -weren't unwrapped — we couldn't tell what was safe. +{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no +assignment, call argument, or comparison involving a user type was ever rejected. The +bidirectional design retires that carve-out — user-defined types are now a regular +participant in `<:`, and tightening `isSubtype` (to walk inheritance and unwrap +constrained types) gradually buys real checking on user-defined code without changing +callers. -The bidirectional design replaces that with two cleanly-separated concerns: +Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This +includes {name Strata.Laurel.StmtExpr.Return}`Return`, +{name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, +{name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, +{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies +— recorded in the rules below. -- {name Strata.Laurel.HighType.Unknown}`Unknown` keeps wildcard semantics, but now as a - *real* semantic claim (gradual typing) rather than a workaround. -- {name Strata.Laurel.HighType.UserDefined}`UserDefined` becomes a regular type. Once - `isSubtype` is implemented properly, `Cat ≤ Animal` will - pass, `Cat ≤ Dog` will fail, and constrained types will be unwrappable to their base. The - current stub is conservative (structural equality only); it can be tightened - incrementally without changing any callers. +## Typing rules -## Notation +Each construct is given as a derivation. Premises sit above the line, conclusion below. +Rules tagged `(impl)` are implemented; rules tagged `(planned)` describe the intended +behavior but aren't yet wired in. `Γ` (the lexical scope) is left implicit; every rule +threads it identically. -Typing rules are written in the standard derivation-tree form: premises above the line, -conclusion below, rule name on the right. +### Sub (subsumption) ``` -premise_1 premise_2 … premise_n -───────────────────────────────────── (Rule-Name) - conclusion +e ⇒ A A <: B +───────────────── (Sub, impl) + e ⇐ B ``` -We use: +The default fallback in `checkStmtExpr`. Used by every construct that doesn't have a +bespoke check rule. -- `e ⇒ T` — _e_ synthesizes _T_ (synth mode, `synthStmtExpr`). -- `e ⇐ T` — _e_ checks against _T_ (check mode, `checkStmtExpr`). -- `T <: U` — gradual consistency-subtyping, i.e. `isConsistentSubtype T U`. -- `Γ` for the lexical scope is left implicit — every rule threads it identically. +### LiteralInt -Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This -includes {name Strata.Laurel.StmtExpr.Return}`Return`, -{name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, -{name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, -{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies -— they're recorded in the rules below. +``` +───────────────────── (Lit-Int, impl) + LiteralInt n ⇒ TInt +``` -## Subsumption (the synth↔check boundary) +### LiteralBool ``` -e ⇒ A A <: B -───────────────── (Sub) - e ⇐ B +────────────────────── (Lit-Bool, impl) + LiteralBool b ⇒ TBool ``` -Subsumption is the *only* place check switches to synth. It fires as the default fallback -in `checkStmtExpr` for every construct without a bespoke check rule. Bespoke check rules -push the expected type *into* subexpressions, which keeps errors localized. +### LiteralString -## Typing rules +``` +──────────────────────────── (Lit-String, impl) + LiteralString s ⇒ TString +``` -Below, each construct is given as a derivation. Rules marked with ✓ in the implementation -column are implemented today; rules marked ✗ are planned. The current implementation has -bespoke check rules for {name Strata.Laurel.StmtExpr.Block}`Block` only; everything else -reaches check mode through Sub. Where a synth rule pushes an expected type into a -subexpression (e.g. `cond ⇐ TBool` in {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), -that's listed as a premise. +### LiteralDecimal -### Literals and references +``` +───────────────────────────── (Lit-Decimal, impl) + LiteralDecimal d ⇒ TReal +``` +### Var (.Local) + +``` + Γ(x) = T +────────────────────── (Var-Local, impl) + Var (.Local x) ⇒ T ``` - (Lit-Int) ✓ -───────────── ──────────────── ───────────────── - LiteralInt n ⇒ TInt LiteralBool b ⇒ TBool LiteralString s ⇒ TString -──────────────────────── Γ(x) = T - LiteralDecimal d ⇒ TReal ───────────────── (Var-Local) ✓ - Var (.Local x) ⇒ T +### Var (.Field) - e ⇒ _ Γ(f) = T_f Γ(x) ↦ T fresh -───────────────────────── (Var-Field) ✓ ───────────────────────── (Var-Declare) ✓ - Var (.Field e f) ⇒ T_f Var (.Declare ⟨x, T⟩) ⇒ TVoid ``` + e ⇒ _ Γ(f) = T_f +───────────────────────── (Var-Field, impl) + Var (.Field e f) ⇒ T_f +``` + +`f` is resolved against the type of `e` (or the enclosing instance type for `self.f`); the +typing rule is independent of which path resolution took. -`Var (.Field e f)` resolves `f` against the type of `e` (or the enclosing instance type for -`self.f`); the typing rule is independent of which path resolution took. +### Var (.Declare) + +``` + Γ(x) ↦ T fresh +────────────────────────────────── (Var-Declare, impl) + Var (.Declare ⟨x, T⟩) ⇒ TVoid +``` ### IfThenElse ``` cond ⇐ TBool thenBr ⇒ T -───────────────────────────── (If-NoElse) ✓ - IfThenElse cond thenBr none ⇒ TVoid +───────────────────────────────────────── (If-NoElse, impl) + IfThenElse cond thenBr none ⇒ TVoid + cond ⇐ TBool thenBr ⇒ T_t elseBr ⇒ T_e -───────────────────────────────────────────────── (If-Synth) ✓ - IfThenElse cond thenBr (some elseBr) ⇒ T_t +───────────────────────────────────────────────── (If-Synth, impl) + IfThenElse cond thenBr (some elseBr) ⇒ T_t + cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T -───────────────────────────────────────────── (If-Check) ✗ (planned) - IfThenElse cond thenBr (some elseBr) ⇐ T +───────────────────────────────────────────── (If-Check, planned) + IfThenElse cond thenBr (some elseBr) ⇐ T ``` -If-Synth picks the then-branch type by convention; the result is always consumed by an -enclosing `checkAssignable` or by Sub, which provides a one-sided check against the -surrounding context. The two branches are deliberately not compared against each other: -statement-position `if`s commonly mix a value branch with a -{name Strata.Laurel.HighType.TVoid}`TVoid` branch (early `return`, `exit`, `assert`, …), -which a strict equality check would reject incorrectly. - If-NoElse synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value -to give back when `cond` is false. This rejects `x : int := if c then 5` at the assignment. +to give back when `cond` is false. Without this rule, `x : int := if c then 5` would +type-check spuriously. + +If-Synth picks the then-branch type; the result is always consumed by an enclosing +`checkAssignable` or by Sub, which provides a one-sided check against the surrounding +context. The two branches are deliberately not compared against each other: statement-position +`if`s commonly mix a value branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch +(early `return`, `exit`, `assert`, …), which a strict equality check would reject incorrectly. ### Block ``` - none of these statements has a typing premise - (their synthesized types are discarded — lax) - ─────────────────────────────────────────── - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T - ──────────────────────────────────────────────────────── (Block-Synth) ✓ - Block [s_1; …; s_n] label ⇒ T + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T +─────────────────────────────────────────────────── (Block-Synth, impl) + Block [s_1; …; s_n] label ⇒ T + -──────────────────── (Block-Synth-Empty) ✓ +──────────────────────── (Block-Synth-Empty, impl) Block [] label ⇒ TVoid - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T -───────────────────────────────────────────── (Block-Check) ✓ - Block [s_1; …; s_n] label ⇐ T + + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T +─────────────────────────────────────────────────── (Block-Check, impl) + Block [s_1; …; s_n] label ⇐ T + TVoid <: T -───────────────────── (Block-Check-Empty) ✓ +────────────────────── (Block-Check-Empty, impl) Block [] label ⇐ T ``` -Block-Synth is lax: non-last statements are synthesized but their types are discarded. -This matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` +The non-last statements are synthesized but their types are discarded — this is the lax +rule. It matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` returns a value is normal idiomatic code. The cost is that `5;` (a literal in statement position) is silently accepted; flagging it would belong to a lint, not the type checker. -Block-Check pushes the expected type into the *last* statement rather than checking the -block's synthesized type at the boundary. Errors then fire at the offending subexpression -inside `s_n` rather than at the surrounding {name Strata.Laurel.StmtExpr.Block}`Block`, and -the expected type keeps propagating through nested +In check mode, the expected type is pushed into the *last* statement rather than checked at +the boundary. Errors then fire at the offending subexpression inside `s_n`, and the +expected type keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to a subsumption -check of {name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. -### Statements that synthesize TVoid +### Exit ``` -───────────────── (Exit) ✓ cond ⇐ TBool invs ⇐ TBool dec ⇐ ? body ⇒ _ - Exit target ⇒ TVoid ──────────────────────────────────────────────────────────────── (While) ✓-ish - While cond invs dec body ⇒ TVoid +───────────────────── (Exit, impl) + Exit target ⇒ TVoid +``` +### Return -───────────────────────── (Return-None) ✓ e ⇒ _ - Return none ⇒ TVoid ───────────────────── (Return-Some) ✓ - Return (some e) ⇒ TVoid +``` +───────────────────────── (Return-None, impl) + Return none ⇒ TVoid -cond ⇐ TBool cond ⇐ TBool -────────────────── (Assert) ✓-ish ────────────── (Assume) ✓-ish - Assert cond ⇒ TVoid Assume cond ⇒ TVoid + e ⇒ _ +────────────────────────── (Return-Some, impl) + Return (some e) ⇒ TVoid - Γ(x) = T_x e ⇒ T_e T_e <: T_x targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────── (Assign-Single) ✓-ish ─────────────────────────────────────────────────────────────────── (Assign-Multi) ✓-ish - Assign [x] e ⇒ TVoid Assign targets e ⇒ TVoid + Γ_proc.outputs = [T] e ⇐ T +───────────────────────────────── (Return-Some-Checked, planned) + Return (some e) ⇒ TVoid ``` -✓-ish marks rules that are implemented but still call the legacy `checkBool` / -`checkAssignable` helpers rather than `checkStmtExpr cond TBool`. Functionally equivalent -under the gradual relation `<:` (since `checkBool` accepts the same types as -`isConsistentSubtype _ TBool` modulo the temporary {name Strata.Laurel.HighType.TCore}`TCore` -carve-out); slated to be migrated to `checkStmtExpr`. +The current `Return-Some` rule discards the value's synthesized type. The planned rule +threads the expected return type through {name Strata.Laurel.ResolveState}`ResolveState` +(set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`), so `return 0` in +a `bool`-returning procedure can be caught at the `Return` site. -The {name Strata.Laurel.StmtExpr.Return}`Return`-with-value rule today only resolves `e` -without checking it against the enclosing procedure's declared output type. The intended -rule is: +### While ``` - Γ_proc.outputs = [T] e ⇐ T -───────────────────────────────── (Return-Some-Checked) ✗ (planned) - Return (some e) ⇒ TVoid + cond ⇐ TBool invs_i ⇐ TBool dec ⇐ ? body ⇒ _ +───────────────────────────────────────────────────────────── (While, impl-ish) + While cond invs dec body ⇒ TVoid ``` -This requires threading the expected return type through `ResolveState`. Without it, -`return 0` in a `bool`-returning procedure goes uncaught. +`impl-ish` here means the rule is implemented but `cond` and `invs_i` go through the legacy +`checkBool` helper rather than `checkStmtExpr cond TBool`. Functionally equivalent under +`<:`; slated for migration. -### Calls and primitive operations +### Assert ``` - callee resolves to procedure with inputs Ts and outputs [T] + cond ⇐ TBool +────────────────────────── (Assert, impl-ish) + Assert cond ⇒ TVoid +``` + +### Assume + +``` + cond ⇐ TBool +───────────────────── (Assume, impl-ish) + Assume cond ⇒ TVoid +``` + +### Assign + +``` + Γ(x) = T_x e ⇒ T_e T_e <: T_x +───────────────────────────────────────── (Assign-Single, impl-ish) + Assign [x] e ⇒ TVoid + + + targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i +───────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) + Assign targets e ⇒ TVoid +``` + +### StaticCall + +``` + callee = static-procedure with inputs Ts and outputs [T] args ⇒ Us U_i <: T_i (pairwise) -────────────────────────────────────────────────────────────── (Static-Call) ✓-ish - StaticCall callee args ⇒ T +──────────────────────────────────────────────────────────── (Static-Call, impl-ish) + StaticCall callee args ⇒ T + - callee resolves to procedure with inputs Ts and outputs [T_1; …; T_n] (n ≠ 1) + callee = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi) ✓-ish - StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] +───────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) + StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] +``` + +### InstanceCall - target ⇒ _ callee resolves with inputs [self; Ts] and outputs [T] +``` + target ⇒ _ callee = instance-procedure with inputs [self; Ts] and outputs [T] args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────── (Instance-Call) ✓-ish - InstanceCall target callee args ⇒ T +───────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) + InstanceCall target callee args ⇒ T +``` +### PrimitiveOp (logical) - args ⇐ TBool (each) -────────────────────────────── (Op-Bool) ✓-ish op ∈ {And, Or, AndThen, OrElse, Not, Implies} +``` + args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} +───────────────────────────── (Op-Bool, impl-ish) PrimitiveOp op args ⇒ TBool +``` +### PrimitiveOp (comparison) - args ⇐ Numeric (each) -───────────────────────────── (Op-Cmp) ✓-ish op ∈ {Lt, Leq, Gt, Geq} +``` + args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} +───────────────────────────── (Op-Cmp, impl-ish) PrimitiveOp op args ⇒ TBool +``` +`Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` +rather than a `checkStmtExpr` chain; equivalent under `<:`. + +### PrimitiveOp (equality) - lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l -────────────────────────────────────────────────────────── (Op-Eq) ✓-ish op ∈ {Eq, Neq} - PrimitiveOp op [lhs; rhs] ⇒ TBool +``` + lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} +────────────────────────────────────────────────────── (Op-Eq, impl-ish) + PrimitiveOp op [lhs; rhs] ⇒ TBool +``` +### PrimitiveOp (arithmetic) - args ⇐ Numeric (each) args.head ⇒ T -────────────────────────────────────────── (Op-Arith) ✓-ish op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} +``` + args_i ⇐ Numeric args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} +────────────────────────────────────────── (Op-Arith, impl-ish) PrimitiveOp op args ⇒ T +``` + +The "result is the type of the first argument" rule handles `int + int → int`, +`real + real → real` etc. without unification. A consequence: `int + real` is *not* +flagged today — each operand individually passes `Numeric`. A real fix would be a +numeric-promotion or unification rule; for now this is a known relaxation. +### PrimitiveOp (string concatenation) - args ⇐ TString (each) — current implementation: no operand check -───────────────────────────── (Op-Concat) ✓-ish +``` + args_i ⇐ TString op = StrConcat +───────────────────────────── (Op-Concat, planned) PrimitiveOp op args ⇒ TString ``` -`Numeric` abbreviates "consistent with one of -{name Strata.Laurel.HighType.TInt}`TInt`, {name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` -rather than a `checkStmtExpr` chain; equivalent under the gradual relation. - -Op-Arith's "result is the type of the first argument" rule handles `int + int → int`, -`real + real → real`, etc. without a unification step. A consequence: `int + real` is *not* -flagged because each operand individually passes the numeric check. A real fix would be a -numeric-promotion or unification rule; for now this is a known relaxation. - -Op-Concat currently performs no operand check; the rule above describes the intended -behavior. +The current implementation performs no operand check on `StrConcat`; the planned rule +above describes the intended behavior. -### Object-related and verification forms +### New ``` ref resolves to a composite or datatype T -───────────────────────────────────────────── (New-Ok) ✓ otherwise New ref ⇒ Unknown +───────────────────────────────────────────── (New-Ok, impl) New ref ⇒ UserDefined T -───────────────── (This) ✓ ──────────────────────────── (Abstract / All / ContractOf) ✓ - This ⇒ Unknown Abstract / All / ContractOf … ⇒ Unknown + ref does not resolve to a composite or datatype +───────────────────────────────────────────────── (New-Fallback, impl) + New ref ⇒ Unknown +``` + +### AsType +``` + target ⇒ _ +───────────────────── (AsType, impl) + AsType target T ⇒ T +``` - lhs ⇒ _ rhs ⇒ _ -───────────────────────── (RefEq) ✓ target ⇒ _ - ReferenceEquals lhs rhs ⇒ TBool ────────────────── (AsType) ✓ - AsType target T ⇒ T +`AsType` does not check `target` against `T` — the cast is the user's claim. +### IsType - target ⇒ _ body ⇒ _ -───────────────── (IsType) ✓ ────────────────────────── (Quantifier) ✓ - IsType target T ⇒ TBool Quantifier mode ⟨x, T⟩ trig body ⇒ TBool +``` + target ⇒ _ +────────────────────────── (IsType, impl) + IsType target T ⇒ TBool +``` +### ReferenceEquals - name ⇒ _ v ⇒ T v ⇒ _ -───────────────── (Assigned) ✓ ──────────── (Old) ✓ ────────────── (Fresh) ✓ - Assigned name ⇒ TBool Old v ⇒ T Fresh v ⇒ TBool +``` + lhs ⇒ _ rhs ⇒ _ +─────────────────────────────── (RefEq, impl) + ReferenceEquals lhs rhs ⇒ TBool +``` +### Quantifier - v ⇒ T proof ⇒ _ target ⇒ T_t newVal ⇒ _ -────────────────────── (ProveBy) ✓ ───────────────────────────────── (PureFieldUpdate) ✓ - ProveBy v proof ⇒ T PureFieldUpdate target f newVal ⇒ T_t +``` + body ⇒ _ +───────────────────────────────────────────── (Quantifier, impl) + Quantifier mode ⟨x, T⟩ trig body ⇒ TBool ``` -### Holes +### Assigned ``` - Unknown <: T -───────────────────── (Hole-Some) ✓ ───────────────────── (Hole-None-Synth) ✓ ───────────────────── (Hole-None-Check) ✗ (planned) - Hole d (some T) ⇒ T Hole d none ⇒ Unknown Hole d none ⇐ T + name ⇒ _ +───────────────────────── (Assigned, impl) + Assigned name ⇒ TBool ``` -In check mode, `Hole d none ⇐ T` reduces to subsumption today (`Unknown <: T`, which always +### Old + +``` + v ⇒ T +───────────── (Old, impl) + Old v ⇒ T +``` + +### Fresh + +``` + v ⇒ _ +────────────────── (Fresh, impl) + Fresh v ⇒ TBool +``` + +### ProveBy + +``` + v ⇒ T proof ⇒ _ +────────────────────────── (ProveBy, impl) + ProveBy v proof ⇒ T +``` + +### PureFieldUpdate + +``` + target ⇒ T_t newVal ⇒ _ +───────────────────────────────────── (PureFieldUpdate, impl) + PureFieldUpdate target f newVal ⇒ T_t +``` + +### This + +``` +───────────────────── (This, impl) + This ⇒ Unknown +``` + +### Abstract / All / ContractOf + +``` +──────────────────────────────────────── (Abstract / All / ContractOf, impl) + Abstract / All / ContractOf … ⇒ Unknown +``` + +### Hole + +``` +─────────────────────── (Hole-Some, impl) + Hole d (some T) ⇒ T + + +───────────────────────── (Hole-None-Synth, impl) + Hole d none ⇒ Unknown + + + Unknown <: T +────────────────────── (Hole-None-Check, planned) + Hole d none ⇐ T +``` + +In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always holds). The planned bespoke rule would record the inferred `T` on the hole node so downstream passes can see it, instead of leaving `none` until the hole-inference pass. -## Mutual recursion and termination - -`synthStmtExpr` and `checkStmtExpr` are mutually recursive: the synth rule for -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for the -condition, and the check function falls back to synth via Sub. - -Termination uses a lexicographic measure `(exprMd, tag)` where the tag is `0` for -`synthStmtExpr` and `1` for `checkStmtExpr`. Any descent into a strict subterm decreases -via `Prod.Lex.left` (first component shrinks); Sub calls synth on the *same* expression, -which decreases via `Prod.Lex.right` (second component goes from 1 to 0). This is the -standard well-founded encoding for bidirectional systems where one direction calls the -other on the same input. - -## Two helpers for resolution sites - -Some positions (procedure preconditions, decreases, invariants, postconditions, modifies -clauses, constrained-type witness, etc.) need resolution to run but the type of the -expression is either uninteresting or already known by another path. They use: - -- {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` — the full synth API, returning - `(StmtExprMd × HighTypeMd)`. -- {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` — the check API, returning the resolved - expression and verifying its type is a consistent subtype of the expected type. -- `resolveStmtExpr` — a thin wrapper that calls - `synthStmtExpr` and discards the synthesized type. Used at sites where typing is not - enforced (verification annotations, modifies/reads clauses). - -The right principle is: when the position has a known expected type -({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for `decreases`, the -declared output for a constant initializer or a functional body), use -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. When it doesn't, use -`resolveStmtExpr`. {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` -itself is mostly an internal interface used by other rules. - -## Returns and the expected return type - -`Return e` synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` (the construct itself -produces no value), but the *value being returned* should be checked against the enclosing -procedure's declared output type. The intended design: thread the expected return type -through {name Strata.Laurel.ResolveState}`ResolveState`, set it from `proc.outputs` in -{name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` before resolving the -body, and have the `Return` rule push the expected type into its value via -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. This closes a soundness gap in the -synth-only design where `return 0` in a `bool`-returning procedure was not caught (because -the body's overall synthesized type was {name Strata.Laurel.HighType.TVoid}`TVoid` and the -body-vs-output check was skipped on `TVoid`). - -## What this is, in type-system terms - -The checker is: - -- *bidirectional*, with a single subsumption rule at the synth↔check boundary, -- with a *gradual* relation (`isConsistentSubtype`) - rather than a strict one — {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic - type, justified by Laurel's targeting of dynamic source languages, -- over a *nominal-with-stubs* subtype relation - (`isSubtype`) — currently structural equality, intended to - walk inheritance chains and unwrap aliases / constrained types, -- with *arity tracking via tuple types* - ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`) for multi-output - procedures, -- and *side-effecting expressions modeled as* - {name Strata.Laurel.HighType.TVoid}`TVoid` so blocks, returns, and loops compose cleanly. - -The wildcard carve-out for {name Strata.Laurel.HighType.UserDefined}`UserDefined` from the -previous design is gone — user-defined types are no longer a backdoor through the checker. -The {name Strata.Laurel.HighType.TCore}`TCore` carve-out is preserved for now as a -migration aid and is expected to be removed. - # Translation Pipeline Laurel programs are verified by translating them to Strata Core and then invoking the Core From 7d24b64f955d6b121479f2d884ed7dfb40f3d74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:48:06 -0400 Subject: [PATCH 016/128] concise explanations --- docs/verso/LaurelDoc.lean | 72 +++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 1577232261..d79bf17900 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -243,10 +243,10 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, ## Typing rules -Each construct is given as a derivation. Premises sit above the line, conclusion below. -Rules tagged `(impl)` are implemented; rules tagged `(planned)` describe the intended -behavior but aren't yet wired in. `Γ` (the lexical scope) is left implicit; every rule -threads it identically. +Each construct is given as a derivation. `(impl)` = implemented; `(planned)` = intended, +not yet wired in. `(impl-ish)` = implemented but still calls a legacy helper (`checkBool` / +`checkNumeric`/`checkAssignable`) instead of going through `checkStmtExpr`; functionally +equivalent under `<:`. ### Sub (subsumption) @@ -256,8 +256,7 @@ e ⇒ A A <: B e ⇐ B ``` -The default fallback in `checkStmtExpr`. Used by every construct that doesn't have a -bespoke check rule. +Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### LiteralInt @@ -303,8 +302,8 @@ bespoke check rule. Var (.Field e f) ⇒ T_f ``` -`f` is resolved against the type of `e` (or the enclosing instance type for `self.f`); the -typing rule is independent of which path resolution took. +Resolution looks `f` up against the type of `e` (or the enclosing instance type for +`self.f`); the typing rule itself is path-agnostic. ### Var (.Declare) @@ -332,15 +331,12 @@ cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T IfThenElse cond thenBr (some elseBr) ⇐ T ``` -If-NoElse synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value -to give back when `cond` is false. Without this rule, `x : int := if c then 5` would -type-check spuriously. +If-NoElse uses {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when +`cond` is false; without it, `x : int := if c then 5` would type-check spuriously. -If-Synth picks the then-branch type; the result is always consumed by an enclosing -`checkAssignable` or by Sub, which provides a one-sided check against the surrounding -context. The two branches are deliberately not compared against each other: statement-position -`if`s commonly mix a value branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch -(early `return`, `exit`, `assert`, …), which a strict equality check would reject incorrectly. +If-Synth picks the then-branch arbitrarily and does *not* compare branches: a statement- +position `if` often pairs a value branch with a `return`/`exit`/`assert`. The surrounding +context's `checkAssignable` or Sub provides the actual check downstream. ### Block @@ -364,15 +360,13 @@ context. The two branches are deliberately not compared against each other: stat Block [] label ⇐ T ``` -The non-last statements are synthesized but their types are discarded — this is the lax -rule. It matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` -returns a value is normal idiomatic code. The cost is that `5;` (a literal in statement -position) is silently accepted; flagging it would belong to a lint, not the type checker. +Non-last statements are synthesized but their types discarded (the lax rule). This matches +Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` +is silently accepted; flagging it belongs to a lint. -In check mode, the expected type is pushed into the *last* statement rather than checked at -the boundary. Errors then fire at the offending subexpression inside `s_n`, and the -expected type keeps propagating through nested -{name Strata.Laurel.StmtExpr.Block}`Block` / +Check mode pushes `T` into the *last* statement instead of comparing the block's +synthesized type at the boundary. Errors then fire at the offending subexpression, and `T` +keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. @@ -401,11 +395,11 @@ expected type keeps propagating through nested Return (some e) ⇒ TVoid ``` -The current `Return-Some` rule discards the value's synthesized type. The planned rule -threads the expected return type through {name Strata.Laurel.ResolveState}`ResolveState` -(set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`), so `return 0` in -a `bool`-returning procedure can be caught at the `Return` site. +`Return-Some` currently throws away the value's type, so `return 0` in a `bool`-returning +procedure isn't caught. The planned rule threads the expected return type through +{name Strata.Laurel.ResolveState}`ResolveState` (set from `proc.outputs` in +{name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`). ### While @@ -415,9 +409,8 @@ a `bool`-returning procedure can be caught at the `Return` site. While cond invs dec body ⇒ TVoid ``` -`impl-ish` here means the rule is implemented but `cond` and `invs_i` go through the legacy -`checkBool` helper rather than `checkStmtExpr cond TBool`. Functionally equivalent under -`<:`; slated for migration. +`dec` (the optional decreases clause) is currently resolved without a type check; the +intended target is a numeric type, not yet enforced. ### Assert @@ -490,8 +483,7 @@ a `bool`-returning procedure can be caught at the `Return` site. `Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, {name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` -rather than a `checkStmtExpr` chain; equivalent under `<:`. +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". ### PrimitiveOp (equality) @@ -509,10 +501,9 @@ rather than a `checkStmtExpr` chain; equivalent under `<:`. PrimitiveOp op args ⇒ T ``` -The "result is the type of the first argument" rule handles `int + int → int`, -`real + real → real` etc. without unification. A consequence: `int + real` is *not* -flagged today — each operand individually passes `Numeric`. A real fix would be a -numeric-promotion or unification rule; for now this is a known relaxation. +"Result is the type of the first argument" handles `int + int → int`, `real + real → real`, +etc. without unification. Known relaxation: `int + real` passes (each operand individually +passes `Numeric`); a proper fix needs numeric promotion or unification. ### PrimitiveOp (string concatenation) @@ -522,8 +513,7 @@ numeric-promotion or unification rule; for now this is a known relaxation. PrimitiveOp op args ⇒ TString ``` -The current implementation performs no operand check on `StrConcat`; the planned rule -above describes the intended behavior. +Operand check not yet implemented — `StrConcat` accepts any operands today. ### New @@ -546,7 +536,7 @@ above describes the intended behavior. AsType target T ⇒ T ``` -`AsType` does not check `target` against `T` — the cast is the user's claim. +`target` is resolved but not checked against `T` — the cast is the user's claim. ### IsType From 2d8531948737c2fda8b8862c5ac5c8b2c763c196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:49:38 -0400 Subject: [PATCH 017/128] restore contexts in rules --- docs/verso/LaurelDoc.lean | 290 ++++++++++++++++++++------------------ 1 file changed, 151 insertions(+), 139 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index d79bf17900..7eb90d4f87 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -243,17 +243,19 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, ## Typing rules -Each construct is given as a derivation. `(impl)` = implemented; `(planned)` = intended, -not yet wired in. `(impl-ish)` = implemented but still calls a legacy helper (`checkBool` / -`checkNumeric`/`checkAssignable`) instead of going through `checkStmtExpr`; functionally -equivalent under `<:`. +Each construct is given as a derivation. `Γ` is the current lexical scope (see +{name Strata.Laurel.ResolveState}`ResolveState`'s `scope`); it threads identically through +every premise and conclusion unless a rule explicitly extends it (written `Γ, x : T`). +`(impl)` = implemented; `(planned)` = intended, not yet wired in. `(impl-ish)` = implemented +but still calls a legacy helper (`checkBool` / `checkNumeric` / `checkAssignable`) instead of +going through `checkStmtExpr`; functionally equivalent under `<:`. ### Sub (subsumption) ``` -e ⇒ A A <: B -───────────────── (Sub, impl) - e ⇐ B +Γ ⊢ e ⇒ A A <: B +───────────────────── (Sub, impl) + Γ ⊢ e ⇐ B ``` Fallback in `checkStmtExpr` whenever no bespoke check rule applies. @@ -261,45 +263,45 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### LiteralInt ``` -───────────────────── (Lit-Int, impl) - LiteralInt n ⇒ TInt +────────────────────────── (Lit-Int, impl) + Γ ⊢ LiteralInt n ⇒ TInt ``` ### LiteralBool ``` -────────────────────── (Lit-Bool, impl) - LiteralBool b ⇒ TBool +─────────────────────────── (Lit-Bool, impl) + Γ ⊢ LiteralBool b ⇒ TBool ``` ### LiteralString ``` -──────────────────────────── (Lit-String, impl) - LiteralString s ⇒ TString +───────────────────────────────── (Lit-String, impl) + Γ ⊢ LiteralString s ⇒ TString ``` ### LiteralDecimal ``` -───────────────────────────── (Lit-Decimal, impl) - LiteralDecimal d ⇒ TReal +────────────────────────────────── (Lit-Decimal, impl) + Γ ⊢ LiteralDecimal d ⇒ TReal ``` ### Var (.Local) ``` - Γ(x) = T -────────────────────── (Var-Local, impl) - Var (.Local x) ⇒ T + Γ(x) = T +─────────────────────────── (Var-Local, impl) + Γ ⊢ Var (.Local x) ⇒ T ``` ### Var (.Field) ``` - e ⇒ _ Γ(f) = T_f -───────────────────────── (Var-Field, impl) - Var (.Field e f) ⇒ T_f + Γ ⊢ e ⇒ _ Γ(f) = T_f +────────────────────────────── (Var-Field, impl) + Γ ⊢ Var (.Field e f) ⇒ T_f ``` Resolution looks `f` up against the type of `e` (or the enclosing instance type for @@ -308,27 +310,30 @@ Resolution looks `f` up against the type of `e` (or the enclosing instance type ### Var (.Declare) ``` - Γ(x) ↦ T fresh -────────────────────────────────── (Var-Declare, impl) - Var (.Declare ⟨x, T⟩) ⇒ TVoid + x ∉ dom(Γ) +───────────────────────────────────────── (Var-Declare, impl) + Γ ⊢ Var (.Declare ⟨x, T⟩) ⇒ TVoid ⊣ Γ, x : T ``` +`⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the +remainder of the enclosing scope. + ### IfThenElse ``` -cond ⇐ TBool thenBr ⇒ T -───────────────────────────────────────── (If-NoElse, impl) - IfThenElse cond thenBr none ⇒ TVoid +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T +───────────────────────────────────────────── (If-NoElse, impl) + Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid -cond ⇐ TBool thenBr ⇒ T_t elseBr ⇒ T_e -───────────────────────────────────────────────── (If-Synth, impl) - IfThenElse cond thenBr (some elseBr) ⇒ T_t +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e +────────────────────────────────────────────────────────────── (If-Synth, impl) + Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t -cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T -───────────────────────────────────────────── (If-Check, planned) - IfThenElse cond thenBr (some elseBr) ⇐ T +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T +────────────────────────────────────────────────────────── (If-Check, planned) + Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T ``` If-NoElse uses {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when @@ -341,25 +346,30 @@ context's `checkAssignable` or Sub provides the actual check downstream. ### Block ``` - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T -─────────────────────────────────────────────────── (Block-Synth, impl) - Block [s_1; …; s_n] label ⇒ T +Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T +─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) + Γ ⊢ Block [s_1; …; s_n] label ⇒ T -──────────────────────── (Block-Synth-Empty, impl) - Block [] label ⇒ TVoid +───────────────────────────── (Block-Synth-Empty, impl) + Γ ⊢ Block [] label ⇒ TVoid - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T -─────────────────────────────────────────────────── (Block-Check, impl) - Block [s_1; …; s_n] label ⇐ T +Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T +─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) + Γ ⊢ Block [s_1; …; s_n] label ⇐ T TVoid <: T -────────────────────── (Block-Check-Empty, impl) - Block [] label ⇐ T +───────────────────────── (Block-Check-Empty, impl) + Γ ⊢ Block [] label ⇐ T ``` +The notation `Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced +by its predecessor and may itself extend the scope (`Var (.Declare …)` does); the +`Γ_{n-1}` that types `s_n` is the scope after all earlier declarations. Bindings introduced +inside the block don't escape — `Γ` is what surrounds the block. + Non-last statements are synthesized but their types discarded (the lax rule). This matches Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` is silently accepted; flagging it belongs to a lint. @@ -374,25 +384,25 @@ keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / ### Exit ``` -───────────────────── (Exit, impl) - Exit target ⇒ TVoid +──────────────────────── (Exit, impl) + Γ ⊢ Exit target ⇒ TVoid ``` ### Return ``` -───────────────────────── (Return-None, impl) - Return none ⇒ TVoid +───────────────────────────── (Return-None, impl) + Γ ⊢ Return none ⇒ TVoid - e ⇒ _ -────────────────────────── (Return-Some, impl) - Return (some e) ⇒ TVoid + Γ ⊢ e ⇒ _ +────────────────────────────── (Return-Some, impl) + Γ ⊢ Return (some e) ⇒ TVoid - Γ_proc.outputs = [T] e ⇐ T -───────────────────────────────── (Return-Some-Checked, planned) - Return (some e) ⇒ TVoid + Γ_proc.outputs = [T] Γ ⊢ e ⇐ T +────────────────────────────────────── (Return-Some-Checked, planned) + Γ ⊢ Return (some e) ⇒ TVoid ``` `Return-Some` currently throws away the value's type, so `return 0` in a `bool`-returning @@ -404,9 +414,9 @@ procedure isn't caught. The planned rule threads the expected return type throug ### While ``` - cond ⇐ TBool invs_i ⇐ TBool dec ⇐ ? body ⇒ _ -───────────────────────────────────────────────────────────── (While, impl-ish) - While cond invs dec body ⇒ TVoid + Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ +─────────────────────────────────────────────────────────────────────────────── (While, impl-ish) + Γ ⊢ While cond invs dec body ⇒ TVoid ``` `dec` (the optional decreases clause) is currently resolved without a type check; the @@ -415,70 +425,70 @@ intended target is a numeric type, not yet enforced. ### Assert ``` - cond ⇐ TBool -────────────────────────── (Assert, impl-ish) - Assert cond ⇒ TVoid + Γ ⊢ cond ⇐ TBool +────────────────────────────── (Assert, impl-ish) + Γ ⊢ Assert cond ⇒ TVoid ``` ### Assume ``` - cond ⇐ TBool -───────────────────── (Assume, impl-ish) - Assume cond ⇒ TVoid + Γ ⊢ cond ⇐ TBool +───────────────────────────── (Assume, impl-ish) + Γ ⊢ Assume cond ⇒ TVoid ``` ### Assign ``` - Γ(x) = T_x e ⇒ T_e T_e <: T_x -───────────────────────────────────────── (Assign-Single, impl-ish) - Assign [x] e ⇒ TVoid + Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x +─────────────────────────────────────────────── (Assign-Single, impl-ish) + Γ ⊢ Assign [x] e ⇒ TVoid - targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) - Assign targets e ⇒ TVoid + Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i +───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) + Γ ⊢ Assign targets e ⇒ TVoid ``` ### StaticCall ``` - callee = static-procedure with inputs Ts and outputs [T] - args ⇒ Us U_i <: T_i (pairwise) -──────────────────────────────────────────────────────────── (Static-Call, impl-ish) - StaticCall callee args ⇒ T + Γ(callee) = static-procedure with inputs Ts and outputs [T] + Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) +───────────────────────────────────────────────────────────── (Static-Call, impl-ish) + Γ ⊢ StaticCall callee args ⇒ T - callee = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 - args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) - StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] + Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 + Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) +────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) + Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` ### InstanceCall ``` - target ⇒ _ callee = instance-procedure with inputs [self; Ts] and outputs [T] - args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) - InstanceCall target callee args ⇒ T + Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] + Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) +───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) + Γ ⊢ InstanceCall target callee args ⇒ T ``` ### PrimitiveOp (logical) ``` - args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} -───────────────────────────── (Op-Bool, impl-ish) - PrimitiveOp op args ⇒ TBool + Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} +────────────────────────────────── (Op-Bool, impl-ish) + Γ ⊢ PrimitiveOp op args ⇒ TBool ``` ### PrimitiveOp (comparison) ``` - args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} -───────────────────────────── (Op-Cmp, impl-ish) - PrimitiveOp op args ⇒ TBool + Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} +───────────────────────────────── (Op-Cmp, impl-ish) + Γ ⊢ PrimitiveOp op args ⇒ TBool ``` `Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, @@ -488,17 +498,17 @@ intended target is a numeric type, not yet enforced. ### PrimitiveOp (equality) ``` - lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} -────────────────────────────────────────────────────── (Op-Eq, impl-ish) - PrimitiveOp op [lhs; rhs] ⇒ TBool + Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} +───────────────────────────────────────────────────────────────── (Op-Eq, impl-ish) + Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` ### PrimitiveOp (arithmetic) ``` - args_i ⇐ Numeric args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} -────────────────────────────────────────── (Op-Arith, impl-ish) - PrimitiveOp op args ⇒ T + Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} +────────────────────────────────────────────────── (Op-Arith, impl-ish) + Γ ⊢ PrimitiveOp op args ⇒ T ``` "Result is the type of the first argument" handles `int + int → int`, `real + real → real`, @@ -508,9 +518,9 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ### PrimitiveOp (string concatenation) ``` - args_i ⇐ TString op = StrConcat -───────────────────────────── (Op-Concat, planned) - PrimitiveOp op args ⇒ TString + Γ ⊢ args_i ⇐ TString op = StrConcat +───────────────────────────────────── (Op-Concat, planned) + Γ ⊢ PrimitiveOp op args ⇒ TString ``` Operand check not yet implemented — `StrConcat` accepts any operands today. @@ -518,22 +528,22 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. ### New ``` - ref resolves to a composite or datatype T -───────────────────────────────────────────── (New-Ok, impl) - New ref ⇒ UserDefined T + Γ(ref) is a composite or datatype T +────────────────────────────────────────── (New-Ok, impl) + Γ ⊢ New ref ⇒ UserDefined T - ref does not resolve to a composite or datatype -───────────────────────────────────────────────── (New-Fallback, impl) - New ref ⇒ Unknown + Γ(ref) is not a composite or datatype +───────────────────────────────────────── (New-Fallback, impl) + Γ ⊢ New ref ⇒ Unknown ``` ### AsType ``` - target ⇒ _ -───────────────────── (AsType, impl) - AsType target T ⇒ T + Γ ⊢ target ⇒ _ +───────────────────────────── (AsType, impl) + Γ ⊢ AsType target T ⇒ T ``` `target` is resolved but not checked against `T` — the cast is the user's claim. @@ -541,95 +551,97 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. ### IsType ``` - target ⇒ _ -────────────────────────── (IsType, impl) - IsType target T ⇒ TBool + Γ ⊢ target ⇒ _ +───────────────────────────────── (IsType, impl) + Γ ⊢ IsType target T ⇒ TBool ``` ### ReferenceEquals ``` - lhs ⇒ _ rhs ⇒ _ -─────────────────────────────── (RefEq, impl) - ReferenceEquals lhs rhs ⇒ TBool + Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ +─────────────────────────────────────── (RefEq, impl) + Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` ### Quantifier ``` - body ⇒ _ -───────────────────────────────────────────── (Quantifier, impl) - Quantifier mode ⟨x, T⟩ trig body ⇒ TBool + Γ, x : T ⊢ body ⇒ _ +───────────────────────────────────────────────── (Quantifier, impl) + Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool ``` +The bound variable `x : T` is introduced in scope only for the body (and trigger). + ### Assigned ``` - name ⇒ _ -───────────────────────── (Assigned, impl) - Assigned name ⇒ TBool + Γ ⊢ name ⇒ _ +───────────────────────────── (Assigned, impl) + Γ ⊢ Assigned name ⇒ TBool ``` ### Old ``` - v ⇒ T -───────────── (Old, impl) - Old v ⇒ T + Γ ⊢ v ⇒ T +───────────────── (Old, impl) + Γ ⊢ Old v ⇒ T ``` ### Fresh ``` - v ⇒ _ -────────────────── (Fresh, impl) - Fresh v ⇒ TBool + Γ ⊢ v ⇒ _ +───────────────────── (Fresh, impl) + Γ ⊢ Fresh v ⇒ TBool ``` ### ProveBy ``` - v ⇒ T proof ⇒ _ -────────────────────────── (ProveBy, impl) - ProveBy v proof ⇒ T + Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ +─────────────────────────────────── (ProveBy, impl) + Γ ⊢ ProveBy v proof ⇒ T ``` ### PureFieldUpdate ``` - target ⇒ T_t newVal ⇒ _ -───────────────────────────────────── (PureFieldUpdate, impl) - PureFieldUpdate target f newVal ⇒ T_t + Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ +─────────────────────────────────────────────── (PureFieldUpdate, impl) + Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t ``` ### This ``` -───────────────────── (This, impl) - This ⇒ Unknown +────────────────────────── (This, impl) + Γ ⊢ This ⇒ Unknown ``` ### Abstract / All / ContractOf ``` -──────────────────────────────────────── (Abstract / All / ContractOf, impl) - Abstract / All / ContractOf … ⇒ Unknown +───────────────────────────────────────────── (Abstract / All / ContractOf, impl) + Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown ``` ### Hole ``` -─────────────────────── (Hole-Some, impl) - Hole d (some T) ⇒ T +──────────────────────────── (Hole-Some, impl) + Γ ⊢ Hole d (some T) ⇒ T -───────────────────────── (Hole-None-Synth, impl) - Hole d none ⇒ Unknown +───────────────────────────────── (Hole-None-Synth, impl) + Γ ⊢ Hole d none ⇒ Unknown - Unknown <: T -────────────────────── (Hole-None-Check, planned) - Hole d none ⇐ T + Unknown <: T +───────────────────────── (Hole-None-Check, planned) + Γ ⊢ Hole d none ⇐ T ``` In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always From 55083a6716ee3a3a3054188846a3dff828d1cc62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:53:23 -0400 Subject: [PATCH 018/128] simplify presentation --- docs/verso/LaurelDoc.lean | 235 +++++++++++++++++++++++++------------- 1 file changed, 157 insertions(+), 78 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 7eb90d4f87..ff0d09183d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -250,7 +250,26 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x but still calls a legacy helper (`checkBool` / `checkNumeric` / `checkAssignable`) instead of going through `checkStmtExpr`; functionally equivalent under `<:`. -### Sub (subsumption) +### Index + +- *Subsumption* — Sub +- *Literals* — Lit-Int, Lit-Bool, Lit-String, Lit-Decimal +- *Variables* — Var-Local, Var-Field, Var-Declare +- *Control flow* — If-NoElse, If-Synth, If-Check (planned); Block-Synth, Block-Synth-Empty, + Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, Return-Some-Checked + (planned); While +- *Verification statements* — Assert, Assume +- *Assignment* — Assign-Single, Assign-Multi +- *Calls* — Static-Call, Static-Call-Multi, Instance-Call +- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat (planned) +- *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate +- *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy +- *Untyped forms* — This; Abstract / All / ContractOf +- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) + +### Subsumption + +#### Sub ``` Γ ⊢ e ⇒ A A <: B @@ -260,35 +279,39 @@ going through `checkStmtExpr`; functionally equivalent under `<:`. Fallback in `checkStmtExpr` whenever no bespoke check rule applies. -### LiteralInt +### Literals + +#### Lit-Int ``` ────────────────────────── (Lit-Int, impl) Γ ⊢ LiteralInt n ⇒ TInt ``` -### LiteralBool +#### Lit-Bool ``` ─────────────────────────── (Lit-Bool, impl) Γ ⊢ LiteralBool b ⇒ TBool ``` -### LiteralString +#### Lit-String ``` ───────────────────────────────── (Lit-String, impl) Γ ⊢ LiteralString s ⇒ TString ``` -### LiteralDecimal +#### Lit-Decimal ``` ────────────────────────────────── (Lit-Decimal, impl) Γ ⊢ LiteralDecimal d ⇒ TReal ``` -### Var (.Local) +### Variables + +#### Var-Local ``` Γ(x) = T @@ -296,7 +319,7 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. Γ ⊢ Var (.Local x) ⇒ T ``` -### Var (.Field) +#### Var-Field ``` Γ ⊢ e ⇒ _ Γ(f) = T_f @@ -307,7 +330,7 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. Resolution looks `f` up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -### Var (.Declare) +#### Var-Declare ``` x ∉ dom(Γ) @@ -318,100 +341,125 @@ Resolution looks `f` up against the type of `e` (or the enclosing instance type `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. -### IfThenElse +### Control flow + +#### If-NoElse ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T ───────────────────────────────────────────── (If-NoElse, impl) Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid +``` + +The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no +value when `cond` is false; without this, `x : int := if c then 5` would type-check +spuriously. +#### If-Synth +``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e ────────────────────────────────────────────────────────────── (If-Synth, impl) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t +``` + +Picks the then-branch type arbitrarily; the two branches are *not* compared, since a +statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The +enclosing `checkAssignable` or Sub provides the actual check downstream. +#### If-Check +``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T ────────────────────────────────────────────────────────── (If-Check, planned) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T ``` -If-NoElse uses {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when -`cond` is false; without it, `x : int := if c then 5` would type-check spuriously. - -If-Synth picks the then-branch arbitrarily and does *not* compare branches: a statement- -position `if` often pairs a value branch with a `return`/`exit`/`assert`. The surrounding -context's `checkAssignable` or Sub provides the actual check downstream. - -### Block +#### Block-Synth ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T ─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) Γ ⊢ Block [s_1; …; s_n] label ⇒ T +``` + +`Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced by its +predecessor and may itself extend it (`Var (.Declare …)` does); `s_n` is typed in +`Γ_{n-1}`. Bindings introduced inside the block don't escape — `Γ` is what surrounds the +block. +Non-last statements are synthesized but their types discarded (the lax rule). This matches +Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` +is silently accepted; flagging it belongs to a lint. +#### Block-Synth-Empty + +``` ───────────────────────────── (Block-Synth-Empty, impl) Γ ⊢ Block [] label ⇒ TVoid +``` +#### Block-Check +``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T ─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) Γ ⊢ Block [s_1; …; s_n] label ⇐ T +``` + +Pushes `T` into the *last* statement rather than comparing the block's synthesized type at +the boundary. Errors fire at the offending subexpression, and `T` keeps propagating through +nested {name Strata.Laurel.StmtExpr.Block}`Block` / +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / +{name Strata.Laurel.StmtExpr.Hole}`Hole` / +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. +#### Block-Check-Empty +``` TVoid <: T ───────────────────────── (Block-Check-Empty, impl) Γ ⊢ Block [] label ⇐ T ``` -The notation `Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced -by its predecessor and may itself extend the scope (`Var (.Declare …)` does); the -`Γ_{n-1}` that types `s_n` is the scope after all earlier declarations. Bindings introduced -inside the block don't escape — `Γ` is what surrounds the block. - -Non-last statements are synthesized but their types discarded (the lax rule). This matches -Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` -is silently accepted; flagging it belongs to a lint. - -Check mode pushes `T` into the *last* statement instead of comparing the block's -synthesized type at the boundary. Errors then fire at the offending subexpression, and `T` -keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / -{name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. - -### Exit +#### Exit ``` ──────────────────────── (Exit, impl) Γ ⊢ Exit target ⇒ TVoid ``` -### Return +#### Return-None ``` ───────────────────────────── (Return-None, impl) Γ ⊢ Return none ⇒ TVoid +``` +#### Return-Some +``` Γ ⊢ e ⇒ _ ────────────────────────────── (Return-Some, impl) Γ ⊢ Return (some e) ⇒ TVoid +``` +The value's synthesized type is currently discarded, so `return 0` in a `bool`-returning +procedure isn't caught. Replaced by Return-Some-Checked once the expected return type is +threaded through {name Strata.Laurel.ResolveState}`ResolveState`. +#### Return-Some-Checked + +``` Γ_proc.outputs = [T] Γ ⊢ e ⇐ T ────────────────────────────────────── (Return-Some-Checked, planned) Γ ⊢ Return (some e) ⇒ TVoid ``` -`Return-Some` currently throws away the value's type, so `return 0` in a `bool`-returning -procedure isn't caught. The planned rule threads the expected return type through -{name Strata.Laurel.ResolveState}`ResolveState` (set from `proc.outputs` in -{name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`). +Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`. -### While +#### While ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ @@ -419,10 +467,12 @@ procedure isn't caught. The planned rule threads the expected return type throug Γ ⊢ While cond invs dec body ⇒ TVoid ``` -`dec` (the optional decreases clause) is currently resolved without a type check; the -intended target is a numeric type, not yet enforced. +`dec` (the optional decreases clause) is resolved without a type check today; the intended +target is a numeric type. + +### Verification statements -### Assert +#### Assert ``` Γ ⊢ cond ⇐ TBool @@ -430,7 +480,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ Assert cond ⇒ TVoid ``` -### Assume +#### Assume ``` Γ ⊢ cond ⇐ TBool @@ -438,35 +488,45 @@ intended target is a numeric type, not yet enforced. Γ ⊢ Assume cond ⇒ TVoid ``` -### Assign +### Assignment + +#### Assign-Single ``` Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x ─────────────────────────────────────────────── (Assign-Single, impl-ish) Γ ⊢ Assign [x] e ⇒ TVoid +``` +#### Assign-Multi +``` Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i ───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) Γ ⊢ Assign targets e ⇒ TVoid ``` -### StaticCall +### Calls + +#### Static-Call ``` Γ(callee) = static-procedure with inputs Ts and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) ───────────────────────────────────────────────────────────── (Static-Call, impl-ish) Γ ⊢ StaticCall callee args ⇒ T +``` +#### Static-Call-Multi +``` Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) ────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` -### InstanceCall +#### Instance-Call ``` Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] @@ -475,7 +535,13 @@ intended target is a numeric type, not yet enforced. Γ ⊢ InstanceCall target callee args ⇒ T ``` -### PrimitiveOp (logical) +### Primitive operations + +`Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". + +#### Op-Bool ``` Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} @@ -483,7 +549,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -### PrimitiveOp (comparison) +#### Op-Cmp ``` Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} @@ -491,11 +557,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -`Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, -{name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`". - -### PrimitiveOp (equality) +#### Op-Eq ``` Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} @@ -503,7 +565,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` -### PrimitiveOp (arithmetic) +#### Op-Arith ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} @@ -515,7 +577,7 @@ intended target is a numeric type, not yet enforced. etc. without unification. Known relaxation: `int + real` passes (each operand individually passes `Numeric`); a proper fix needs numeric promotion or unification. -### PrimitiveOp (string concatenation) +#### Op-Concat ``` Γ ⊢ args_i ⇐ TString op = StrConcat @@ -525,20 +587,25 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. Operand check not yet implemented — `StrConcat` accepts any operands today. -### New +### Object forms + +#### New-Ok ``` Γ(ref) is a composite or datatype T ────────────────────────────────────────── (New-Ok, impl) Γ ⊢ New ref ⇒ UserDefined T +``` +#### New-Fallback +``` Γ(ref) is not a composite or datatype ───────────────────────────────────────── (New-Fallback, impl) Γ ⊢ New ref ⇒ Unknown ``` -### AsType +#### AsType ``` Γ ⊢ target ⇒ _ @@ -548,7 +615,7 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. `target` is resolved but not checked against `T` — the cast is the user's claim. -### IsType +#### IsType ``` Γ ⊢ target ⇒ _ @@ -556,7 +623,7 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. Γ ⊢ IsType target T ⇒ TBool ``` -### ReferenceEquals +#### RefEq ``` Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ @@ -564,7 +631,17 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` -### Quantifier +#### PureFieldUpdate + +``` + Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ +─────────────────────────────────────────────── (PureFieldUpdate, impl) + Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t +``` + +### Verification expressions + +#### Quantifier ``` Γ, x : T ⊢ body ⇒ _ @@ -574,7 +651,7 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. The bound variable `x : T` is introduced in scope only for the body (and trigger). -### Assigned +#### Assigned ``` Γ ⊢ name ⇒ _ @@ -582,7 +659,7 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ Assigned name ⇒ TBool ``` -### Old +#### Old ``` Γ ⊢ v ⇒ T @@ -590,7 +667,7 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ Old v ⇒ T ``` -### Fresh +#### Fresh ``` Γ ⊢ v ⇒ _ @@ -598,7 +675,7 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ Fresh v ⇒ TBool ``` -### ProveBy +#### ProveBy ``` Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ @@ -606,47 +683,49 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ ProveBy v proof ⇒ T ``` -### PureFieldUpdate - -``` - Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ -─────────────────────────────────────────────── (PureFieldUpdate, impl) - Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t -``` +### Untyped forms -### This +#### This ``` ────────────────────────── (This, impl) Γ ⊢ This ⇒ Unknown ``` -### Abstract / All / ContractOf +#### Abstract / All / ContractOf ``` ───────────────────────────────────────────── (Abstract / All / ContractOf, impl) Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown ``` -### Hole +### Holes + +#### Hole-Some ``` ──────────────────────────── (Hole-Some, impl) Γ ⊢ Hole d (some T) ⇒ T +``` +#### Hole-None-Synth +``` ───────────────────────────────── (Hole-None-Synth, impl) Γ ⊢ Hole d none ⇒ Unknown +``` +#### Hole-None-Check +``` Unknown <: T ───────────────────────── (Hole-None-Check, planned) Γ ⊢ Hole d none ⇐ T ``` In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always -holds). The planned bespoke rule would record the inferred `T` on the hole node so -downstream passes can see it, instead of leaving `none` until the hole-inference pass. +holds). The planned rule would record the inferred `T` on the hole node so downstream +passes can see it, instead of leaving `none` until the hole-inference pass. # Translation Pipeline From 1feda5bc8c7246f6b1ef0389bfeb1e78aad68c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:14:16 -0400 Subject: [PATCH 019/128] add back in contexts --- docs/verso/LaurelDoc.lean | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index ff0d09183d..4b5f314c2d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -160,8 +160,8 @@ mismatches against the surrounding context become diagnostics. The implementatio There are two operations on expressions, written here in standard bidirectional notation: ``` -e ⇒ T -- "e synthesizes T" (synthStmtExpr) -e ⇐ T -- "e checks against T" (checkStmtExpr) +Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) +Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) ``` Synthesis returns a type inferred from the expression itself; checking verifies that the @@ -170,9 +170,9 @@ is determined locally (synth) or by context (check). The two judgments are conne single change-of-direction rule, *subsumption*: ``` -e ⇒ A A <: B -───────────────── (Sub) - e ⇐ B +Γ ⊢ e ⇒ A A <: B +───────────────────── (Sub) + Γ ⊢ e ⇐ B ``` Subsumption is the *only* place the checker switches from check to synth mode. It fires as From 9bb2990e9de638b16a7bea013d815742c759b5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:50:07 -0400 Subject: [PATCH 020/128] =?UTF-8?q?describe=20literals=20and=20easy=20rule?= =?UTF-8?q?s=20(call,=20assert/assume=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/verso/LaurelDoc.lean | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4b5f314c2d..4abf2eff2c 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -246,9 +246,7 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, Each construct is given as a derivation. `Γ` is the current lexical scope (see {name Strata.Laurel.ResolveState}`ResolveState`'s `scope`); it threads identically through every premise and conclusion unless a rule explicitly extends it (written `Γ, x : T`). -`(impl)` = implemented; `(planned)` = intended, not yet wired in. `(impl-ish)` = implemented -but still calls a legacy helper (`checkBool` / `checkNumeric` / `checkAssignable`) instead of -going through `checkStmtExpr`; functionally equivalent under `<:`. +`(impl)` = implemented; `(planned)` = intended, not yet wired in. ### Index @@ -261,7 +259,7 @@ going through `checkStmtExpr`; functionally equivalent under `<:`. - *Verification statements* — Assert, Assume - *Assignment* — Assign-Single, Assign-Multi - *Calls* — Static-Call, Static-Call-Multi, Instance-Call -- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat (planned) +- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate - *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy - *Untyped forms* — This; Abstract / All / ContractOf @@ -365,7 +363,8 @@ spuriously. Picks the then-branch type arbitrarily; the two branches are *not* compared, since a statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The -enclosing `checkAssignable` or Sub provides the actual check downstream. +enclosing context's check (Sub, or a containing `checkSubtype` like an assignment) provides +the actual check downstream. #### If-Check @@ -463,7 +462,7 @@ Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedur ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ -─────────────────────────────────────────────────────────────────────────────── (While, impl-ish) +─────────────────────────────────────────────────────────────────────────────── (While, impl) Γ ⊢ While cond invs dec body ⇒ TVoid ``` @@ -476,7 +475,7 @@ target is a numeric type. ``` Γ ⊢ cond ⇐ TBool -────────────────────────────── (Assert, impl-ish) +────────────────────────────── (Assert, impl) Γ ⊢ Assert cond ⇒ TVoid ``` @@ -484,7 +483,7 @@ target is a numeric type. ``` Γ ⊢ cond ⇐ TBool -───────────────────────────── (Assume, impl-ish) +───────────────────────────── (Assume, impl) Γ ⊢ Assume cond ⇒ TVoid ``` @@ -494,7 +493,7 @@ target is a numeric type. ``` Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x -─────────────────────────────────────────────── (Assign-Single, impl-ish) +─────────────────────────────────────────────── (Assign-Single, impl) Γ ⊢ Assign [x] e ⇒ TVoid ``` @@ -502,7 +501,7 @@ target is a numeric type. ``` Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) +───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl) Γ ⊢ Assign targets e ⇒ TVoid ``` @@ -513,7 +512,7 @@ target is a numeric type. ``` Γ(callee) = static-procedure with inputs Ts and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────── (Static-Call, impl-ish) +───────────────────────────────────────────────────────────── (Static-Call, impl) Γ ⊢ StaticCall callee args ⇒ T ``` @@ -522,7 +521,7 @@ target is a numeric type. ``` Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) +────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl) Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` @@ -531,7 +530,7 @@ target is a numeric type. ``` Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) +───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl) Γ ⊢ InstanceCall target callee args ⇒ T ``` @@ -545,7 +544,7 @@ target is a numeric type. ``` Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} -────────────────────────────────── (Op-Bool, impl-ish) +────────────────────────────────── (Op-Bool, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` @@ -553,7 +552,7 @@ target is a numeric type. ``` Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} -───────────────────────────────── (Op-Cmp, impl-ish) +───────────────────────────────── (Op-Cmp, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` @@ -561,7 +560,7 @@ target is a numeric type. ``` Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} -───────────────────────────────────────────────────────────────── (Op-Eq, impl-ish) +───────────────────────────────────────────────────────────────── (Op-Eq, impl) Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` @@ -569,7 +568,7 @@ target is a numeric type. ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} -────────────────────────────────────────────────── (Op-Arith, impl-ish) +────────────────────────────────────────────────── (Op-Arith, impl) Γ ⊢ PrimitiveOp op args ⇒ T ``` @@ -581,12 +580,10 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ``` Γ ⊢ args_i ⇐ TString op = StrConcat -───────────────────────────────────── (Op-Concat, planned) +───────────────────────────────────── (Op-Concat, impl) Γ ⊢ PrimitiveOp op args ⇒ TString ``` -Operand check not yet implemented — `StrConcat` accepts any operands today. - ### Object forms #### New-Ok From fea9f951b282d780f4a5af6cfc5bfcd738aad73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:50:34 -0400 Subject: [PATCH 021/128] remove old helpers + mis-subtyping diagnostics --- Strata/Languages/Laurel/Resolution.lean | 112 +++++++++--------------- 1 file changed, 41 insertions(+), 71 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 97f6556331..1259185178 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -37,10 +37,10 @@ Walks the AST under `ResolveM`, a state monad over `ResolveState`. Phase 1: declared type to build a qualified lookup key), - opens fresh nested scopes via `withScope` for blocks, quantifiers, procedure bodies, and constrained-type constraint/witness expressions, -- synthesizes a `HighType` for every `StmtExpr` and runs the type-checking - helpers (`checkBool`, `checkNumeric`, `checkAssignable`, `checkComparable`) - on assignments, call arguments, condition positions, functional bodies, and - constant initializers. +- synthesizes a `HighType` for every `StmtExpr` and checks it (via + `checkStmtExpr` for fresh subexpressions, or `checkSubtype` when a type is + already in hand) on assignments, call arguments, condition positions, + functional bodies, and constant initializers. Before any bodies are walked, `preRegisterTopLevel` registers every top-level name (types and their constructors / testers / destructors / instance @@ -436,54 +436,21 @@ private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := | .TCore _, _ | _, .TCore _ => true | _, _ => isSubtype sub sup -/-- Check that a type is boolean, emitting a diagnostic if not. -/ -private def checkBool (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do +/-- Type-level subtype check: emits the standard "expected/got" diagnostic when + `actual` is not a consistent subtype of `expected`. Used at sites where the + actual type is already in hand (assignment, call args, body vs declared + output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ +private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do + unless isConsistentSubtype actual expected do + typeMismatch source (s!"'{formatType expected}'") actual + +/-- Test whether a type is in the set of numeric primitives, modulo gradual + consistency. Used by Op-Cmp / Op-Arith. -/ +private def isConsistentNumeric (ty : HighTypeMd) : Bool := match ty.val with - | .TBool | .Unknown => pure () - | .UserDefined _ => pure () -- constrained types may wrap bool - | _ => typeMismatch source "bool" ty - -/-- Check that a type is numeric (int, real, or float64), emitting a diagnostic if not. -/ -private def checkNumeric (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do - match ty.val with - | .TInt | .TReal | .TFloat64 | .Unknown => pure () - | .UserDefined _ => pure () -- constrained types may wrap numeric types - | _ => typeMismatch source "a numeric type" ty - -/-- Check that two types are compatible, emitting a diagnostic if not. - UserDefined types are always considered compatible with each other since - subtype relationships (inheritance) are not tracked during resolution. - TCore types are not checked since they are pass-through types from the Core language. -/ -private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do - match expected.val, actual.val with - | .Unknown, _ => pure () - | _, .Unknown => pure () - | .UserDefined _, _ => pure () -- subtype relationships not tracked here - | _, .UserDefined _ => pure () -- subtype relationships not tracked here - | .TCore _, _ => pure () -- pass-through Core types not checked during resolution - | _, .TCore _ => pure () -- pass-through Core types not checked during resolution - | _, _ => - if !highEq expected actual then - let expectedStr := formatType expected - let actualStr := formatType actual - let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" - modify fun s => { s with errors := s.errors.push diag } - -/-- Check that two types are comparable (for == and !=), emitting a symmetric diagnostic if not. -/ -private def checkComparable (source : Option FileRange) (lhsTy : HighTypeMd) (rhsTy : HighTypeMd) : ResolveM Unit := do - match lhsTy.val, rhsTy.val with - | .Unknown, _ => pure () - | _, .Unknown => pure () - | .UserDefined _, _ => pure () - | _, .UserDefined _ => pure () - | .TCore _, _ => pure () - | _, .TCore _ => pure () - | _, _ => - if !highEq lhsTy rhsTy then - let lhsStr := formatType lhsTy - let rhsStr := formatType rhsTy - let diag := diagnosticFromSource source s!"Operands of '==' have incompatible types '{lhsStr}' and '{rhsStr}'" - modify fun s => { s with errors := s.errors.push diag } + | .TInt | .TReal | .TFloat64 | .Unknown => true + | .TCore _ => true + | _ => false /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do @@ -547,10 +514,9 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | none => { val := .TVoid, source := source } pure (.Block stmts' label, lastTy) | .While cond invs dec body => - let (cond', condTy) ← synthStmtExpr cond - checkBool cond'.source condTy + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } let invs' ← invs.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') + checkStmtExpr a.val { val := .TBool, source := a.val.source }) let dec' ← dec.attach.mapM (fun a => have := a.property; do let (e', _) ← synthStmtExpr a.val; pure e') let (body', _) ← synthStmtExpr body @@ -614,7 +580,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | some (_, node) => pure node.getType | none => pure { val := HighType.Unknown, source := fieldName.source : HighTypeMd } let tTy ← targetTy - checkAssignable source tTy valueTy + checkSubtype source tTy valueTy pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => let (target', _) ← synthStmtExpr target @@ -633,9 +599,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee - -- Check argument types match parameter types for (argTy, paramTy) in argTypes.zip paramTypes do - checkAssignable source paramTy argTy + checkSubtype source paramTy argTy pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => let results ← args.mapM synthStmtExpr @@ -649,18 +614,25 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | some headTy => headTy.val | none => HighType.TInt | .StrConcat => HighType.TString - -- Type check operands match op with | .And | .Or | .AndThen | .OrElse | .Not | .Implies => - for aTy in argTypes do checkBool source aTy + for aTy in argTypes do + checkSubtype source { val := .TBool, source := aTy.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => - for aTy in argTypes do checkNumeric source aTy + for aTy in argTypes do + unless isConsistentNumeric aTy do typeMismatch aTy.source "a numeric type" aTy | .Eq | .Neq => - -- Check that operands are compatible with each other (symmetric check) + -- Symmetric: pass if either direction is consistent. match argTypes with - | [lhsTy, rhsTy] => checkComparable source lhsTy rhsTy + | [lhsTy, rhsTy] => + unless isConsistentSubtype lhsTy rhsTy || isConsistentSubtype rhsTy lhsTy do + let diag := diagnosticFromSource source + s!"Operands of '==' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + modify fun s => { s with errors := s.errors.push diag } | _ => pure () - | .StrConcat => pure () + | .StrConcat => + for aTy in argTypes do + checkSubtype source { val := .TString, source := aTy.source } aTy pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source @@ -695,10 +667,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee - -- Check argument types match parameter types (skip first param which is 'self') + -- Skip first param (self) when matching args. let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] for (argTy, paramTy) in argTypes.zip callParamTypes do - checkAssignable source paramTy argTy + checkSubtype source paramTy argTy pure (.InstanceCall target' callee' args', retTy) | .Quantifier mode param trigger body => withScope do @@ -718,12 +690,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (val', _) ← synthStmtExpr val pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => - let (cond', condTy) ← synthStmtExpr condExpr - checkBool cond'.source condTy + let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) | .Assume cond => - let (cond', condTy) ← synthStmtExpr cond - checkBool cond'.source condTy + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } pure (.Assume cond', { val := .TVoid, source := source }) | .ProveBy val proof => let (val', valTy) ← synthStmtExpr val @@ -829,7 +799,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do | [singleOutput] => -- Only check when body produces a value (not void from return/while/assign) if bodyTy.val != HighType.TVoid then - checkAssignable proc.name.source singleOutput.type bodyTy + checkSubtype proc.name.source singleOutput.type bodyTy | _ => pure () let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', @@ -869,7 +839,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv match proc.outputs with | [singleOutput] => if bodyTy.val != HighType.TVoid then - checkAssignable proc.name.source singleOutput.type bodyTy + checkSubtype proc.name.source singleOutput.type bodyTy | _ => pure () let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr modify fun s => { s with instanceTypeName := savedInstType } From ce11f125c62c5820b48a70165903a4156a0531b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:56:38 -0400 Subject: [PATCH 022/128] quantifier check for bool body --- Strata/Languages/Laurel/Resolution.lean | 2 +- docs/verso/LaurelDoc.lean | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 1259185178..01676402aa 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -678,7 +678,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do let (e', _) ← synthStmtExpr pv.val; pure e') - let (body', _) ← synthStmtExpr body + let body' ← checkStmtExpr body { val := .TBool, source := body.source } pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) | .Assigned name => let (name', _) ← synthStmtExpr name diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4abf2eff2c..34217be4ab 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -641,12 +641,14 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. #### Quantifier ``` - Γ, x : T ⊢ body ⇒ _ + Γ, x : T ⊢ body ⇐ TBool ───────────────────────────────────────────────── (Quantifier, impl) Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool ``` -The bound variable `x : T` is introduced in scope only for the body (and trigger). +The bound variable `x : T` is introduced in scope only for the body (and trigger). The body +is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a +proposition; without this, `forall x: int :: x + 1` would be silently accepted. #### Assigned From 2207acc29c55802e387d9ce5f71770bbb1385f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 15:03:02 -0400 Subject: [PATCH 023/128] remove redundant headings --- docs/verso/LaurelDoc.lean | 96 --------------------------------------- 1 file changed, 96 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 34217be4ab..6bb9271cbf 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -267,8 +267,6 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x ### Subsumption -#### Sub - ``` Γ ⊢ e ⇒ A A <: B ───────────────────── (Sub, impl) @@ -279,29 +277,21 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### Literals -#### Lit-Int - ``` ────────────────────────── (Lit-Int, impl) Γ ⊢ LiteralInt n ⇒ TInt ``` -#### Lit-Bool - ``` ─────────────────────────── (Lit-Bool, impl) Γ ⊢ LiteralBool b ⇒ TBool ``` -#### Lit-String - ``` ───────────────────────────────── (Lit-String, impl) Γ ⊢ LiteralString s ⇒ TString ``` -#### Lit-Decimal - ``` ────────────────────────────────── (Lit-Decimal, impl) Γ ⊢ LiteralDecimal d ⇒ TReal @@ -309,16 +299,12 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### Variables -#### Var-Local - ``` Γ(x) = T ─────────────────────────── (Var-Local, impl) Γ ⊢ Var (.Local x) ⇒ T ``` -#### Var-Field - ``` Γ ⊢ e ⇒ _ Γ(f) = T_f ────────────────────────────── (Var-Field, impl) @@ -328,8 +314,6 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. Resolution looks `f` up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -#### Var-Declare - ``` x ∉ dom(Γ) ───────────────────────────────────────── (Var-Declare, impl) @@ -341,8 +325,6 @@ remainder of the enclosing scope. ### Control flow -#### If-NoElse - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T ───────────────────────────────────────────── (If-NoElse, impl) @@ -353,8 +335,6 @@ The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because the value when `cond` is false; without this, `x : int := if c then 5` would type-check spuriously. -#### If-Synth - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e ────────────────────────────────────────────────────────────── (If-Synth, impl) @@ -366,16 +346,12 @@ statement-position `if` often pairs a value branch with a `return`/`exit`/`asser enclosing context's check (Sub, or a containing `checkSubtype` like an assignment) provides the actual check downstream. -#### If-Check - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T ────────────────────────────────────────────────────────── (If-Check, planned) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T ``` -#### Block-Synth - ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T ─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) @@ -391,15 +367,11 @@ Non-last statements are synthesized but their types discarded (the lax rule). Th Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` is silently accepted; flagging it belongs to a lint. -#### Block-Synth-Empty - ``` ───────────────────────────── (Block-Synth-Empty, impl) Γ ⊢ Block [] label ⇒ TVoid ``` -#### Block-Check - ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T ─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) @@ -413,30 +385,22 @@ nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. -#### Block-Check-Empty - ``` TVoid <: T ───────────────────────── (Block-Check-Empty, impl) Γ ⊢ Block [] label ⇐ T ``` -#### Exit - ``` ──────────────────────── (Exit, impl) Γ ⊢ Exit target ⇒ TVoid ``` -#### Return-None - ``` ───────────────────────────── (Return-None, impl) Γ ⊢ Return none ⇒ TVoid ``` -#### Return-Some - ``` Γ ⊢ e ⇒ _ ────────────────────────────── (Return-Some, impl) @@ -447,8 +411,6 @@ The value's synthesized type is currently discarded, so `return 0` in a `bool`-r procedure isn't caught. Replaced by Return-Some-Checked once the expected return type is threaded through {name Strata.Laurel.ResolveState}`ResolveState`. -#### Return-Some-Checked - ``` Γ_proc.outputs = [T] Γ ⊢ e ⇐ T ────────────────────────────────────── (Return-Some-Checked, planned) @@ -458,8 +420,6 @@ threaded through {name Strata.Laurel.ResolveState}`ResolveState`. Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / {name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`. -#### While - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ ─────────────────────────────────────────────────────────────────────────────── (While, impl) @@ -471,16 +431,12 @@ target is a numeric type. ### Verification statements -#### Assert - ``` Γ ⊢ cond ⇐ TBool ────────────────────────────── (Assert, impl) Γ ⊢ Assert cond ⇒ TVoid ``` -#### Assume - ``` Γ ⊢ cond ⇐ TBool ───────────────────────────── (Assume, impl) @@ -489,16 +445,12 @@ target is a numeric type. ### Assignment -#### Assign-Single - ``` Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x ─────────────────────────────────────────────── (Assign-Single, impl) Γ ⊢ Assign [x] e ⇒ TVoid ``` -#### Assign-Multi - ``` Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i ───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl) @@ -507,8 +459,6 @@ target is a numeric type. ### Calls -#### Static-Call - ``` Γ(callee) = static-procedure with inputs Ts and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) @@ -516,8 +466,6 @@ target is a numeric type. Γ ⊢ StaticCall callee args ⇒ T ``` -#### Static-Call-Multi - ``` Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) @@ -525,8 +473,6 @@ target is a numeric type. Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` -#### Instance-Call - ``` Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) @@ -540,32 +486,24 @@ target is a numeric type. {name Strata.Laurel.HighType.TReal}`TReal`, {name Strata.Laurel.HighType.TFloat64}`TFloat64`". -#### Op-Bool - ``` Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} ────────────────────────────────── (Op-Bool, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -#### Op-Cmp - ``` Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} ───────────────────────────────── (Op-Cmp, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -#### Op-Eq - ``` Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} ───────────────────────────────────────────────────────────────── (Op-Eq, impl) Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` -#### Op-Arith - ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} ────────────────────────────────────────────────── (Op-Arith, impl) @@ -576,8 +514,6 @@ target is a numeric type. etc. without unification. Known relaxation: `int + real` passes (each operand individually passes `Numeric`); a proper fix needs numeric promotion or unification. -#### Op-Concat - ``` Γ ⊢ args_i ⇐ TString op = StrConcat ───────────────────────────────────── (Op-Concat, impl) @@ -586,24 +522,18 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ### Object forms -#### New-Ok - ``` Γ(ref) is a composite or datatype T ────────────────────────────────────────── (New-Ok, impl) Γ ⊢ New ref ⇒ UserDefined T ``` -#### New-Fallback - ``` Γ(ref) is not a composite or datatype ───────────────────────────────────────── (New-Fallback, impl) Γ ⊢ New ref ⇒ Unknown ``` -#### AsType - ``` Γ ⊢ target ⇒ _ ───────────────────────────── (AsType, impl) @@ -612,24 +542,18 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. `target` is resolved but not checked against `T` — the cast is the user's claim. -#### IsType - ``` Γ ⊢ target ⇒ _ ───────────────────────────────── (IsType, impl) Γ ⊢ IsType target T ⇒ TBool ``` -#### RefEq - ``` Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ ─────────────────────────────────────── (RefEq, impl) Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` -#### PureFieldUpdate - ``` Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ ─────────────────────────────────────────────── (PureFieldUpdate, impl) @@ -638,8 +562,6 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ### Verification expressions -#### Quantifier - ``` Γ, x : T ⊢ body ⇐ TBool ───────────────────────────────────────────────── (Quantifier, impl) @@ -650,32 +572,24 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a proposition; without this, `forall x: int :: x + 1` would be silently accepted. -#### Assigned - ``` Γ ⊢ name ⇒ _ ───────────────────────────── (Assigned, impl) Γ ⊢ Assigned name ⇒ TBool ``` -#### Old - ``` Γ ⊢ v ⇒ T ───────────────── (Old, impl) Γ ⊢ Old v ⇒ T ``` -#### Fresh - ``` Γ ⊢ v ⇒ _ ───────────────────── (Fresh, impl) Γ ⊢ Fresh v ⇒ TBool ``` -#### ProveBy - ``` Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ ─────────────────────────────────── (ProveBy, impl) @@ -684,15 +598,11 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. ### Untyped forms -#### This - ``` ────────────────────────── (This, impl) Γ ⊢ This ⇒ Unknown ``` -#### Abstract / All / ContractOf - ``` ───────────────────────────────────────────── (Abstract / All / ContractOf, impl) Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown @@ -700,22 +610,16 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. ### Holes -#### Hole-Some - ``` ──────────────────────────── (Hole-Some, impl) Γ ⊢ Hole d (some T) ⇒ T ``` -#### Hole-None-Synth - ``` ───────────────────────────────── (Hole-None-Synth, impl) Γ ⊢ Hole d none ⇒ Unknown ``` -#### Hole-None-Check - ``` Unknown <: T ───────────────────────── (Hole-None-Check, planned) From 4cbab19c4b809853cfd45484462c6f2660890bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 15:09:05 -0400 Subject: [PATCH 024/128] class-rules : updates on same-typed fields + this in class context only --- Strata/Languages/Laurel/Resolution.lean | 17 +++++++++++-- docs/verso/LaurelDoc.lean | 32 +++++++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 01676402aa..bd139a67d1 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -590,7 +590,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .PureFieldUpdate target fieldName newVal => let (target', targetTy) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let (newVal', _) ← synthStmtExpr newVal + let fieldTy ← getVarType fieldName' + let newVal' ← checkStmtExpr newVal fieldTy pure (.PureFieldUpdate target' fieldName' newVal', targetTy) | .StaticCall callee args => let callee' ← resolveRef callee source @@ -646,7 +647,19 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let ty := if kindOk then { val := HighType.UserDefined ref', source := source } else { val := HighType.Unknown, source := source } pure (.New ref', ty) - | .This => pure (.This, { val := .Unknown, source := source }) + | .This => + let s ← get + match s.instanceTypeName with + | some typeName => + let typeId : Identifier := + match s.scope.get? typeName with + | some (uid, _) => { text := typeName, uniqueId := some uid, source := source } + | none => { text := typeName, source := source } + pure (.This, { val := .UserDefined typeId, source := source }) + | none => + let diag := diagnosticFromSource source "'this' is not allowed outside instance methods" + modify fun s => { s with errors := s.errors.push diag } + pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => let (lhs', _) ← synthStmtExpr lhs let (rhs', _) ← synthStmtExpr rhs diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 6bb9271cbf..8506285865 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -262,7 +262,8 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate - *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy -- *Untyped forms* — This; Abstract / All / ContractOf +- *Self reference* — This-Inside, This-Outside +- *Untyped forms* — Abstract / All / ContractOf - *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) ### Subsumption @@ -555,11 +556,14 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ``` ``` - Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ -─────────────────────────────────────────────── (PureFieldUpdate, impl) - Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t + Γ ⊢ target ⇒ T_t Γ(f) = T_f Γ ⊢ newVal ⇐ T_f +───────────────────────────────────────────────────────────── (PureFieldUpdate, impl) + Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t ``` +`f` is resolved against `T_t` (or the enclosing instance type) and `newVal` is checked +against the field's declared type. + ### Verification expressions ``` @@ -596,13 +600,27 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. Γ ⊢ ProveBy v proof ⇒ T ``` -### Untyped forms +### Self reference ``` -────────────────────────── (This, impl) - Γ ⊢ This ⇒ Unknown + Γ.instanceTypeName = some T +────────────────────────────────── (This-Inside, impl) + Γ ⊢ This ⇒ UserDefined T + + + Γ.instanceTypeName = none +────────────────────────────── (This-Outside, impl) + Γ ⊢ This ⇒ Unknown [emits "'this' is not allowed outside instance methods"] ``` +`Γ.instanceTypeName` is the +{name Strata.Laurel.ResolveState}`ResolveState` field set by +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of +an instance method body. With it, `this.field` and instance-method dispatch synthesize real +types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}`Unknown`. + +### Untyped forms + ``` ───────────────────────────────────────────── (Abstract / All / ContractOf, impl) Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown From 0a38a7cf2b159802ba89bbb911edd37fdbb69d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 15:37:40 -0400 Subject: [PATCH 025/128] references checks --- Strata/Languages/Laurel/Resolution.lean | 21 ++++++++++++++++++--- docs/verso/LaurelDoc.lean | 23 +++++++++++++++++------ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index bd139a67d1..3df24ce05e 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -452,6 +452,15 @@ private def isConsistentNumeric (ty : HighTypeMd) : Bool := | .TCore _ => true | _ => false +/-- Test whether a type is a user-defined reference type, modulo gradual + consistency. Used by Fresh and ReferenceEquals, which only make sense on + composite/datatype references. -/ +private def isConsistentReference (ty : HighTypeMd) : Bool := + match ty.val with + | .UserDefined _ | .Unknown => true + | .TCore _ => true + | _ => false + /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -661,8 +670,12 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := modify fun s => { s with errors := s.errors.push diag } pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => - let (lhs', _) ← synthStmtExpr lhs - let (rhs', _) ← synthStmtExpr rhs + let (lhs', lhsTy) ← synthStmtExpr lhs + let (rhs', rhsTy) ← synthStmtExpr rhs + unless isConsistentReference lhsTy do + typeMismatch lhsTy.source "a reference type" lhsTy + unless isConsistentReference rhsTy do + typeMismatch rhsTy.source "a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => let (target', _) ← synthStmtExpr target @@ -700,7 +713,9 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (val', valTy) ← synthStmtExpr val pure (.Old val', valTy) | .Fresh val => - let (val', _) ← synthStmtExpr val + let (val', valTy) ← synthStmtExpr val + unless isConsistentReference valTy do + typeMismatch valTy.source "a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 8506285865..2c7f9542d2 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -550,11 +550,18 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ``` ``` - Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ -─────────────────────────────────────── (RefEq, impl) - Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool + Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r isReference T_l isReference T_r +───────────────────────────────────────────────────────────────────────────── (RefEq, impl) + Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` +`isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined`, +{name Strata.Laurel.HighType.Unknown}`Unknown`, or {name Strata.Laurel.HighType.TCore}`TCore` +type. Reference equality is meaningless on primitives. Compatibility between `T_l` and +`T_r` (e.g. rejecting `Cat === Dog` for unrelated user-defined types) is delegated to +future tightening of `<:` — today, two distinct user-defined names already mismatch +structurally, so the check would only fire under stronger subtyping. + ``` Γ ⊢ target ⇒ T_t Γ(f) = T_f Γ ⊢ newVal ⇐ T_f ───────────────────────────────────────────────────────────── (PureFieldUpdate, impl) @@ -589,11 +596,15 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. ``` ``` - Γ ⊢ v ⇒ _ -───────────────────── (Fresh, impl) - Γ ⊢ Fresh v ⇒ TBool + Γ ⊢ v ⇒ T isReference T +───────────────────────────────── (Fresh, impl) + Γ ⊢ Fresh v ⇒ TBool ``` +`isReference T` is the same predicate as in {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals`. +{name Strata.Laurel.StmtExpr.Fresh}`Fresh` only makes sense on heap-allocated references; +`fresh(5)` is rejected. + ``` Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ ─────────────────────────────────── (ProveBy, impl) From 3efef2fad0357bd5514ca6c8694397612eba8254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 16:21:06 -0400 Subject: [PATCH 026/128] pretty printers --- Strata/Languages/Laurel/Laurel.lean | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index a5cd11439c..2fd34ce3b0 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -100,6 +100,20 @@ inductive Operation : Type where | StrConcat deriving Repr +instance : ToString Operation where + toString + | .Eq => "==" | .Neq => "!=" + | .And => "&&" | .Or => "||" + | .Not => "!" | .Implies => "==>" + | .AndThen => "&&!" | .OrElse => "||!" + | .Neg => "-" | .Add => "+" + | .Sub => "-" | .Mul => "*" + | .Div => "/" | .Mod => "%" + | .DivT => "/t" | .ModT => "%t" + | .Lt => "<" | .Leq => "<=" + | .Gt => ">" | .Geq => ">=" + | .StrConcat => "++" + /-- A wrapper that pairs a value with source-level metadata such as source locations and annotations. All Laurel AST nodes are wrapped in @@ -334,6 +348,40 @@ inductive ContractType where | Reads | Modifies | Precondition | PostCondition end +/-- A short user-facing name for the construct, used in diagnostic messages. -/ +def StmtExpr.constrName : StmtExpr → String + | .IfThenElse .. => "if" + | .Block .. => "block" + | .While .. => "while" + | .Exit .. => "exit" + | .Return .. => "return" + | .LiteralInt .. => "integer literal" + | .LiteralBool .. => "boolean literal" + | .LiteralString .. => "string literal" + | .LiteralDecimal .. => "decimal literal" + | .Var .. => "variable" + | .Assign .. => ":=" + | .PureFieldUpdate .. => "field update" + | .StaticCall .. => "call" + | .PrimitiveOp op _ => toString op + | .New .. => "new" + | .This => "this" + | .ReferenceEquals .. => "reference equality" + | .AsType .. => "as" + | .IsType .. => "is" + | .InstanceCall .. => "method call" + | .Quantifier .. => "quantifier" + | .Assigned .. => "assigned" + | .Old .. => "old" + | .Fresh .. => "fresh" + | .Assert .. => "assert" + | .Assume .. => "assume" + | .ProveBy .. => "by" + | .ContractOf .. => "contractOf" + | .Abstract => "abstract" + | .All => "all" + | .Hole .. => "hole" + @[expose] abbrev HighTypeMd := AstNode HighType @[expose] abbrev StmtExprMd := AstNode StmtExpr @[expose] abbrev VariableMd := AstNode Variable From 59ce64d229796657ff883861b3a739a312a3f9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 16:38:07 -0400 Subject: [PATCH 027/128] better type mismatch diagnostics --- Strata/Languages/Laurel/Resolution.lean | 74 ++++++++++++++----------- docs/verso/LaurelDoc.lean | 36 +++++++----- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 3df24ce05e..26783e2de9 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -417,24 +417,35 @@ private def formatType (ty : HighTypeMd) : String := "(" ++ ", ".intercalate parts ++ ")" | other => toString (formatHighTypeVal other) -/-- Emit a type mismatch diagnostic. -/ -private def typeMismatch (source : Option FileRange) (expected : String) (actual : HighTypeMd) : ResolveM Unit := do - let actualStr := formatType actual - let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" +/-- Emit a type mismatch diagnostic. With a `construct`, the message is + "'' , got ''"; without, + ", got ''". -/ +private def typeMismatch (source : Option FileRange) (construct : Option StmtExpr) + (problem : String) (actual : HighTypeMd) : ResolveM Unit := do + let constructor := match construct with + | some c => s!"'{c.constrName}' " + | none => "" + let diag := diagnosticFromSource source s!"{constructor}{problem}, got '{formatType actual}'" modify fun s => { s with errors := s.errors.push diag } /-- Subtyping. Stub: structural equality via `highEq`. TODO: To be replaced with a real check that walks `extending` chains for composites, unfolds aliases, and unwraps constrained types to their base. -/ private def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup -/-- Gradual consistency-subtyping (Siek–Taha style): `Unknown` is the dynamic - type and is consistent with everything in either direction. `TCore` is a - migration escape hatch and is bivariantly compatible for now. -/ -private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := - match sub.val, sup.val with +/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the + dynamic type and is consistent with everything; otherwise the relation + delegates to structural equality. `TCore` is a temporary migration + escape hatch. -/ +private def isConsistent (a b : HighTypeMd) : Bool := + match a.val, b.val with | .Unknown, _ | _, .Unknown => true | .TCore _, _ | _, .TCore _ => true - | _, _ => isSubtype sub sup + | _, _ => highEq a b + +/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For the flat type + lattice this collapses to `sub ~ sup ∨ sub <: sup`. -/ +private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := + isConsistent sub sup || isSubtype sub sup /-- Type-level subtype check: emits the standard "expected/got" diagnostic when `actual` is not a consistent subtype of `expected`. Used at sites where the @@ -442,20 +453,20 @@ private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do unless isConsistentSubtype actual expected do - typeMismatch source (s!"'{formatType expected}'") actual + typeMismatch source none s!"expected '{formatType expected}'" actual -/-- Test whether a type is in the set of numeric primitives, modulo gradual - consistency. Used by Op-Cmp / Op-Arith. -/ -private def isConsistentNumeric (ty : HighTypeMd) : Bool := +/-- Test whether a type is in the set of numeric primitives. `Unknown` and + `TCore` are accepted as gradual escape hatches. Used by Op-Cmp / Op-Arith. -/ +private def isNumeric (ty : HighTypeMd) : Bool := match ty.val with | .TInt | .TReal | .TFloat64 | .Unknown => true | .TCore _ => true | _ => false -/-- Test whether a type is a user-defined reference type, modulo gradual - consistency. Used by Fresh and ReferenceEquals, which only make sense on - composite/datatype references. -/ -private def isConsistentReference (ty : HighTypeMd) : Bool := +/-- Test whether a type is a user-defined reference type. `Unknown` and `TCore` + are accepted as gradual escape hatches. Used by Fresh and ReferenceEquals, + which only make sense on composite/datatype references. -/ +private def isReference (ty : HighTypeMd) : Bool := match ty.val with | .UserDefined _ | .Unknown => true | .TCore _ => true @@ -630,14 +641,14 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := checkSubtype source { val := .TBool, source := aTy.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => for aTy in argTypes do - unless isConsistentNumeric aTy do typeMismatch aTy.source "a numeric type" aTy + unless isNumeric aTy do + typeMismatch aTy.source (some expr) "expected a numeric type" aTy | .Eq | .Neq => - -- Symmetric: pass if either direction is consistent. match argTypes with | [lhsTy, rhsTy] => - unless isConsistentSubtype lhsTy rhsTy || isConsistentSubtype rhsTy lhsTy do + unless isConsistent lhsTy rhsTy do let diag := diagnosticFromSource source - s!"Operands of '==' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" modify fun s => { s with errors := s.errors.push diag } | _ => pure () | .StrConcat => @@ -672,10 +683,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .ReferenceEquals lhs rhs => let (lhs', lhsTy) ← synthStmtExpr lhs let (rhs', rhsTy) ← synthStmtExpr rhs - unless isConsistentReference lhsTy do - typeMismatch lhsTy.source "a reference type" lhsTy - unless isConsistentReference rhsTy do - typeMismatch rhsTy.source "a reference type" rhsTy + unless isReference lhsTy do + typeMismatch lhsTy.source (some expr) "expected a reference type" lhsTy + unless isReference rhsTy do + typeMismatch rhsTy.source (some expr) "expected a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => let (target', _) ← synthStmtExpr target @@ -714,8 +725,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.Old val', valTy) | .Fresh val => let (val', valTy) ← synthStmtExpr val - unless isConsistentReference valTy do - typeMismatch valTy.source "a reference type" valTy + unless isReference valTy do + typeMismatch valTy.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } @@ -761,9 +772,7 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE let (s', _) ← synthStmtExpr s; pure s') match _lastResult: stmts.getLast? with | none => - let tvoid : HighTypeMd := { val := .TVoid, source := source } - unless isConsistentSubtype tvoid expected do - typeMismatch source (formatType expected) tvoid + checkSubtype source expected { val := .TVoid, source := source } pure { val := .Block init' label, source := source } | some last => have := List.mem_of_getLast? _lastResult @@ -772,8 +781,7 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd - unless isConsistentSubtype actual expected do - typeMismatch source (formatType expected) actual + checkSubtype source expected actual pure e' termination_by (exprMd, 1) decreasing_by all_goals first diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 2c7f9542d2..4ff9cd7f0f 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -201,27 +201,30 @@ internal interface used by other rules. ### Gradual typing -The relation `<:` is implemented by two Lean functions — both currently stubs, both -intended to be sharpened: +The relation `<:` (used in Sub) is built from three Lean functions: - `isSubtype` — pure subtyping. The stub is structural equality via {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. -- `isConsistentSubtype` — gradual consistency, in the Siek–Taha sense. - {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type `?` and is consistent - with everything in either direction; otherwise the relation delegates to `isSubtype`. - {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now, as a - clearly-labelled migration escape hatch from the Core language — this carve-out is - intentionally temporary. - -Subsumption (and every bespoke check rule) uses `isConsistentSubtype`, never raw -`isSubtype`. That single choice is what makes the system *gradual*: an expression of type +- `isConsistent` — the symmetric gradual relation `~` (Siek–Taha): + {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type and is consistent with + everything; otherwise structural equality. +- `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this + is the standard collapse of `∃R. T ~ R ∧ R <: U`. + +{name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now as a temporary +migration escape hatch from the Core language; the carve-out lives in `isConsistent` and is +intentionally temporary. + +Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what +makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely into any typed slot, and any expression flows freely into a slot of type {name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between -fully-known types only. +fully-known types only. The symmetric `isConsistent` is used directly by Op-Eq, where the +operand types must be mutually consistent (no subtype direction is privileged). A previous iteration was synth-only with three *bivariantly-compatible* wildcards: {name Strata.Laurel.HighType.Unknown}`Unknown`, @@ -500,11 +503,14 @@ target is a numeric type. ``` ``` - Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} -───────────────────────────────────────────────────────────────── (Op-Eq, impl) - Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool + Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l ~ T_r op ∈ {Eq, Neq} +───────────────────────────────────────────────────────── (Op-Eq, impl) + Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` +`~` is the consistency relation `isConsistent` — symmetric, with the +{name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. + ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} ────────────────────────────────────────────────── (Op-Arith, impl) From 538a68779c01857887c494545f1bc48eccb979ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 16:41:21 -0400 Subject: [PATCH 028/128] check ifthenelse --- Strata/Languages/Laurel/Resolution.lean | 10 ++++++++++ docs/verso/LaurelDoc.lean | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 26783e2de9..03fc98e54e 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -778,6 +778,16 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE have := List.mem_of_getLast? _lastResult let last' ← checkStmtExpr last expected pure { val := .Block (init' ++ [last']) label, source := source } + | .IfThenElse cond thenBr elseBr => + -- Push `expected` into both branches (rather than going through the synth + -- rule + Sub at the boundary). Without an else branch, fall back to + -- subsumption of TVoid against `expected`. + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let thenBr' ← checkStmtExpr thenBr expected + let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) + if elseBr.isNone then + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .IfThenElse cond' thenBr' elseBr', source := source } | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4ff9cd7f0f..20550333f8 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -256,9 +256,9 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Subsumption* — Sub - *Literals* — Lit-Int, Lit-Bool, Lit-String, Lit-Decimal - *Variables* — Var-Local, Var-Field, Var-Declare -- *Control flow* — If-NoElse, If-Synth, If-Check (planned); Block-Synth, Block-Synth-Empty, - Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, Return-Some-Checked - (planned); While +- *Control flow* — If-NoElse, If-Synth, If-Check, If-Check-NoElse; Block-Synth, + Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, + Return-Some-Checked (planned); While - *Verification statements* — Assert, Assume - *Assignment* — Assign-Single, Assign-Multi - *Calls* — Static-Call, Static-Call-Multi, Instance-Call @@ -352,10 +352,21 @@ the actual check downstream. ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T -────────────────────────────────────────────────────────── (If-Check, planned) +────────────────────────────────────────────────────────── (If-Check, impl) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T + + +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T TVoid <: T +───────────────────────────────────────────────────── (If-Check-NoElse, impl) + Γ ⊢ IfThenElse cond thenBr none ⇐ T ``` +Check mode pushes `T` into both branches (rather than going through If-Synth + Sub at the +boundary). Errors fire at the offending branch instead of the surrounding `if`. Without an +else branch, the construct can only succeed when `T` admits +{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `Block-Check-Empty` +performs for an empty block. + ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T ─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) From a0862ee46a7a0de74cc4b098e5fc4f584b49fc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:01:12 -0400 Subject: [PATCH 029/128] type check returns multiple return arity : 0 = Void 1 = return expr // return; with returns (res:T) signature n = return; allowed only --- Strata/Languages/Laurel/Resolution.lean | 33 ++++++++++++++++++- docs/verso/LaurelDoc.lean | 43 ++++++++++++++++++------- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 03fc98e54e..e781c9b65d 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -262,6 +262,10 @@ structure ResolveState where /-- When resolving inside an instance procedure, the owning composite type name. Used by `resolveFieldRef` to resolve `self.field` when `self` has type `Any`. -/ instanceTypeName : Option String := none + /-- When resolving inside a procedure body, the declared output types (in + declaration order). `none` means no enclosing procedure. Used by `Return` + to type-check the optional return value and to flag arity/shape mismatches. -/ + expectedReturnTypes : Option (List HighTypeMd) := none @[expose] abbrev ResolveM := StateM ResolveState @@ -543,8 +547,29 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) | .Return val => do + -- Match the optional return value against the enclosing procedure's + -- declared outputs. `expectedReturnTypes = none` means we're not inside a + -- procedure body (e.g. resolving a constant initializer); skip the check. + let expected := (← get).expectedReturnTypes let val' ← val.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') + match expected with + | some [singleOutput] => checkStmtExpr a.val singleOutput + | _ => let (e', _) ← synthStmtExpr a.val; pure e') + -- Arity/shape diagnostics independent of the value's own type. + match val, expected with + | none, some [] => pure () + | none, some [_] => pure () -- Dafny-style early exit + | none, some _ => pure () -- multi-output: bare return is fine + | some _, some [] => + let diag := diagnosticFromSource source + "void procedure cannot return a value" + modify fun s => { s with errors := s.errors.push diag } + | some _, some [_] => pure () -- value already checked above + | some _, some _ => + let diag := diagnosticFromSource source + "multi-output procedure cannot use 'return e'; assign to named outputs instead" + modify fun s => { s with errors := s.errors.push diag } + | _, none => pure () -- no enclosing procedure pure (.Return val', { val := .TVoid, source := source }) | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) @@ -834,7 +859,10 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do let outputs' ← proc.outputs.mapM resolveParameter let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) let dec' ← proc.decreases.mapM resolveStmtExpr + let savedReturns := (← get).expectedReturnTypes + modify fun s => { s with expectedReturnTypes := some (outputs'.map (·.type)) } let (body', bodyTy) ← resolveBody proc.body + modify fun s => { s with expectedReturnTypes := savedReturns } if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" @@ -875,7 +903,10 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv let outputs' ← proc.outputs.mapM resolveParameter let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) let dec' ← proc.decreases.mapM resolveStmtExpr + let savedReturns := (← get).expectedReturnTypes + modify fun s => { s with expectedReturnTypes := some (outputs'.map (·.type)) } let (body', bodyTy) ← resolveBody proc.body + modify fun s => { s with expectedReturnTypes := savedReturns } if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 20550333f8..b7b74c5af2 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -258,7 +258,7 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Variables* — Var-Local, Var-Field, Var-Declare - *Control flow* — If-NoElse, If-Synth, If-Check, If-Check-NoElse; Block-Synth, Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, - Return-Some-Checked (planned); While + Return-Void-Error, Return-Multi-Error; While - *Verification statements* — Assert, Assume - *Assignment* — Assign-Single, Assign-Multi - *Calls* — Static-Call, Static-Call-Multi, Instance-Call @@ -411,29 +411,48 @@ nested {name Strata.Laurel.StmtExpr.Block}`Block` / Γ ⊢ Exit target ⇒ TVoid ``` +`Return` matches the optional return value against the enclosing procedure's declared +outputs. The expected output types are threaded through +{name Strata.Laurel.ResolveState}`ResolveState`'s `expectedReturnTypes`, set from +`proc.outputs` by {name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of +the body. `none` means "no enclosing procedure" — e.g. resolving a constant initializer — +and skips all `Return` checks. + ``` ───────────────────────────── (Return-None, impl) Γ ⊢ Return none ⇒ TVoid ``` +A bare `return;` is allowed in any context. In a single-output procedure it acts as a +Dafny-style early exit — the output parameter retains whatever was last assigned to it. + ``` - Γ ⊢ e ⇒ _ -────────────────────────────── (Return-Some, impl) - Γ ⊢ Return (some e) ⇒ TVoid + Γ_proc.outputs = [T] Γ ⊢ e ⇐ T +────────────────────────────────────── (Return-Some, impl) + Γ ⊢ Return (some e) ⇒ TVoid ``` -The value's synthesized type is currently discarded, so `return 0` in a `bool`-returning -procedure isn't caught. Replaced by Return-Some-Checked once the expected return type is -threaded through {name Strata.Laurel.ResolveState}`ResolveState`. +In a single-output procedure, the value is checked against the declared output type. This +closes the prior soundness gap where `return 0` in a `bool`-returning procedure went +uncaught. ``` - Γ_proc.outputs = [T] Γ ⊢ e ⇐ T -────────────────────────────────────── (Return-Some-Checked, planned) - Γ ⊢ Return (some e) ⇒ TVoid + Γ_proc.outputs = [] +───────────────────────────────── (Return-Void-Error, impl) + Γ ⊢ Return (some e) — error: "void procedure cannot return a value" + + + Γ_proc.outputs = [T_1; …; T_n] (n ≥ 2) +────────────────────────────────────────────────────────── (Return-Multi-Error, impl) + Γ ⊢ Return (some e) — error: "multi-output procedure cannot + use 'return e'; assign to named outputs instead" ``` -Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`. +Multi-output procedures use named-output assignment (`r := …` on the declared output +parameters). `return e` syntactically takes a single +{name Strata.Laurel.StmtExpr.Return}`Option StmtExpr`, so it cannot carry multiple values; +flagging it points users at the named-output convention. ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ From b58a578b5538157a7b560b262f89f5492e28ab4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:15:26 -0400 Subject: [PATCH 030/128] type check procedures contracts --- Strata/Languages/Laurel/Resolution.lean | 25 ++++++++++++- docs/verso/LaurelDoc.lean | 47 +++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index e781c9b65d..a0e441092c 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -764,8 +764,31 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (proof', _) ← synthStmtExpr proof pure (.ProveBy val' proof', valTy) | .ContractOf ty fn => + -- `fn` must be a direct identifier reference resolving to a procedure. + -- Anything else (arbitrary expressions, references to non-procedures) is + -- ill-formed: a contract belongs to a *named* procedure. let (fn', _) ← synthStmtExpr fn - pure (.ContractOf ty fn', { val := .Unknown, source := source }) + let s ← get + let fnIsProcRef : Bool := match fn'.val with + | .Var (.Local ref) => + match s.scope.get? ref.text with + | some (_, node) => + node.kind == .staticProcedure || + node.kind == .instanceProcedure || + node.kind == .unresolved + | none => true -- unresolved name already reported + | _ => false + unless fnIsProcRef do + let diag := diagnosticFromSource fn.source + "'contractOf' expected a procedure reference" + modify fun s => { s with errors := s.errors.push diag } + -- Result type: Bool for pre/postconditions, set of heap references for + -- reads/modifies. The element type of the set is left as Unknown for now + -- since the rule doesn't recover it from `fn`. + let resultTy : HighType := match ty with + | .Precondition | .PostCondition => .TBool + | .Reads | .Modifies => .TSet { val := .Unknown, source := none } + pure (.ContractOf ty fn', { val := resultTy, source := source }) | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) | .All => pure (.All, { val := .Unknown, source := source }) | .Hole det type => match type with diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index b7b74c5af2..b98a1bda98 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -266,7 +266,8 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate - *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy - *Self reference* — This-Inside, This-Outside -- *Untyped forms* — Abstract / All / ContractOf +- *Untyped forms* — Abstract / All +- *ContractOf* — ContractOf-Bool, ContractOf-Set, ContractOf-Error - *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) ### Subsumption @@ -669,10 +670,50 @@ types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}` ### Untyped forms ``` -───────────────────────────────────────────── (Abstract / All / ContractOf, impl) - Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown +───────────────────────────────── (Abstract / All, impl) + Γ ⊢ Abstract / All … ⇒ Unknown ``` +### ContractOf + +`ContractOf ty fn` extracts a procedure's contract clause as a value: its preconditions +(`Precondition`), postconditions (`PostCondition`), reads set (`Reads`), or modifies set +(`Modifies`). `fn` must be a direct identifier reference to a procedure — a contract belongs +to a *named* procedure, not an arbitrary expression. + +``` + fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} +───────────────────────────────────────────────────────────────────────── (ContractOf-Bool, impl) + Γ ⊢ ContractOf Precondition fn ⇒ TBool + Γ ⊢ ContractOf PostCondition fn ⇒ TBool + + + fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} +───────────────────────────────────────────────────────────────────────── (ContractOf-Set, impl) + Γ ⊢ ContractOf Reads fn ⇒ TSet Unknown + Γ ⊢ ContractOf Modifies fn ⇒ TSet Unknown +``` + +`Precondition` and `PostCondition` are propositions, hence +{name Strata.Laurel.HighType.TBool}`TBool`. `Reads` and `Modifies` are sets of heap-allocated +locations — composite/datatype references and fields. The element type is left as +{name Strata.Laurel.HighType.Unknown}`Unknown` for now since the rule doesn't yet recover it +from `fn`'s declared modifies/reads clauses. + +``` + fn is not a procedure reference +───────────────────────────────────────────── (ContractOf-Error, impl) + Γ ⊢ ContractOf … fn — error: "'contractOf' expected a procedure reference" +``` + +When `fn` doesn't resolve to a procedure (e.g. it's an arbitrary expression, or resolves to +a constant/variable), the diagnostic fires and the construct synthesizes +{name Strata.Laurel.HighType.Unknown}`Unknown` to suppress cascading errors. + +The constructor is reserved for future use — Laurel's grammar has no `contractOf` +production today, and the translator emits "not yet implemented" for it. The typing rule +exists so resolution remains exhaustive over `StmtExpr`. + ### Holes ``` From 1a639d14a3fa2474353b8c96909a657b9ddf1577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:28:15 -0400 Subject: [PATCH 031/128] check untyped holes --- Strata/Languages/Laurel/Resolution.lean | 5 +++++ docs/verso/LaurelDoc.lean | 17 ++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index a0e441092c..5022293b73 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -836,6 +836,11 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE if elseBr.isNone then checkSubtype source expected { val := .TVoid, source := source } pure { val := .IfThenElse cond' thenBr' elseBr', source := source } + | .Hole det none => + -- Untyped hole in check mode: record the expected type on the node so + -- downstream passes don't have to infer it again. Subsumption is trivial + -- (Unknown <: T always holds). + pure { val := .Hole det (some expected), source := source } | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index b98a1bda98..68c1fe0f62 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -268,7 +268,7 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Self reference* — This-Inside, This-Outside - *Untyped forms* — Abstract / All - *ContractOf* — ContractOf-Bool, ContractOf-Set, ContractOf-Error -- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) +- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check ### Subsumption @@ -727,14 +727,17 @@ exists so resolution remains exhaustive over `StmtExpr`. ``` ``` - Unknown <: T -───────────────────────── (Hole-None-Check, planned) - Γ ⊢ Hole d none ⇐ T +───────────────────────────────────── (Hole-None-Check, impl) + Γ ⊢ Hole d none ⇐ T ↦ Hole d (some T) ``` -In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always -holds). The planned rule would record the inferred `T` on the hole node so downstream -passes can see it, instead of leaving `none` until the hole-inference pass. +In check mode, an untyped hole records the expected type `T` on the node directly. The +subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it +just preserves the type information that's available at the check-mode boundary instead of +discarding it. A separate +{name Strata.Laurel.InferHoleTypes}`InferHoleTypes` pass still runs after resolution to +annotate holes that ended up in synth-only positions; over time, as more constructs gain +bespoke check rules, fewer holes will need that pass. # Translation Pipeline From 5717c346ef2dbc2196fc7e1ed7247b8bc320c11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:28:49 -0400 Subject: [PATCH 032/128] remove dangling reference --- docs/verso/LaurelDoc.lean | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 68c1fe0f62..bf812f7eb5 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -734,10 +734,9 @@ exists so resolution remains exhaustive over `StmtExpr`. In check mode, an untyped hole records the expected type `T` on the node directly. The subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it just preserves the type information that's available at the check-mode boundary instead of -discarding it. A separate -{name Strata.Laurel.InferHoleTypes}`InferHoleTypes` pass still runs after resolution to -annotate holes that ended up in synth-only positions; over time, as more constructs gain -bespoke check rules, fewer holes will need that pass. +discarding it. A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended +up in synth-only positions; over time, as more constructs gain bespoke check rules, fewer +holes will need that pass. # Translation Pipeline From d8b8c7ca25071ed5a023912210c33821b8382e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:44:38 -0400 Subject: [PATCH 033/128] move subtyping/consistency rules in type definition --- Strata/Languages/Laurel/Laurel.lean | 19 +++++++++++++++++++ Strata/Languages/Laurel/Resolution.lean | 19 ------------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index 2fd34ce3b0..4fc77650cc 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -488,6 +488,25 @@ instance : BEq HighTypeMd where deriving instance BEq for HighType +/-- Subtyping. Stub: structural equality via `highEq`. + TODO: walk `extending` chains for composites, unfold aliases, unwrap + constrained types to their base. -/ +def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup + +/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the + dynamic type and is consistent with everything; otherwise structural + equality. `TCore` is a temporary migration escape hatch. -/ +def isConsistent (a b : HighTypeMd) : Bool := + match a.val, b.val with + | .Unknown, _ | _, .Unknown => true + | .TCore _, _ | _, .TCore _ => true + | _, _ => highEq a b + +/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice + this collapses to `sub ~ sup ∨ sub <: sup`. -/ +def isConsistentSubtype (sub sup : HighTypeMd) : Bool := + isConsistent sub sup || isSubtype sub sup + def HighType.isBool : HighType → Bool | TBool => true | _ => false diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 5022293b73..8c88d5189b 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -432,25 +432,6 @@ private def typeMismatch (source : Option FileRange) (construct : Option StmtExp let diag := diagnosticFromSource source s!"{constructor}{problem}, got '{formatType actual}'" modify fun s => { s with errors := s.errors.push diag } -/-- Subtyping. Stub: structural equality via `highEq`. - TODO: To be replaced with a real check that walks `extending` chains for composites, unfolds aliases, and unwraps constrained types to their base. -/ -private def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup - -/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the - dynamic type and is consistent with everything; otherwise the relation - delegates to structural equality. `TCore` is a temporary migration - escape hatch. -/ -private def isConsistent (a b : HighTypeMd) : Bool := - match a.val, b.val with - | .Unknown, _ | _, .Unknown => true - | .TCore _, _ | _, .TCore _ => true - | _, _ => highEq a b - -/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For the flat type - lattice this collapses to `sub ~ sup ∨ sub <: sup`. -/ -private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := - isConsistent sub sup || isSubtype sub sup - /-- Type-level subtype check: emits the standard "expected/got" diagnostic when `actual` is not a consistent subtype of `expected`. Used at sites where the actual type is already in hand (assignment, call args, body vs declared From bbee6a7a0516c93aa14552b94c24f81a6302248a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:45:09 -0400 Subject: [PATCH 034/128] inferholetypes flag already filled hole types when inconsistent --- Strata/Languages/Laurel/InferHoleTypes.lean | 13 ++++++++++++- docs/verso/LaurelDoc.lean | 11 ++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Strata/Languages/Laurel/InferHoleTypes.lean b/Strata/Languages/Laurel/InferHoleTypes.lean index d56ad86881..026a82e5b9 100644 --- a/Strata/Languages/Laurel/InferHoleTypes.lean +++ b/Strata/Languages/Laurel/InferHoleTypes.lean @@ -87,7 +87,7 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol match expr with | AstNode.mk val source => match val with - | .Hole det _ => + | .Hole det existingTy => if expectedType.val == .Unknown then modify fun s => { s with statistics := s.statistics.increment s!"{InferHoleTypesStats.holesLeftUnknown}" @@ -95,6 +95,17 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol } return expr else + -- If the hole already carried a type (from resolution's Hole-None-Check + -- rule, or from a user-written `?: T`), flag a conflict when the two + -- types disagree under consistency (gradual ~). + match existingTy with + | some prior => + unless isConsistent prior expectedType do + modify fun s => { s with + diagnostics := s.diagnostics ++ [diagnosticFromSource source + s!"hole annotated with '{formatHighTypeVal prior.val}' but context expects '{formatHighTypeVal expectedType.val}'"] + } + | none => pure () modify fun s => { s with statistics := s.statistics.increment s!"{InferHoleTypesStats.holesAnnotated}" } return ⟨.Hole det (some expectedType), source⟩ | .PrimitiveOp op args => diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index bf812f7eb5..300c7393c7 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -734,9 +734,14 @@ exists so resolution remains exhaustive over `StmtExpr`. In check mode, an untyped hole records the expected type `T` on the node directly. The subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it just preserves the type information that's available at the check-mode boundary instead of -discarding it. A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended -up in synth-only positions; over time, as more constructs gain bespoke check rules, fewer -holes will need that pass. +discarding it. + +A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended +up in synth-only positions. When that pass encounters a hole whose type was already set +(by Hole-None-Check or by a user-written `?: T`), it checks the resolution-time and +inference-time types for consistency under `~`; a disagreement fires the diagnostic +*"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what +would otherwise be a silent overwrite. # Translation Pipeline From 9e4c2f3dfc723ee36e55285bf53dfcd6ed7fe49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:50:14 -0400 Subject: [PATCH 035/128] future roadmap --- Strata/Languages/Laurel/Resolution.lean | 19 ++++++++++++ docs/verso/LaurelDoc.lean | 41 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 8c88d5189b..152b2bf529 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -104,6 +104,25 @@ Each of these nodes carries a `uniqueId : Option Nat` field (defaulting to `none`). Phase 1 fills in unique values; Phase 2 then builds a map from reference IDs to `ResolvedNode` values describing the definition each reference resolves to. + +## Future structural changes + +A few open structural questions worth recording — see the *Type checking* section of +`LaurelDoc.lean` for context. + +- *Rename to `NameTypeResolution`.* This pass resolves names and type-checks expressions in + one walk. The current name only mentions half of what it does. `NameTypeResolution.lean` + (or similar) would advertise both responsibilities. +- *Eliminate `LaurelTypes.computeExprType` by caching types.* Five later passes + (`LaurelToCoreTranslator`, `ModifiesClauses`, `LiftImperativeExpressions`, + `HeapParameterization`, `TypeHierarchy`) re-derive `StmtExpr` types after resolution. + Resolution already synthesizes those types and discards them. Caching per-node types on + `SemanticModel` (or directly on the AST) would let the later passes look them up instead + of recomputing. +- *Shrink or remove `InferHoleTypes`.* `Hole-None-Check` already records expected types + during resolution for holes in check-mode positions. Holes in synth-only positions still + need the post-pass, but as more constructs gain bespoke check rules, fewer holes need + it; eventually the pass can go away. -/ namespace Strata.Laurel diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 300c7393c7..0b39902de0 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -743,6 +743,47 @@ inference-time types for consistency under `~`; a disagreement fires the diagnos *"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what would otherwise be a silent overwrite. +## Future structural changes + +The current pipeline has resolution and several downstream passes that recompute or +re-derive type information that resolution already synthesized. A few cleanups worth +considering: + +### Rename `Resolution.lean` → `NameTypeResolution.lean` + +The pass resolves names *and* type-checks expressions in one walk; the file name only +advertises the first half. A rename (e.g. `NameTypeResolution.lean` or +`ResolutionAndTyping.lean`) would describe what the pass actually does. The +`SemanticModel` and `ResolvedNode` types could keep their names — they're about resolved +references, not typing. + +### Eliminate `LaurelTypes.computeExprType` by caching types + +`LaurelTypes.lean` exports `computeExprType : SemanticModel → StmtExprMd → HighTypeMd`, +which five later passes call (`LaurelToCoreTranslator`, `ModifiesClauses`, +`LiftImperativeExpressions`, `HeapParameterization`, `TypeHierarchy`) to ask "what's the +type of this expression?" after resolution. Resolution already synthesizes the same types +during its walk, then discards them. Two ways to remove the duplication: + +- *Cache types on the AST.* Add a `HighTypeMd` field to `StmtExpr` (or a parallel + `Std.HashMap Nat HighTypeMd` keyed by node-id, attached to `SemanticModel`), populate it + during resolution, and have later passes read it. `computeExprType` becomes a lookup, + not a re-traversal. +- *Make the cache opt-in.* Same idea, but only enable the type-cache for passes that need + it. Less invasive but partially defeats the point. + +The duplication isn't a correctness issue today (both paths produce consistent results), +just wasted work and a maintenance hazard. + +### Shrink or remove `InferHoleTypes` + +`InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that +Hole-None-Check writes the expected type during resolution for holes in check-mode +positions, the post-pass only needs to handle holes in synth-only positions (e.g. call +arguments resolved through `synthStmtExpr` instead of `checkStmtExpr`). As more constructs +gain bespoke check rules, fewer holes will reach `InferHoleTypes`; eventually the pass +can be deleted entirely. + # Translation Pipeline Laurel programs are verified by translating them to Strata Core and then invoking the Core From 6fc8e368d90be69cc942518607ebfd2a79f23352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:15:30 -0400 Subject: [PATCH 036/128] fix multi value return destructuring --- Strata/Languages/Laurel/Resolution.lean | 54 ++++++++++++------------- docs/verso/LaurelDoc.lean | 25 +++++++----- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 152b2bf529..622b35c302 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -599,33 +599,33 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) let (value', valueTy) ← synthStmtExpr value - -- Check that LHS target count matches the RHS arity (derived from the value type). - let expectedOutputCount := match valueTy.val with - | .MultiValuedExpr tys => tys.length - | _ => 1 - if valueTy.val != HighType.TVoid && targets'.length != expectedOutputCount then - let diag := diagnosticFromSource source - s!"Assignment target count mismatch: {targets'.length} targets but right-hand side produces {expectedOutputCount} values" - modify fun s => { s with errors := s.errors.push diag } - -- Type check: for single-target assignments, check value type matches target type - -- Skip when value type is void (RHS is a statement like while/return that doesn't produce a value) - -- Skip when there's an arity mismatch (already reported above) - if targets'.length == 1 && targets'.length == expectedOutputCount && valueTy.val != HighType.TVoid then - if let some target := targets'.head? then - let targetTy := match target.val with - | .Local ref => do - let s ← get - match s.scope.get? ref.text with - | some (_, node) => pure node.getType - | none => pure { val := HighType.Unknown, source := ref.source : HighTypeMd } - | .Declare param => pure param.type - | .Field _ fieldName => do - let s ← get - match s.scope.get? fieldName.text with - | some (_, node) => pure node.getType - | none => pure { val := HighType.Unknown, source := fieldName.source : HighTypeMd } - let tTy ← targetTy - checkSubtype source tTy valueTy + -- Compute the target's declared type, regardless of whether it's a Local, + -- a Field, or a fresh Declare. + let targetType (t : VariableMd) : ResolveM HighTypeMd := do + let s ← get + match t.val with + | .Local ref => + match s.scope.get? ref.text with + | some (_, node) => pure node.getType + | none => pure { val := .Unknown, source := ref.source } + | .Declare param => pure param.type + | .Field _ fieldName => + match s.scope.get? fieldName.text with + | some (_, node) => pure node.getType + | none => pure { val := .Unknown, source := fieldName.source } + -- Skip all checks when the RHS is a statement (TVoid) — no value to assign. + if valueTy.val != HighType.TVoid then + let targetTys ← targets'.mapM targetType + -- Build the expected type from the targets' declared types: a single + -- type when there's one target, a tuple (MultiValuedExpr) otherwise. + -- This matches the shape of `valueTy`, which is itself MultiValuedExpr + -- exactly when the RHS produces multiple values. A single tuple-vs-tuple + -- check then covers both arity and per-position type mismatches in one + -- diagnostic. + let expectedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + checkSubtype source expectedTy valueTy pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => let (target', _) ← synthStmtExpr target diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 0b39902de0..63fed89e9b 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -260,7 +260,7 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, Return-Void-Error, Return-Multi-Error; While - *Verification statements* — Assert, Assume -- *Assignment* — Assign-Single, Assign-Multi +- *Assignment* — Assign - *Calls* — Static-Call, Static-Call-Multi, Instance-Call - *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate @@ -481,16 +481,23 @@ target is a numeric type. ### Assignment ``` - Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x -─────────────────────────────────────────────── (Assign-Single, impl) - Γ ⊢ Assign [x] e ⇒ TVoid -``` + Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e ExpectedTy <: T_e +───────────────────────────────────────────────────────────────── (Assign, impl) + Γ ⊢ Assign targets e ⇒ TVoid + where ExpectedTy = T_1 if |targets| = 1 + = MultiValuedExpr [T_1; …; T_n] otherwise ``` - Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl) - Γ ⊢ Assign targets e ⇒ TVoid -``` + +The target's declared type `T_i` comes from the variable's scope entry (for +{name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) +or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. Both +single- and multi-target forms collapse into one tuple-vs-tuple check: when the RHS is a +{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`, both arity and per-position +type mismatches surface in a single diagnostic of shape *"expected '(int, int, int)', got +'(int, string)'"*. When the RHS is {name Strata.Laurel.HighType.TVoid}`TVoid` (a +side-effecting statement: `while`, `return`, …), all checks are skipped — there's no value +to assign. ### Calls From dbc220a6716371a0458bb05b3e4588293f0b4137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:15:49 -0400 Subject: [PATCH 037/128] fix error messages to match current type mismatch reporting --- .../Fundamentals/T22_ArityMismatch.lean | 2 +- .../Laurel/ResolutionTypeCheckTests.lean | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean index 94c0f22371..dea2d510fb 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean @@ -39,7 +39,7 @@ procedure mismatch() { var x: int; assign x := twoReturns() -//^^^^^^^^^^^^^^^^^^^^^^^^ error: Assignment target count mismatch +//^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'int', got '(int, int)' }; " diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 3a9fa8f174..85318ad7e9 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -39,7 +39,7 @@ private def processResolution (input : Lean.Parser.InputContext) : IO (Array Dia def ifCondNotBool := r" function foo(x: int): int { if x then 1 else 0 -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -50,7 +50,7 @@ def assertCondNotBool := r" procedure baz() opaque { var x: int := 42; assert x -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -61,7 +61,7 @@ def assumeCondNotBool := r" procedure qux() opaque { var x: int := 42; assume x -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -72,7 +72,7 @@ def whileCondNotBool := r" procedure wh() opaque { var x: int := 1; while (x) { } -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -84,7 +84,7 @@ procedure wh() opaque { def logicalAndNotBool := r" function foo(x: int, y: bool): bool { x && y -//^^^^^^ error: expected bool, but got 'int' +//^^^^^^ error: expected 'bool', got 'int' }; " @@ -95,8 +95,8 @@ function foo(x: int, y: bool): bool { def comparisonNotNumeric := r" function cmp(x: string, y: int): bool { +// ^^^^^^ error: '<' expected a numeric type, got 'string' x < y -//^^^^^ error: expected a numeric type, but got 'string' }; " @@ -108,7 +108,7 @@ function cmp(x: string, y: int): bool { def assignTypeMismatch := r" procedure foo() opaque { var x: int := true -//^^^^^^^^^^^^^^^^^^ error: expected 'int', but got 'bool' +//^^^^^^^^^^^^^^^^^^ error: expected 'int', got 'bool' }; " @@ -119,7 +119,7 @@ procedure foo() opaque { def returnTypeMismatch := r" function foo(): int { -// ^^^ error: expected 'int', but got 'bool' +// ^^^ error: expected 'int', got 'bool' true }; " @@ -133,7 +133,7 @@ def callArgTypeMismatch := r" function bar(x: int): int { x }; function foo(): int { bar(true) -//^^^^^^^^^ error: expected 'int', but got 'bool' +//^^^^^^^^^ error: expected 'int', got 'bool' }; " @@ -169,30 +169,30 @@ def assignTargetCountMismatch := r" procedure multi() returns (a: int, b: int) opaque; procedure test() opaque { var x: int := multi() -//^^^^^^^^^^^^^^^^^^^^^ error: Assignment target count mismatch:1 targets but right-hand side produces 2 values +//^^^^^^^^^^^^^^^^^^^^^ error: expected 'int', got '(int, int)' }; " #guard_msgs (error, drop all) in #eval testInputWithOffset "AssignTargetCountMismatch" assignTargetCountMismatch 156 processResolution -/-! ## UserDefined type pass-through (known limitation) +/-! ## UserDefined cross-type assignment (now rejected) -UserDefined types skip strict assignability checks because subtype/inheritance -relationships are not tracked during resolution. This test documents that -cross-type assignments are silently accepted today. When hierarchy tracking -lands, this test should be updated to expect a rejection. -/ +Cross-type assignments between unrelated user-defined types are rejected +because `isSubtype` is currently structural equality. Once `isSubtype` walks +`extending` chains, this test will need a related-types example to keep +exercising the success path. -/ -def userDefinedPassThrough := r" +def userDefinedCrossType := r" composite Dog { } composite Cat { } procedure test() opaque { var x: Dog := new Cat +//^^^^^^^^^^^^^^^^^^^^^ error: expected 'Dog', got 'Cat' }; " --- This should produce NO diagnostics (UserDefined types are not checked against each other) #guard_msgs (error, drop all) in -#eval testInputWithOffset "UserDefinedPassThrough" userDefinedPassThrough 170 processResolution +#eval testInputWithOffset "UserDefinedCrossType" userDefinedCrossType 170 processResolution end Laurel From 4cb6de774c65249a57970623b4bc2c1b69859443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:17:26 -0400 Subject: [PATCH 038/128] fix error reporting location --- Strata/Languages/Laurel/Resolution.lean | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 622b35c302..677ba564d2 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -645,8 +645,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee - for (argTy, paramTy) in argTypes.zip paramTypes do - checkSubtype source paramTy argTy + for ((a, aTy), paramTy) in (args'.zip argTypes).zip paramTypes do + checkSubtype a.source paramTy aTy pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => let results ← args.mapM synthStmtExpr @@ -662,12 +662,12 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .StrConcat => HighType.TString match op with | .And | .Or | .AndThen | .OrElse | .Not | .Implies => - for aTy in argTypes do - checkSubtype source { val := .TBool, source := aTy.source } aTy + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TBool, source := a.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => - for aTy in argTypes do + for (a, aTy) in args'.zip argTypes do unless isNumeric aTy do - typeMismatch aTy.source (some expr) "expected a numeric type" aTy + typeMismatch a.source (some expr) "expected a numeric type" aTy | .Eq | .Neq => match argTypes with | [lhsTy, rhsTy] => @@ -677,8 +677,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := modify fun s => { s with errors := s.errors.push diag } | _ => pure () | .StrConcat => - for aTy in argTypes do - checkSubtype source { val := .TString, source := aTy.source } aTy + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TString, source := a.source } aTy pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source @@ -709,9 +709,9 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (lhs', lhsTy) ← synthStmtExpr lhs let (rhs', rhsTy) ← synthStmtExpr rhs unless isReference lhsTy do - typeMismatch lhsTy.source (some expr) "expected a reference type" lhsTy + typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy unless isReference rhsTy do - typeMismatch rhsTy.source (some expr) "expected a reference type" rhsTy + typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => let (target', _) ← synthStmtExpr target @@ -731,8 +731,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (retTy, paramTypes) ← getCallInfo callee -- Skip first param (self) when matching args. let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] - for (argTy, paramTy) in argTypes.zip callParamTypes do - checkSubtype source paramTy argTy + for ((a, aTy), paramTy) in (args'.zip argTypes).zip callParamTypes do + checkSubtype a.source paramTy aTy pure (.InstanceCall target' callee' args', retTy) | .Quantifier mode param trigger body => withScope do @@ -751,7 +751,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .Fresh val => let (val', valTy) ← synthStmtExpr val unless isReference valTy do - typeMismatch valTy.source (some expr) "expected a reference type" valTy + typeMismatch val'.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } From 12c7a965182ca977949755b990cda45f72d8c706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:22:07 -0400 Subject: [PATCH 039/128] fix location error reporting --- StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 85318ad7e9..112fa7eba9 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -84,7 +84,7 @@ procedure wh() opaque { def logicalAndNotBool := r" function foo(x: int, y: bool): bool { x && y -//^^^^^^ error: expected 'bool', got 'int' +//^ error: expected 'bool', got 'int' }; " @@ -95,8 +95,8 @@ function foo(x: int, y: bool): bool { def comparisonNotNumeric := r" function cmp(x: string, y: int): bool { -// ^^^^^^ error: '<' expected a numeric type, got 'string' x < y +//^ error: '<' expected a numeric type, got 'string' }; " @@ -133,7 +133,7 @@ def callArgTypeMismatch := r" function bar(x: int): int { x }; function foo(): int { bar(true) -//^^^^^^^^^ error: expected 'int', got 'bool' +// ^^^^ error: expected 'int', got 'bool' }; " From 9e353acb9be95c1d3a94f42210e21f46dc3ac09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 11:30:16 -0400 Subject: [PATCH 040/128] fix field lookup --- Strata/Languages/Laurel/Resolution.lean | 35 +++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 677ba564d2..cb9ab36b00 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -272,6 +272,12 @@ structure ResolveState where nextId : Nat := 1 /-- Current lexical scope (name → definition ID). -/ scope : Scope := {} + /-- Map from definition uniqueId to its ResolvedNode. Populated alongside + `scope` whenever a definition is registered. Unlike `scope`, this map is + *not* saved/restored by `withScope` — uniqueIds are global. Used by + `getVarType` to look up types for references whose `text` doesn't match + a scope key (notably fields, which are scoped under qualified keys). -/ + idToNode : Std.HashMap Nat ResolvedNode := {} /-- Names defined at the current scope level (for duplicate detection). -/ currentScopeNames : Std.HashSet String := {} /-- Per-composite-type field scopes (type name → field name → scope entry). -/ @@ -315,8 +321,10 @@ def defineNameCheckDup (iden : Identifier) (node : ResolvedNode) (overrideResolu let id ← freshId pure ({ iden with uniqueId := some (id) }, id) - modify fun s => { s with scope := s.scope.insert resolutionName (uniqueId, node), - currentScopeNames := s.currentScopeNames.insert resolutionName } + modify fun s => { s with + scope := s.scope.insert resolutionName (uniqueId, node), + idToNode := s.idToNode.insert uniqueId node, + currentScopeNames := s.currentScopeNames.insert resolutionName } return name' /-- Resolve a reference: look up the name in scope and assign the definition's ID. @@ -476,12 +484,18 @@ private def isReference (ty : HighTypeMd) : Bool := | .TCore _ => true | _ => false -/-- Get the type of a resolved variable reference from scope. -/ +/-- Get the type of a resolved reference. Tries the lexical scope by name + first; if that misses (notably for fields, which are scoped under + qualified keys like "Container.intValue"), falls back to a uniqueId + lookup populated as definitions are registered. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get match s.scope.get? ref.text with | some (_, node) => pure node.getType - | none => pure { val := .Unknown, source := ref.source } + | none => + match ref.uniqueId.bind s.idToNode.get? with + | some node => pure node.getType + | none => pure { val := .Unknown, source := ref.source } /-- Get the call return type and parameter types for a callee from scope. -/ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List HighTypeMd) := do @@ -602,17 +616,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := -- Compute the target's declared type, regardless of whether it's a Local, -- a Field, or a fresh Declare. let targetType (t : VariableMd) : ResolveM HighTypeMd := do - let s ← get match t.val with - | .Local ref => - match s.scope.get? ref.text with - | some (_, node) => pure node.getType - | none => pure { val := .Unknown, source := ref.source } + | .Local ref => getVarType ref | .Declare param => pure param.type - | .Field _ fieldName => - match s.scope.get? fieldName.text with - | some (_, node) => pure node.getType - | none => pure { val := .Unknown, source := fieldName.source } + | .Field _ fieldName => getVarType fieldName -- Skip all checks when the RHS is a statement (TVoid) — no value to assign. if valueTy.val != HighType.TVoid then let targetTys ← targets'.mapM targetType @@ -630,7 +637,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .Var (.Field target fieldName) => let (target', _) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let ty ← getVarType fieldName + let ty ← getVarType fieldName' pure (.Var (.Field target' fieldName'), ty) | .PureFieldUpdate target fieldName newVal => let (target', targetTy) ← synthStmtExpr target From 228559e8693597bd0bdf72f23701055e401a369b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 11:50:19 -0400 Subject: [PATCH 041/128] fix silent fail --- .../Languages/Laurel/Examples/Objects/T5_inheritance.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean index ba406b0ddc..4db9a56da2 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean @@ -98,5 +98,5 @@ procedure diamondInheritance() //} " -#guard_msgs (drop info) in +#guard_msgs in #eval testInputWithOffset "Inheritance" program 14 processLaurelFile From 3b3e598201c3724dfec5f066aca11cec27e133c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 16:47:06 -0400 Subject: [PATCH 042/128] thread typing context through type resolution/inheritance --- Strata/Languages/Laurel/InferHoleTypes.lean | 10 ++- Strata/Languages/Laurel/Laurel.lean | 85 ++++++++++++++++++--- Strata/Languages/Laurel/Resolution.lean | 35 ++++++--- 3 files changed, 106 insertions(+), 24 deletions(-) diff --git a/Strata/Languages/Laurel/InferHoleTypes.lean b/Strata/Languages/Laurel/InferHoleTypes.lean index 026a82e5b9..248d90716d 100644 --- a/Strata/Languages/Laurel/InferHoleTypes.lean +++ b/Strata/Languages/Laurel/InferHoleTypes.lean @@ -51,6 +51,8 @@ inductive InferHoleTypesStats where structure InferHoleState where model : SemanticModel + /-- Type-relation tables used by the consistency check on pre-annotated holes. -/ + typeContext : TypeContext currentOutputType : HighTypeMd statistics : Statistics := {} diagnostics : List DiagnosticModel := [] @@ -100,7 +102,8 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol -- types disagree under consistency (gradual ~). match existingTy with | some prior => - unless isConsistent prior expectedType do + let ctx := (← get).typeContext + unless isConsistent ctx prior expectedType do modify fun s => { s with diagnostics := s.diagnostics ++ [diagnosticFromSource source s!"hole annotated with '{formatHighTypeVal prior.val}' but context expects '{formatHighTypeVal expectedType.val}'"] @@ -183,7 +186,10 @@ private def inferProcedure (proc : Procedure) : InferHoleM Procedure := do Annotate every `.Hole` in the program with a type inferred from context. -/ def inferHoleTypes (model : SemanticModel) (program : Program) : Program × List DiagnosticModel × Statistics := - let initState : InferHoleState := { model := model, currentOutputType := { val := .Unknown, source := none }} + let initState : InferHoleState := { + model := model, + typeContext := TypeContext.ofTypes program.types, + currentOutputType := { val := .Unknown, source := none } } let (procs, finalState) := (program.staticProcedures.mapM inferProcedure).run initState ({ program with staticProcedures := procs }, finalState.diagnostics, finalState.statistics) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index 4fc77650cc..b15ef2040f 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -488,24 +488,76 @@ instance : BEq HighTypeMd where deriving instance BEq for HighType -/-- Subtyping. Stub: structural equality via `highEq`. - TODO: walk `extending` chains for composites, unfold aliases, unwrap - constrained types to their base. -/ -def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup +/-- Lookup tables threaded through subtyping/consistency checks. Built from + the program's `TypeDefinition`s by the resolution pass: + - `unfoldMap` maps an alias or constrained type's name to the type it + unwraps to (alias target / constrained base). Followed transitively to + reach a non-alias, non-constrained type. + - `extendingMap` maps a composite type's name to the *direct* parents in + its `extending` list. Walked transitively for the subtype check. -/ +structure TypeContext where + unfoldMap : Std.HashMap String HighTypeMd := {} + extendingMap : Std.HashMap String (List String) := {} + deriving Inhabited + +/-- Unfold aliases and constrained types to their underlying type. + Composites and primitives are returned unchanged. A `visited` set guards + against cycles in the alias/constrained graph (already cycle-checked + elsewhere, but keeps `unfold` safe to call independently). -/ +partial def TypeContext.unfold (ctx : TypeContext) (ty : HighTypeMd) + (visited : Std.HashSet String := {}) : HighTypeMd := + match ty.val with + | .UserDefined name => + if visited.contains name.text then ty + else match ctx.unfoldMap.get? name.text with + | some target => ctx.unfold target (visited.insert name.text) + | none => ty + | _ => ty + +/-- All ancestors of a composite type (including itself), reachable via + repeated `extending` lookups. The `fuel` cap is the number of distinct + type names ever registered, bounding the BFS even with malformed input. -/ +partial def TypeContext.ancestors (ctx : TypeContext) (name : String) : Std.HashSet String := + let rec go (acc : Std.HashSet String) (frontier : List String) : Std.HashSet String := + match frontier with + | [] => acc + | n :: rest => + if acc.contains n then go acc rest + else + let acc' := acc.insert n + let parents := (ctx.extendingMap.get? n).getD [] + go acc' (parents ++ rest) + go {} [name] + +/-- Subtyping. Walks `extending` chains for composites, unfolds aliases, and + unwraps constrained types to their base before falling back to structural + equality via `highEq`. -/ +def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := + let sub' := ctx.unfold sub + let sup' := ctx.unfold sup + match sub'.val, sup'.val with + | .UserDefined subName, .UserDefined supName => + -- After unfolding, both sides are composites (or unresolved). A composite + -- is a subtype of any type in its extending chain. + (ctx.ancestors subName.text).contains supName.text || highEq sub' sup' + | _, _ => highEq sub' sup' /-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the dynamic type and is consistent with everything; otherwise structural - equality. `TCore` is a temporary migration escape hatch. -/ -def isConsistent (a b : HighTypeMd) : Bool := - match a.val, b.val with + equality after unfolding aliases / constrained types. `TCore` is a + temporary migration escape hatch. -/ +def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := + let a' := ctx.unfold a + let b' := ctx.unfold b + match a'.val, b'.val with | .Unknown, _ | _, .Unknown => true | .TCore _, _ | _, .TCore _ => true - | _, _ => highEq a b + | _, _ => highEq a' b' /-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice this collapses to `sub ~ sup ∨ sub <: sup`. -/ -def isConsistentSubtype (sub sup : HighTypeMd) : Bool := - isConsistent sub sup || isSubtype sub sup +def isConsistentSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := + isConsistent ctx sub sup || isSubtype ctx sub sup def HighType.isBool : HighType → Bool | TBool => true @@ -644,6 +696,19 @@ def TypeDefinition.name : TypeDefinition → Identifier | .Datatype ty => ty.name | .Alias ty => ty.name +/-- Build a `TypeContext` from a list of `TypeDefinition`s. + Aliases populate `unfoldMap` with their target; constrained types populate + it with their base; composites populate `extendingMap` with their direct + parents. Datatypes contribute nothing — they're nominal and irreducible. -/ +def TypeContext.ofTypes (types : List TypeDefinition) : TypeContext := + types.foldl (init := {}) fun ctx td => + match td with + | .Alias ta => { ctx with unfoldMap := ctx.unfoldMap.insert ta.name.text ta.target } + | .Constrained ct => { ctx with unfoldMap := ctx.unfoldMap.insert ct.name.text ct.base } + | .Composite c => + { ctx with extendingMap := ctx.extendingMap.insert c.name.text (c.extending.map (·.text)) } + | .Datatype _ => ctx + structure Constant where name : Identifier type : HighTypeMd diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index cb9ab36b00..0efbe7060b 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -291,6 +291,10 @@ structure ResolveState where declaration order). `none` means no enclosing procedure. Used by `Return` to type-check the optional return value and to flag arity/shape mismatches. -/ expectedReturnTypes : Option (List HighTypeMd) := none + /-- Type-relation tables (alias/constrained unfolding + composite extending + chains) used by the subtyping/consistency checks. Built once from + `program.types` at the start of `resolve`. -/ + typeContext : TypeContext := {} @[expose] abbrev ResolveM := StateM ResolveState @@ -464,13 +468,16 @@ private def typeMismatch (source : Option FileRange) (construct : Option StmtExp actual type is already in hand (assignment, call args, body vs declared output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do - unless isConsistentSubtype actual expected do + let ctx := (← get).typeContext + unless isConsistentSubtype ctx actual expected do typeMismatch source none s!"expected '{formatType expected}'" actual /-- Test whether a type is in the set of numeric primitives. `Unknown` and - `TCore` are accepted as gradual escape hatches. Used by Op-Cmp / Op-Arith. -/ -private def isNumeric (ty : HighTypeMd) : Bool := - match ty.val with + `TCore` are accepted as gradual escape hatches. Aliases and constrained + types are unfolded first so e.g. `nat` (constrained over `int`) counts as + numeric. Used by Op-Cmp / Op-Arith. -/ +private def isNumeric (ctx : TypeContext) (ty : HighTypeMd) : Bool := + match (ctx.unfold ty).val with | .TInt | .TReal | .TFloat64 | .Unknown => true | .TCore _ => true | _ => false @@ -478,8 +485,8 @@ private def isNumeric (ty : HighTypeMd) : Bool := /-- Test whether a type is a user-defined reference type. `Unknown` and `TCore` are accepted as gradual escape hatches. Used by Fresh and ReferenceEquals, which only make sense on composite/datatype references. -/ -private def isReference (ty : HighTypeMd) : Bool := - match ty.val with +private def isReference (ctx : TypeContext) (ty : HighTypeMd) : Bool := + match (ctx.unfold ty).val with | .UserDefined _ | .Unknown => true | .TCore _ => true | _ => false @@ -672,13 +679,15 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := for (a, aTy) in args'.zip argTypes do checkSubtype a.source { val := .TBool, source := a.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => + let ctx := (← get).typeContext for (a, aTy) in args'.zip argTypes do - unless isNumeric aTy do + unless isNumeric ctx aTy do typeMismatch a.source (some expr) "expected a numeric type" aTy | .Eq | .Neq => match argTypes with | [lhsTy, rhsTy] => - unless isConsistent lhsTy rhsTy do + let ctx := (← get).typeContext + unless isConsistent ctx lhsTy rhsTy do let diag := diagnosticFromSource source s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" modify fun s => { s with errors := s.errors.push diag } @@ -715,9 +724,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .ReferenceEquals lhs rhs => let (lhs', lhsTy) ← synthStmtExpr lhs let (rhs', rhsTy) ← synthStmtExpr rhs - unless isReference lhsTy do + let ctx := (← get).typeContext + unless isReference ctx lhsTy do typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy - unless isReference rhsTy do + unless isReference ctx rhsTy do typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => @@ -757,7 +767,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.Old val', valTy) | .Fresh val => let (val', valTy) ← synthStmtExpr val - unless isReference valTy do + unless isReference (← get).typeContext valTy do typeMismatch val'.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => @@ -1246,7 +1256,8 @@ def resolve (program : Program) (existingModel: Option SemanticModel := none) : return { staticProcedures := staticProcs', staticFields := staticFields', types := types', constants := constants' } let nextId := existingModel.elim 1 (fun m => m.nextId) - let (program', finalState) := phase1.run { nextId := nextId } + let typeContext := TypeContext.ofTypes program.types + let (program', finalState) := phase1.run { nextId := nextId, typeContext } -- Phase 2: build refToDef from the resolved program (all definitions now have UUIDs) let refToDef := buildRefToDef program' { program := program', From 2dffa2a48932e6bafd891b761244ae48b90c11dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 16:48:29 -0400 Subject: [PATCH 043/128] drop info report of an expected downcast failure ; to fix this, we need to improve the testing facilities for Laurel --- .../Languages/Laurel/Examples/Objects/T5_inheritance.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean index 4db9a56da2..ba406b0ddc 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean @@ -98,5 +98,5 @@ procedure diamondInheritance() //} " -#guard_msgs in +#guard_msgs (drop info) in #eval testInputWithOffset "Inheritance" program 14 processLaurelFile From 6fa3c22675f310cf33b7700a598b3b9ecd03a649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 10:28:05 -0400 Subject: [PATCH 044/128] fix typing doc direction --- docs/verso/LaurelDoc.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 63fed89e9b..1acbd5c02f 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -481,7 +481,7 @@ target is a numeric type. ### Assignment ``` - Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e ExpectedTy <: T_e + Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e T_e <: ExpectedTy ───────────────────────────────────────────────────────────────── (Assign, impl) Γ ⊢ Assign targets e ⇒ TVoid From 3440420f952c6804a24886ac6e91b400e3265415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 10:32:50 -0400 Subject: [PATCH 045/128] fix documentation : subtyping is implemented --- docs/verso/LaurelDoc.lean | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 1acbd5c02f..52e2cda2b7 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -203,11 +203,13 @@ internal interface used by other rules. The relation `<:` (used in Sub) is built from three Lean functions: -- `isSubtype` — pure subtyping. The stub is structural equality via - {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the `extending` - chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds +- `isSubtype` — pure subtyping. Walks the `extending` chain for + {name Strata.Laurel.CompositeType}`CompositeType` (via + {name Strata.Laurel.TypeContext.ancestors}`TypeContext.ancestors`), unfolds {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps - {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. + {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base (both via + {name Strata.Laurel.TypeContext.unfold}`TypeContext.unfold`), then falls back to + structural equality via {name Strata.Laurel.highEq}`highEq`. - `isConsistent` — the symmetric gradual relation `~` (Siek–Taha): {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type and is consistent with everything; otherwise structural equality. @@ -233,9 +235,8 @@ A previous iteration was synth-only with three *bivariantly-compatible* wildcard {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no assignment, call argument, or comparison involving a user type was ever rejected. The bidirectional design retires that carve-out — user-defined types are now a regular -participant in `<:`, and tightening `isSubtype` (to walk inheritance and unwrap -constrained types) gradually buys real checking on user-defined code without changing -callers. +participant in `<:`, with `isSubtype` walking inheritance chains and unwrapping aliases +and constrained types to deliver real checking on user-defined code. Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This includes {name Strata.Laurel.StmtExpr.Return}`Return`, From 8fc56ae0206c18b257ed06f7174fd174a578dbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 13:14:35 -0400 Subject: [PATCH 046/128] remove special treatment of TCore --- Strata/Languages/Laurel/Laurel.lean | 5 ++--- Strata/Languages/Laurel/Resolution.lean | 16 +++++++--------- docs/verso/LaurelDoc.lean | 15 +++++---------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index b15ef2040f..ff67dafe1b 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -467,6 +467,7 @@ def highEq (a : HighTypeMd) (b : HighTypeMd) : Bool := match _a: a.val, _b: b.va | HighType.TSet t1, HighType.TSet t2 => highEq t1 t2 | HighType.TMap k1 v1, HighType.TMap k2 v2 => highEq k1 k2 && highEq v1 v2 | HighType.UserDefined r1, HighType.UserDefined r2 => r1.text == r2.text + | HighType.TCore s1, HighType.TCore s2 => s1 == s2 | HighType.Applied b1 args1, HighType.Applied b2 args2 => highEq b1 b2 && args1.length == args2.length && (args1.attach.zip args2 |>.all (fun (a1, a2) => highEq a1.1 a2)) | HighType.Pure b1, HighType.Pure b2 => highEq b1 b2 @@ -544,14 +545,12 @@ def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := /-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the dynamic type and is consistent with everything; otherwise structural - equality after unfolding aliases / constrained types. `TCore` is a - temporary migration escape hatch. -/ + equality after unfolding aliases / constrained types. -/ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := let a' := ctx.unfold a let b' := ctx.unfold b match a'.val, b'.val with | .Unknown, _ | _, .Unknown => true - | .TCore _, _ | _, .TCore _ => true | _, _ => highEq a' b' /-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 0efbe7060b..81d96adca6 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -472,23 +472,21 @@ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (ac unless isConsistentSubtype ctx actual expected do typeMismatch source none s!"expected '{formatType expected}'" actual -/-- Test whether a type is in the set of numeric primitives. `Unknown` and - `TCore` are accepted as gradual escape hatches. Aliases and constrained - types are unfolded first so e.g. `nat` (constrained over `int`) counts as - numeric. Used by Op-Cmp / Op-Arith. -/ +/-- Test whether a type is in the set of numeric primitives. `Unknown` is + accepted as a gradual escape hatch. Aliases and constrained types are + unfolded first so e.g. `nat` (constrained over `int`) counts as numeric. + Used by Op-Cmp / Op-Arith. -/ private def isNumeric (ctx : TypeContext) (ty : HighTypeMd) : Bool := match (ctx.unfold ty).val with | .TInt | .TReal | .TFloat64 | .Unknown => true - | .TCore _ => true | _ => false -/-- Test whether a type is a user-defined reference type. `Unknown` and `TCore` - are accepted as gradual escape hatches. Used by Fresh and ReferenceEquals, - which only make sense on composite/datatype references. -/ +/-- Test whether a type is a user-defined reference type. `Unknown` is accepted + as a gradual escape hatch. Used by Fresh and ReferenceEquals, which only + make sense on composite/datatype references. -/ private def isReference (ctx : TypeContext) (ty : HighTypeMd) : Bool := match (ctx.unfold ty).val with | .UserDefined _ | .Unknown => true - | .TCore _ => true | _ => false /-- Get the type of a resolved reference. Tries the lexical scope by name diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 52e2cda2b7..e73ab90f00 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -216,10 +216,6 @@ The relation `<:` (used in Sub) is built from three Lean functions: - `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this is the standard collapse of `∃R. T ~ R ∧ R <: U`. -{name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now as a temporary -migration escape hatch from the Core language; the carve-out lives in `isConsistent` and is -intentionally temporary. - Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) @@ -228,10 +224,9 @@ flows freely into any typed slot, and any expression flows freely into a slot of fully-known types only. The symmetric `isConsistent` is used directly by Op-Eq, where the operand types must be mutually consistent (no subtype direction is privileged). -A previous iteration was synth-only with three *bivariantly-compatible* wildcards: -{name Strata.Laurel.HighType.Unknown}`Unknown`, -{name Strata.Laurel.HighType.UserDefined}`UserDefined`, and -{name Strata.Laurel.HighType.TCore}`TCore`. The +A previous iteration was synth-only with two *bivariantly-compatible* wildcards: +{name Strata.Laurel.HighType.Unknown}`Unknown` and +{name Strata.Laurel.HighType.UserDefined}`UserDefined`. The {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no assignment, call argument, or comparison involving a user type was ever rejected. The bidirectional design retires that carve-out — user-defined types are now a regular @@ -600,8 +595,8 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` -`isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined`, -{name Strata.Laurel.HighType.Unknown}`Unknown`, or {name Strata.Laurel.HighType.TCore}`TCore` +`isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` +or {name Strata.Laurel.HighType.Unknown}`Unknown` type. Reference equality is meaningless on primitives. Compatibility between `T_l` and `T_r` (e.g. rejecting `Cat === Dog` for unrelated user-defined types) is delegated to future tightening of `<:` — today, two distinct user-defined names already mismatch From d476f6b6f76a58aa48ea933670ecdcf123a1016e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 13:50:03 -0400 Subject: [PATCH 047/128] fix TCore documentation --- .../Languages/Laurel/ResolutionTypeCheckTests.lean | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 112fa7eba9..b78f3b22df 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -176,12 +176,11 @@ procedure test() opaque { #guard_msgs (error, drop all) in #eval testInputWithOffset "AssignTargetCountMismatch" assignTargetCountMismatch 156 processResolution -/-! ## UserDefined cross-type assignment (now rejected) +/-! ## UserDefined cross-type assignment -Cross-type assignments between unrelated user-defined types are rejected -because `isSubtype` is currently structural equality. Once `isSubtype` walks -`extending` chains, this test will need a related-types example to keep -exercising the success path. -/ +Assignments between unrelated composites are rejected: `isSubtype` walks +`extending` chains, so two composites with no common ancestor are not +subtypes of each other. -/ def userDefinedCrossType := r" composite Dog { } From 40236cecd644312a27d28b1df5403e26788376f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 13:50:31 -0400 Subject: [PATCH 048/128] uniform <=/=> and use latex rule presentation --- docs/verso/LaurelDoc.lean | 390 ++++++++++---------------------------- 1 file changed, 95 insertions(+), 295 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index e73ab90f00..140a250407 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -169,11 +169,7 @@ expression has a given expected type. Each construct picks a mode based on wheth is determined locally (synth) or by context (check). The two judgments are connected by a single change-of-direction rule, *subsumption*: -``` -Γ ⊢ e ⇒ A A <: B -───────────────────── (Sub) - Γ ⊢ e ⇐ B -``` +$$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` Subsumption is the *only* place the checker switches from check to synth mode. It fires as the default fallback in @@ -186,9 +182,10 @@ propagate through nested control flow. `synthStmtExpr` and `checkStmtExpr` are mutually recursive: synth rules invoke check on subexpressions whose expected type is known (e.g. `cond ⇐ TBool` in {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), and `checkStmtExpr` falls back to -`synthStmtExpr` via Sub. Termination uses a lexicographic measure `(exprMd, tag)` where the -tag is `0` for synth and `1` for check; any descent into a strict subterm decreases via -`Prod.Lex.left`, while Sub calls synth on the *same* expression and decreases via +`synthStmtExpr` via \[⇐\] Sub. Termination uses a lexicographic measure `(exprMd, tag)` +where the tag is `0` for synth and `1` for check; any descent into a strict subterm +decreases via `Prod.Lex.left`, while \[⇐\] Sub calls synth on the *same* expression and +decreases via `Prod.Lex.right`. This is the standard well-founded encoding for bidirectional systems. There is also a thin `resolveStmtExpr` wrapper that calls `synthStmtExpr` and discards the @@ -201,7 +198,7 @@ internal interface used by other rules. ### Gradual typing -The relation `<:` (used in Sub) is built from three Lean functions: +The relation `<:` (used in \[⇐\] Sub) is built from three Lean functions: - `isSubtype` — pure subtyping. Walks the `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType` (via @@ -216,13 +213,13 @@ The relation `<:` (used in Sub) is built from three Lean functions: - `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this is the standard collapse of `∃R. T ~ R ∧ R <: U`. -Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what +\[⇐\] Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely into any typed slot, and any expression flows freely into a slot of type {name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between -fully-known types only. The symmetric `isConsistent` is used directly by Op-Eq, where the -operand types must be mutually consistent (no subtype direction is privileged). +fully-known types only. The symmetric `isConsistent` is used directly by \[⇒\] Op-Eq, where +the operand types must be mutually consistent (no subtype direction is privileged). A previous iteration was synth-only with two *bivariantly-compatible* wildcards: {name Strata.Laurel.HighType.Unknown}`Unknown` and @@ -245,130 +242,90 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, Each construct is given as a derivation. `Γ` is the current lexical scope (see {name Strata.Laurel.ResolveState}`ResolveState`'s `scope`); it threads identically through every premise and conclusion unless a rule explicitly extends it (written `Γ, x : T`). -`(impl)` = implemented; `(planned)` = intended, not yet wired in. + +Each rule is tagged with `[⇒]` (synthesis) or `[⇐]` (checking) to make the +direction explicit. When a construct has both modes, the `-Synth` / `-Check` +suffix is dropped in favor of the prefix. ### Index -- *Subsumption* — Sub -- *Literals* — Lit-Int, Lit-Bool, Lit-String, Lit-Decimal -- *Variables* — Var-Local, Var-Field, Var-Declare -- *Control flow* — If-NoElse, If-Synth, If-Check, If-Check-NoElse; Block-Synth, - Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, - Return-Void-Error, Return-Multi-Error; While -- *Verification statements* — Assert, Assume -- *Assignment* — Assign -- *Calls* — Static-Call, Static-Call-Multi, Instance-Call -- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat -- *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate -- *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy -- *Self reference* — This-Inside, This-Outside -- *Untyped forms* — Abstract / All -- *ContractOf* — ContractOf-Bool, ContractOf-Set, ContractOf-Error -- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check +- *Subsumption* — \[⇐\] Sub +- *Literals* — \[⇒\] Lit-Int, \[⇒\] Lit-Bool, \[⇒\] Lit-String, \[⇒\] Lit-Decimal +- *Variables* — \[⇒\] Var-Local, \[⇒\] Var-Field, \[⇒\] Var-Declare +- *Control flow* — \[⇒\] If-NoElse, \[⇒\] If, \[⇐\] If, \[⇐\] If-NoElse; + \[⇒\] Block, \[⇒\] Block-Empty, \[⇐\] Block, \[⇐\] Block-Empty; \[⇒\] Exit; + \[⇒\] Return-None, \[⇒\] Return-Some, \[⇒\] Return-Void-Error, + \[⇒\] Return-Multi-Error; \[⇒\] While +- *Verification statements* — \[⇒\] Assert, \[⇒\] Assume +- *Assignment* — \[⇒\] Assign +- *Calls* — \[⇒\] Static-Call, \[⇒\] Static-Call-Multi, \[⇒\] Instance-Call +- *Primitive operations* — \[⇒\] Op-Bool, \[⇒\] Op-Cmp, \[⇒\] Op-Eq, \[⇒\] Op-Arith, + \[⇒\] Op-Concat +- *Object forms* — \[⇒\] New-Ok, \[⇒\] New-Fallback; \[⇒\] AsType; \[⇒\] IsType; + \[⇒\] RefEq; \[⇒\] PureFieldUpdate +- *Verification expressions* — \[⇒\] Quantifier, \[⇒\] Assigned, \[⇒\] Old, + \[⇒\] Fresh, \[⇒\] ProveBy +- *Self reference* — \[⇒\] This-Inside, \[⇒\] This-Outside +- *Untyped forms* — \[⇒\] Abstract / All +- *ContractOf* — \[⇒\] ContractOf-Bool, \[⇒\] ContractOf-Set, \[⇒\] ContractOf-Error +- *Holes* — \[⇒\] Hole-Some, \[⇒\] Hole-None, \[⇐\] Hole-None ### Subsumption -``` -Γ ⊢ e ⇒ A A <: B -───────────────────── (Sub, impl) - Γ ⊢ e ⇐ B -``` +$$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### Literals -``` -────────────────────────── (Lit-Int, impl) - Γ ⊢ LiteralInt n ⇒ TInt -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralInt}\;n \Rightarrow \mathsf{TInt}} \quad \text{([⇒] Lit-Int)}` -``` -─────────────────────────── (Lit-Bool, impl) - Γ ⊢ LiteralBool b ⇒ TBool -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralBool}\;b \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Lit-Bool)}` -``` -───────────────────────────────── (Lit-String, impl) - Γ ⊢ LiteralString s ⇒ TString -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralString}\;s \Rightarrow \mathsf{TString}} \quad \text{([⇒] Lit-String)}` -``` -────────────────────────────────── (Lit-Decimal, impl) - Γ ⊢ LiteralDecimal d ⇒ TReal -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralDecimal}\;d \Rightarrow \mathsf{TReal}} \quad \text{([⇒] Lit-Decimal)}` ### Variables -``` - Γ(x) = T -─────────────────────────── (Var-Local, impl) - Γ ⊢ Var (.Local x) ⇒ T -``` +$$`\frac{\Gamma(x) = T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Local}\;x) \Rightarrow T} \quad \text{([⇒] Var-Local)}` -``` - Γ ⊢ e ⇒ _ Γ(f) = T_f -────────────────────────────── (Var-Field, impl) - Γ ⊢ Var (.Field e f) ⇒ T_f -``` +$$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Field}\;e\;f) \Rightarrow T_f} \quad \text{([⇒] Var-Field)}` Resolution looks `f` up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -``` - x ∉ dom(Γ) -───────────────────────────────────────── (Var-Declare, impl) - Γ ⊢ Var (.Declare ⟨x, T⟩) ⇒ TVoid ⊣ Γ, x : T -``` +$$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. ### Control flow -``` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T -───────────────────────────────────────────── (If-NoElse, impl) - Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when `cond` is false; without this, `x : int := if c then 5` would type-check spuriously. -``` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e -────────────────────────────────────────────────────────────── (If-Synth, impl) - Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t} \quad \text{([⇒] If)}` Picks the then-branch type arbitrarily; the two branches are *not* compared, since a statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The -enclosing context's check (Sub, or a containing `checkSubtype` like an assignment) provides -the actual check downstream. - -``` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T -────────────────────────────────────────────────────────── (If-Check, impl) - Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T +enclosing context's check (\[⇐\] Sub, or a containing `checkSubtype` like an assignment) +provides the actual check downstream. +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T TVoid <: T -───────────────────────────────────────────────────── (If-Check-NoElse, impl) - Γ ⊢ IfThenElse cond thenBr none ⇐ T -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}` -Check mode pushes `T` into both branches (rather than going through If-Synth + Sub at the -boundary). Errors fire at the offending branch instead of the surrounding `if`. Without an -else branch, the construct can only succeed when `T` admits -{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `Block-Check-Empty` +Check mode pushes `T` into both branches (rather than going through \[⇒\] If + \[⇐\] Sub at +the boundary). Errors fire at the offending branch instead of the surrounding `if`. +Without an else branch, the construct can only succeed when `T` admits +{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `\[⇐\] Block-Empty` performs for an empty block. -``` -Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T -─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) - Γ ⊢ Block [s_1; …; s_n] label ⇒ T -``` +$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` `Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced by its predecessor and may itself extend it (`Var (.Declare …)` does); `s_n` is typed in @@ -379,16 +336,9 @@ Non-last statements are synthesized but their types discarded (the lax rule). Th Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` is silently accepted; flagging it belongs to a lint. -``` -───────────────────────────── (Block-Synth-Empty, impl) - Γ ⊢ Block [] label ⇒ TVoid -``` +$$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` -``` -Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T -─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) - Γ ⊢ Block [s_1; …; s_n] label ⇐ T -``` +$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` Pushes `T` into the *last* statement rather than comparing the block's synthesized type at the boundary. Errors fire at the offending subexpression, and `T` keeps propagating through @@ -397,16 +347,9 @@ nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. -``` - TVoid <: T -───────────────────────── (Block-Check-Empty, impl) - Γ ⊢ Block [] label ⇐ T -``` +$$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` -``` -──────────────────────── (Exit, impl) - Γ ⊢ Exit target ⇒ TVoid -``` +$$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` `Return` matches the optional return value against the enclosing procedure's declared outputs. The expected output types are threaded through @@ -416,74 +359,42 @@ outputs. The expected output types are threaded through the body. `none` means "no enclosing procedure" — e.g. resolving a constant initializer — and skips all `Return` checks. -``` -───────────────────────────── (Return-None, impl) - Γ ⊢ Return none ⇒ TVoid -``` +$$`\frac{}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None)}` A bare `return;` is allowed in any context. In a single-output procedure it acts as a Dafny-style early exit — the output parameter retains whatever was last assigned to it. -``` - Γ_proc.outputs = [T] Γ ⊢ e ⇐ T -────────────────────────────────────── (Return-Some, impl) - Γ ⊢ Return (some e) ⇒ TVoid -``` +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T] \quad \Gamma \vdash e \Leftarrow T}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-Some)}` In a single-output procedure, the value is checked against the declared output type. This closes the prior soundness gap where `return 0` in a `bool`-returning procedure went uncaught. -``` - Γ_proc.outputs = [] -───────────────────────────────── (Return-Void-Error, impl) - Γ ⊢ Return (some e) — error: "void procedure cannot return a value" - +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “void procedure cannot return a value”}} \quad \text{([⇒] Return-Void-Error)}` - Γ_proc.outputs = [T_1; …; T_n] (n ≥ 2) -────────────────────────────────────────────────────────── (Return-Multi-Error, impl) - Γ ⊢ Return (some e) — error: "multi-output procedure cannot - use 'return e'; assign to named outputs instead" -``` +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}` Multi-output procedures use named-output assignment (`r := …` on the declared output parameters). `return e` syntactically takes a single {name Strata.Laurel.StmtExpr.Return}`Option StmtExpr`, so it cannot carry multiple values; flagging it points users at the named-output convention. -``` - Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ -─────────────────────────────────────────────────────────────────────────────── (While, impl) - Γ ⊢ While cond invs dec body ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Rightarrow \_}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}` `dec` (the optional decreases clause) is resolved without a type check today; the intended target is a numeric type. ### Verification statements -``` - Γ ⊢ cond ⇐ TBool -────────────────────────────── (Assert, impl) - Γ ⊢ Assert cond ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}` -``` - Γ ⊢ cond ⇐ TBool -───────────────────────────── (Assume, impl) - Γ ⊢ Assume cond ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}` ### Assignment -``` - Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e T_e <: ExpectedTy -───────────────────────────────────────────────────────────────── (Assign, impl) - Γ ⊢ Assign targets e ⇒ TVoid +$$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Rightarrow T_e \quad T_e <: \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assign)}` - where ExpectedTy = T_1 if |targets| = 1 - = MultiValuedExpr [T_1; …; T_n] otherwise -``` +where `ExpectedTy = T_1` if `|targets| = 1` and `MultiValuedExpr [T_1; …; T_n]` otherwise. The target's declared type `T_i` comes from the variable's scope entry (for {name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) @@ -497,26 +408,11 @@ to assign. ### Calls -``` - Γ(callee) = static-procedure with inputs Ts and outputs [T] - Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────── (Static-Call, impl) - Γ ⊢ StaticCall callee args ⇒ T -``` +$$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Static-Call)}` -``` - Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 - Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl) - Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] -``` +$$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T_1; \ldots; T_n],\; n \ne 1 \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Static-Call-Multi)}` -``` - Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] - Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl) - Γ ⊢ InstanceCall target callee args ⇒ T -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_ \quad \Gamma(\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{InstanceCall}\;\mathit{target}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Instance-Call)}` ### Primitive operations @@ -524,76 +420,36 @@ to assign. {name Strata.Laurel.HighType.TReal}`TReal`, {name Strata.Laurel.HighType.TFloat64}`TFloat64`". -``` - Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} -────────────────────────────────── (Op-Bool, impl) - Γ ⊢ PrimitiveOp op args ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TBool} \quad \mathit{op} \in \{\mathsf{And}, \mathsf{Or}, \mathsf{AndThen}, \mathsf{OrElse}, \mathsf{Not}, \mathsf{Implies}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Bool)}` -``` - Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} -───────────────────────────────── (Op-Cmp, impl) - Γ ⊢ PrimitiveOp op args ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \mathit{op} \in \{\mathsf{Lt}, \mathsf{Leq}, \mathsf{Gt}, \mathsf{Geq}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Cmp)}` -``` - Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l ~ T_r op ∈ {Eq, Neq} -───────────────────────────────────────────────────────── (Op-Eq, impl) - Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad T_l \sim T_r \quad \mathit{op} \in \{\mathsf{Eq}, \mathsf{Neq}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;[\mathit{lhs}; \mathit{rhs}] \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Eq)}` `~` is the consistency relation `isConsistent` — symmetric, with the {name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. -``` - Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} -────────────────────────────────────────────────── (Op-Arith, impl) - Γ ⊢ PrimitiveOp op args ⇒ T -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \Gamma \vdash \mathit{args}.\mathsf{head} \Rightarrow T \quad \mathit{op} \in \{\mathsf{Neg}, \mathsf{Add}, \mathsf{Sub}, \mathsf{Mul}, \mathsf{Div}, \mathsf{Mod}, \mathsf{DivT}, \mathsf{ModT}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Op-Arith)}` "Result is the type of the first argument" handles `int + int → int`, `real + real → real`, etc. without unification. Known relaxation: `int + real` passes (each operand individually passes `Numeric`); a proper fix needs numeric promotion or unification. -``` - Γ ⊢ args_i ⇐ TString op = StrConcat -───────────────────────────────────── (Op-Concat, impl) - Γ ⊢ PrimitiveOp op args ⇒ TString -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TString} \quad \mathit{op} = \mathsf{StrConcat}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TString}} \quad \text{([⇒] Op-Concat)}` ### Object forms -``` - Γ(ref) is a composite or datatype T -────────────────────────────────────────── (New-Ok, impl) - Γ ⊢ New ref ⇒ UserDefined T -``` +$$`\frac{\Gamma(\mathit{ref}) \text{ is a composite or datatype } T}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] New-Ok)}` -``` - Γ(ref) is not a composite or datatype -───────────────────────────────────────── (New-Fallback, impl) - Γ ⊢ New ref ⇒ Unknown -``` +$$`\frac{\Gamma(\mathit{ref}) \text{ is not a composite or datatype}}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] New-Fallback)}` -``` - Γ ⊢ target ⇒ _ -───────────────────────────── (AsType, impl) - Γ ⊢ AsType target T ⇒ T -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsType}\;\mathit{target}\;T \Rightarrow T} \quad \text{([⇒] AsType)}` `target` is resolved but not checked against `T` — the cast is the user's claim. -``` - Γ ⊢ target ⇒ _ -───────────────────────────────── (IsType, impl) - Γ ⊢ IsType target T ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` -``` - Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r isReference T_l isReference T_r -───────────────────────────────────────────────────────────────────────────── (RefEq, impl) - Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` `isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` or {name Strata.Laurel.HighType.Unknown}`Unknown` @@ -602,67 +458,36 @@ type. Reference equality is meaningless on primitives. Compatibility between `T_ future tightening of `<:` — today, two distinct user-defined names already mismatch structurally, so the check would only fire under stronger subtyping. -``` - Γ ⊢ target ⇒ T_t Γ(f) = T_f Γ ⊢ newVal ⇐ T_f -───────────────────────────────────────────────────────────── (PureFieldUpdate, impl) - Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` `f` is resolved against `T_t` (or the enclosing instance type) and `newVal` is checked against the field's declared type. ### Verification expressions -``` - Γ, x : T ⊢ body ⇐ TBool -───────────────────────────────────────────────── (Quantifier, impl) - Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool -``` +$$`\frac{\Gamma, x : T \vdash \mathit{body} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Quantifier}\;\mathit{mode}\;\langle x, T\rangle\;\mathit{trig}\;\mathit{body} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Quantifier)}` The bound variable `x : T` is introduced in scope only for the body (and trigger). The body is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a proposition; without this, `forall x: int :: x + 1` would be silently accepted. -``` - Γ ⊢ name ⇒ _ -───────────────────────────── (Assigned, impl) - Γ ⊢ Assigned name ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assigned}\;\mathit{name} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Assigned)}` -``` - Γ ⊢ v ⇒ T -───────────────── (Old, impl) - Γ ⊢ Old v ⇒ T -``` +$$`\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old)}` -``` - Γ ⊢ v ⇒ T isReference T -───────────────────────────────── (Fresh, impl) - Γ ⊢ Fresh v ⇒ TBool -``` +$$`\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}` `isReference T` is the same predicate as in {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals`. {name Strata.Laurel.StmtExpr.Fresh}`Fresh` only makes sense on heap-allocated references; `fresh(5)` is rejected. -``` - Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ -─────────────────────────────────── (ProveBy, impl) - Γ ⊢ ProveBy v proof ⇒ T -``` +$$`\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy)}` ### Self reference -``` - Γ.instanceTypeName = some T -────────────────────────────────── (This-Inside, impl) - Γ ⊢ This ⇒ UserDefined T - +$$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{some}\;T}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] This-Inside)}` - Γ.instanceTypeName = none -────────────────────────────── (This-Outside, impl) - Γ ⊢ This ⇒ Unknown [emits "'this' is not allowed outside instance methods"] -``` +$$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{none}}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{Unknown}\;\;[\text{emits “‘this’ is not allowed outside instance methods”}]} \quad \text{([⇒] This-Outside)}` `Γ.instanceTypeName` is the {name Strata.Laurel.ResolveState}`ResolveState` field set by @@ -672,10 +497,7 @@ types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}` ### Untyped forms -``` -───────────────────────────────── (Abstract / All, impl) - Γ ⊢ Abstract / All … ⇒ Unknown -``` +$$`\frac{}{\Gamma \vdash \mathsf{Abstract}\;/\;\mathsf{All}\;\ldots \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Abstract / All)}` ### ContractOf @@ -684,18 +506,9 @@ types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}` (`Modifies`). `fn` must be a direct identifier reference to a procedure — a contract belongs to a *named* procedure, not an arbitrary expression. -``` - fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} -───────────────────────────────────────────────────────────────────────── (ContractOf-Bool, impl) - Γ ⊢ ContractOf Precondition fn ⇒ TBool - Γ ⊢ ContractOf PostCondition fn ⇒ TBool +$$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Precondition}\;\mathit{fn} \Rightarrow \mathsf{TBool} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{PostCondition}\;\mathit{fn} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] ContractOf-Bool)}` - - fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} -───────────────────────────────────────────────────────────────────────── (ContractOf-Set, impl) - Γ ⊢ ContractOf Reads fn ⇒ TSet Unknown - Γ ⊢ ContractOf Modifies fn ⇒ TSet Unknown -``` +$$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Reads}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{Modifies}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown}} \quad \text{([⇒] ContractOf-Set)}` `Precondition` and `PostCondition` are propositions, hence {name Strata.Laurel.HighType.TBool}`TBool`. `Reads` and `Modifies` are sets of heap-allocated @@ -703,11 +516,7 @@ locations — composite/datatype references and fields. The element type is left {name Strata.Laurel.HighType.Unknown}`Unknown` for now since the rule doesn't yet recover it from `fn`'s declared modifies/reads clauses. -``` - fn is not a procedure reference -───────────────────────────────────────────── (ContractOf-Error, impl) - Γ ⊢ ContractOf … fn — error: "'contractOf' expected a procedure reference" -``` +$$`\frac{\mathit{fn} \text{ is not a procedure reference}}{\Gamma \vdash \mathsf{ContractOf}\;\ldots\;\mathit{fn} \rightsquigarrow \text{error: “‘contractOf’ expected a procedure reference”}} \quad \text{([⇒] ContractOf-Error)}` When `fn` doesn't resolve to a procedure (e.g. it's an arbitrary expression, or resolves to a constant/variable), the diagnostic fires and the construct synthesizes @@ -719,20 +528,11 @@ exists so resolution remains exhaustive over `StmtExpr`. ### Holes -``` -──────────────────────────── (Hole-Some, impl) - Γ ⊢ Hole d (some T) ⇒ T -``` +$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T) \Rightarrow T} \quad \text{([⇒] Hole-Some)}` -``` -───────────────────────────────── (Hole-None-Synth, impl) - Γ ⊢ Hole d none ⇒ Unknown -``` +$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-None)}` -``` -───────────────────────────────────── (Hole-None-Check, impl) - Γ ⊢ Hole d none ⇐ T ↦ Hole d (some T) -``` +$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapsto\;\; \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}` In check mode, an untyped hole records the expected type `T` on the node directly. The subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it @@ -741,7 +541,7 @@ discarding it. A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended up in synth-only positions. When that pass encounters a hole whose type was already set -(by Hole-None-Check or by a user-written `?: T`), it checks the resolution-time and +(by \[⇐\] Hole-None or by a user-written `?: T`), it checks the resolution-time and inference-time types for consistency under `~`; a disagreement fires the diagnostic *"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what would otherwise be a silent overwrite. @@ -781,7 +581,7 @@ just wasted work and a maintenance hazard. ### Shrink or remove `InferHoleTypes` `InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that -Hole-None-Check writes the expected type during resolution for holes in check-mode +\[⇐\] Hole-None writes the expected type during resolution for holes in check-mode positions, the post-pass only needs to handle holes in synth-only positions (e.g. call arguments resolved through `synthStmtExpr` instead of `checkStmtExpr`). As more constructs gain bespoke check rules, fewer holes will reach `InferHoleTypes`; eventually the pass From c544b2bc39876174b365902018a4814c77b100df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 15:30:10 -0400 Subject: [PATCH 049/128] extract typing rules out in helper functions for easier verso documentation --- Strata/Languages/Laurel/Resolution.lean | 1091 ++++++++++++++++------- 1 file changed, 791 insertions(+), 300 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 81d96adca6..80982bfa59 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -526,345 +526,836 @@ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List Hig | some (_, .constant c) => pure (c.type, []) | _ => pure ({ val := .Unknown, source := callee.source }, []) +/-! ## Typing rules + +Each typing rule from the Laurel manual is implemented as its own helper +inside the mutual block below. Helpers are grouped by section to mirror the +*Typing rules* index in `LaurelDoc.lean`: + +- Literals — `synthLitInt`, `synthLitBool`, `synthLitString`, `synthLitDecimal` +- Variables — `synthVarLocal`, `synthVarField`, `synthVarDeclare` +- Control flow — `synthIfThenElse`, `synthBlock`, `synthWhile`, `synthExit`, + `synthReturn`, `checkBlock`, `checkIfThenElse` +- Verification statements — `synthAssert`, `synthAssume` +- Assignment — `synthAssign` +- Calls — `synthStaticCall`, `synthInstanceCall` +- Primitive operations — `synthPrimitiveOp` +- Object forms — `synthNew`, `synthAsType`, `synthIsType`, `synthRefEq`, + `synthPureFieldUpdate` +- Verification expressions — `synthQuantifier`, `synthAssigned`, `synthOld`, + `synthFresh`, `synthProveBy` +- Self reference — `synthThis` +- Untyped forms — `synthAbstract`, `synthAll` +- ContractOf — `synthContractOf` +- Holes — `synthHole`, `checkHoleNone` + +The dispatch functions `synthStmtExpr` and `checkStmtExpr` simply pattern-match +on the constructor and delegate to the corresponding helper. -/ + +-- The `h : exprMd.val = .Foo args ...` parameters on the recursive helpers +-- look unused to the linter, but each one is referenced by that helper's +-- `decreasing_by` tactic to relate `sizeOf args` to `sizeOf exprMd`. +set_option linter.unusedVariables false in mutual + +-- ### Dispatch + +/-- Synth-mode resolution: resolve `e` and synthesize its `HighType`. + Each constructor delegates to its rule's helper. -/ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do - match _: exprMd with + match h_node: exprMd with | AstNode.mk expr source => - let (val', ty) ← match _: expr with + let (val', ty) ← match h_expr: expr with | .IfThenElse cond thenBr elseBr => - -- Condition is checked against TBool. The result type is TVoid when the - -- else branch is absent (statement form: the then-branch's value is - -- discarded), otherwise the then-branch's synthesized type. We don't - -- compare the two branches against each other since statement-position - -- ifs commonly mix a value branch with a TVoid branch (return/exit). - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let (thenBr', thenTy) ← synthStmtExpr thenBr - let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') - let resultTy := match elseBr with - | none => { val := .TVoid, source := source } - | some _ => thenTy - pure (.IfThenElse cond' thenBr' elseBr', resultTy) + synthIfThenElse exprMd cond thenBr elseBr (by rw [h_node]) | .Block stmts label => - -- Synth-mode block: non-last statements have their synthesized type discarded - -- (lax rule, matches Java/Python/JS expression-statement semantics). - -- The last statement's synthesized type becomes the block's type. - withScope do - let results ← stmts.mapM synthStmtExpr - let stmts' := results.map (·.1) - let lastTy := match results.getLast? with - | some (_, ty) => ty - | none => { val := .TVoid, source := source } - pure (.Block stmts' label, lastTy) + synthBlock exprMd stmts label (by rw [h_node]) | .While cond invs dec body => - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let invs' ← invs.attach.mapM (fun a => have := a.property; do - checkStmtExpr a.val { val := .TBool, source := a.val.source }) - let dec' ← dec.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') - let (body', _) ← synthStmtExpr body - pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) - | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) - | .Return val => do - -- Match the optional return value against the enclosing procedure's - -- declared outputs. `expectedReturnTypes = none` means we're not inside a - -- procedure body (e.g. resolving a constant initializer); skip the check. - let expected := (← get).expectedReturnTypes - let val' ← val.attach.mapM (fun a => have := a.property; do - match expected with - | some [singleOutput] => checkStmtExpr a.val singleOutput - | _ => let (e', _) ← synthStmtExpr a.val; pure e') - -- Arity/shape diagnostics independent of the value's own type. - match val, expected with - | none, some [] => pure () - | none, some [_] => pure () -- Dafny-style early exit - | none, some _ => pure () -- multi-output: bare return is fine - | some _, some [] => - let diag := diagnosticFromSource source - "void procedure cannot return a value" - modify fun s => { s with errors := s.errors.push diag } - | some _, some [_] => pure () -- value already checked above - | some _, some _ => - let diag := diagnosticFromSource source - "multi-output procedure cannot use 'return e'; assign to named outputs instead" - modify fun s => { s with errors := s.errors.push diag } - | _, none => pure () -- no enclosing procedure - pure (.Return val', { val := .TVoid, source := source }) - | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) - | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) - | .LiteralString v => pure (.LiteralString v, { val := .TString, source := source }) - | .LiteralDecimal v => pure (.LiteralDecimal v, { val := .TReal, source := source }) - | .Var (.Local ref) => - let ref' ← resolveRef ref source - let ty ← getVarType ref - pure (.Var (.Local ref'), ty) - | .Var (.Declare param) => - let ty' ← resolveHighType param.type - let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) - | .Assign targets value => - let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do - let ⟨vv, vs⟩ := v - match vv with - | .Local ref => - let ref' ← resolveRef ref source - pure (⟨.Local ref', vs⟩ : VariableMd) - | .Field target fieldName => - let (target', _) ← synthStmtExpr target - let fieldName' ← resolveFieldRef target' fieldName source - pure (⟨.Field target' fieldName', vs⟩ : VariableMd) - | .Declare param => - let ty' ← resolveHighType param.type - let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← synthStmtExpr value - -- Compute the target's declared type, regardless of whether it's a Local, - -- a Field, or a fresh Declare. - let targetType (t : VariableMd) : ResolveM HighTypeMd := do - match t.val with - | .Local ref => getVarType ref - | .Declare param => pure param.type - | .Field _ fieldName => getVarType fieldName - -- Skip all checks when the RHS is a statement (TVoid) — no value to assign. - if valueTy.val != HighType.TVoid then - let targetTys ← targets'.mapM targetType - -- Build the expected type from the targets' declared types: a single - -- type when there's one target, a tuple (MultiValuedExpr) otherwise. - -- This matches the shape of `valueTy`, which is itself MultiValuedExpr - -- exactly when the RHS produces multiple values. A single tuple-vs-tuple - -- check then covers both arity and per-position type mismatches in one - -- diagnostic. - let expectedTy : HighTypeMd := match targetTys with - | [single] => single - | _ => { val := .MultiValuedExpr targetTys, source := source } - checkSubtype source expectedTy valueTy - pure (.Assign targets' value', valueTy) + synthWhile exprMd cond invs dec body (by rw [h_node]) + | .Exit target => pure (synthExit target source) + | .Return val => + synthReturn exprMd source val (by rw [h_node]) + | .LiteralInt v => pure (synthLitInt v source) + | .LiteralBool v => pure (synthLitBool v source) + | .LiteralString v => pure (synthLitString v source) + | .LiteralDecimal v => pure (synthLitDecimal v source) + | .Var (.Local ref) => synthVarLocal ref source + | .Var (.Declare param) => synthVarDeclare param source | .Var (.Field target fieldName) => - let (target', _) ← synthStmtExpr target - let fieldName' ← resolveFieldRef target' fieldName source - let ty ← getVarType fieldName' - pure (.Var (.Field target' fieldName'), ty) + synthVarField exprMd target fieldName source (by rw [h_node]) + | .Assign targets value => + synthAssign exprMd targets value source (by rw [h_node]) | .PureFieldUpdate target fieldName newVal => - let (target', targetTy) ← synthStmtExpr target - let fieldName' ← resolveFieldRef target' fieldName source - let fieldTy ← getVarType fieldName' - let newVal' ← checkStmtExpr newVal fieldTy - pure (.PureFieldUpdate target' fieldName' newVal', targetTy) + synthPureFieldUpdate exprMd target fieldName newVal (by rw [h_node]) | .StaticCall callee args => - let callee' ← resolveRef callee source - (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let results ← args.mapM synthStmtExpr - let args' := results.map (·.1) - let argTypes := results.map (·.2) - let (retTy, paramTypes) ← getCallInfo callee - for ((a, aTy), paramTy) in (args'.zip argTypes).zip paramTypes do - checkSubtype a.source paramTy aTy - pure (.StaticCall callee' args', retTy) + synthStaticCall exprMd callee args source (by rw [h_node]) | .PrimitiveOp op args => - let results ← args.mapM synthStmtExpr - let args' := results.map (·.1) - let argTypes := results.map (·.2) - let resultTy := match op with - | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies - | .Lt | .Leq | .Gt | .Geq => HighType.TBool - | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT => - match argTypes.head? with - | some headTy => headTy.val - | none => HighType.TInt - | .StrConcat => HighType.TString - match op with - | .And | .Or | .AndThen | .OrElse | .Not | .Implies => - for (a, aTy) in args'.zip argTypes do - checkSubtype a.source { val := .TBool, source := a.source } aTy - | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => - let ctx := (← get).typeContext - for (a, aTy) in args'.zip argTypes do - unless isNumeric ctx aTy do - typeMismatch a.source (some expr) "expected a numeric type" aTy - | .Eq | .Neq => - match argTypes with - | [lhsTy, rhsTy] => - let ctx := (← get).typeContext - unless isConsistent ctx lhsTy rhsTy do - let diag := diagnosticFromSource source - s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" - modify fun s => { s with errors := s.errors.push diag } - | _ => pure () - | .StrConcat => - for (a, aTy) in args'.zip argTypes do - checkSubtype a.source { val := .TString, source := a.source } aTy - pure (.PrimitiveOp op args', { val := resultTy, source := source }) - | .New ref => - let ref' ← resolveRef ref source - (expected := #[.compositeType, .datatypeDefinition]) - -- If the reference resolved to the wrong kind, use Unknown type to avoid cascading errors - let s ← get - let kindOk : Bool := match s.scope.get? ref.text with - | some (_, node) => node.kind == .unresolved || - (#[ResolvedNodeKind.compositeType, .datatypeDefinition].contains node.kind) - | none => true - let ty := if kindOk then { val := HighType.UserDefined ref', source := source } - else { val := HighType.Unknown, source := source } - pure (.New ref', ty) - | .This => - let s ← get - match s.instanceTypeName with - | some typeName => - let typeId : Identifier := - match s.scope.get? typeName with - | some (uid, _) => { text := typeName, uniqueId := some uid, source := source } - | none => { text := typeName, source := source } - pure (.This, { val := .UserDefined typeId, source := source }) - | none => - let diag := diagnosticFromSource source "'this' is not allowed outside instance methods" - modify fun s => { s with errors := s.errors.push diag } - pure (.This, { val := .Unknown, source := source }) + synthPrimitiveOp exprMd expr op args source h_expr (by rw [h_node]) + | .New ref => synthNew ref source + | .This => synthThis source | .ReferenceEquals lhs rhs => - let (lhs', lhsTy) ← synthStmtExpr lhs - let (rhs', rhsTy) ← synthStmtExpr rhs - let ctx := (← get).typeContext - unless isReference ctx lhsTy do - typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy - unless isReference ctx rhsTy do - typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy - pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) + synthRefEq exprMd expr lhs rhs source h_expr (by rw [h_node]) | .AsType target ty => - let (target', _) ← synthStmtExpr target - let ty' ← resolveHighType ty - pure (.AsType target' ty', ty') + synthAsType exprMd target ty (by rw [h_node]) | .IsType target ty => - let (target', _) ← synthStmtExpr target - let ty' ← resolveHighType ty - pure (.IsType target' ty', { val := .TBool, source := source }) + synthIsType exprMd target ty source (by rw [h_node]) | .InstanceCall target callee args => - let (target', _) ← synthStmtExpr target - let callee' ← resolveRef callee source - (expected := #[.instanceProcedure, .staticProcedure]) - let results ← args.mapM synthStmtExpr - let args' := results.map (·.1) - let argTypes := results.map (·.2) - let (retTy, paramTypes) ← getCallInfo callee - -- Skip first param (self) when matching args. - let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] - for ((a, aTy), paramTy) in (args'.zip argTypes).zip callParamTypes do - checkSubtype a.source paramTy aTy - pure (.InstanceCall target' callee' args', retTy) + synthInstanceCall exprMd target callee args source (by rw [h_node]) | .Quantifier mode param trigger body => - withScope do - let paramTy' ← resolveHighType param.type - let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') - let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do - let (e', _) ← synthStmtExpr pv.val; pure e') - let body' ← checkStmtExpr body { val := .TBool, source := body.source } - pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) + synthQuantifier exprMd mode param trigger body source (by rw [h_node]) | .Assigned name => - let (name', _) ← synthStmtExpr name - pure (.Assigned name', { val := .TBool, source := source }) + synthAssigned exprMd name source (by rw [h_node]) | .Old val => - let (val', valTy) ← synthStmtExpr val - pure (.Old val', valTy) + synthOld exprMd val (by rw [h_node]) | .Fresh val => - let (val', valTy) ← synthStmtExpr val - unless isReference (← get).typeContext valTy do - typeMismatch val'.source (some expr) "expected a reference type" valTy - pure (.Fresh val', { val := .TBool, source := source }) + synthFresh exprMd expr val source h_expr (by rw [h_node]) | .Assert ⟨condExpr, summary⟩ => - let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } - pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) + synthAssert exprMd condExpr summary source (by rw [h_node]) | .Assume cond => - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - pure (.Assume cond', { val := .TVoid, source := source }) + synthAssume exprMd cond source (by rw [h_node]) | .ProveBy val proof => - let (val', valTy) ← synthStmtExpr val - let (proof', _) ← synthStmtExpr proof - pure (.ProveBy val' proof', valTy) + synthProveBy exprMd val proof (by rw [h_node]) | .ContractOf ty fn => - -- `fn` must be a direct identifier reference resolving to a procedure. - -- Anything else (arbitrary expressions, references to non-procedures) is - -- ill-formed: a contract belongs to a *named* procedure. - let (fn', _) ← synthStmtExpr fn - let s ← get - let fnIsProcRef : Bool := match fn'.val with - | .Var (.Local ref) => - match s.scope.get? ref.text with - | some (_, node) => - node.kind == .staticProcedure || - node.kind == .instanceProcedure || - node.kind == .unresolved - | none => true -- unresolved name already reported - | _ => false - unless fnIsProcRef do - let diag := diagnosticFromSource fn.source - "'contractOf' expected a procedure reference" - modify fun s => { s with errors := s.errors.push diag } - -- Result type: Bool for pre/postconditions, set of heap references for - -- reads/modifies. The element type of the set is left as Unknown for now - -- since the rule doesn't recover it from `fn`. - let resultTy : HighType := match ty with - | .Precondition | .PostCondition => .TBool - | .Reads | .Modifies => .TSet { val := .Unknown, source := none } - pure (.ContractOf ty fn', { val := resultTy, source := source }) - | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) - | .All => pure (.All, { val := .Unknown, source := source }) - | .Hole det type => match type with - | some ty => - let ty' ← resolveHighType ty - pure (.Hole det ty', ty') - | none => pure (.Hole det none, { val := .Unknown, source := source }) + synthContractOf exprMd ty fn source (by rw [h_node]) + | .Abstract => pure (synthAbstract source) + | .All => pure (synthAll source) + | .Hole det type => synthHole det type source return ({ val := val', source := source }, ty) - termination_by (exprMd, 0) + termination_by (exprMd, 2) decreasing_by all_goals first | (apply Prod.Lex.left; term_by_mem) + | (try subst h_node; apply Prod.Lex.right; decide) | (apply Prod.Lex.right; decide) -/-- Check-mode resolution: resolve `e` and verify its type is a consistent - subtype of `expected`. Bidirectional rules for individual constructs push - `expected` into subexpressions; everything else falls back to subsumption - (synth, then `isConsistentSubtype actual expected`). -/ +/-- Check-mode resolution (rule **Sub** at the boundary): resolve `e` and + verify its type is a consistent subtype of `expected`. Bidirectional rules + for individual constructs push `expected` into subexpressions; everything + else falls back to subsumption (synth, then `isConsistentSubtype actual + expected`). -/ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do - match _: exprMd with + match h_node: exprMd with | AstNode.mk expr source => - match _: expr with + match h_expr: expr with | .Block stmts label => - -- Bespoke check rule: discard non-last statement types (lax), push - -- `expected` into the last statement. Empty block reduces to subsumption - -- of TVoid against `expected`. - withScope do - let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do - have : s ∈ stmts := List.dropLast_subset stmts hMem - let (s', _) ← synthStmtExpr s; pure s') - match _lastResult: stmts.getLast? with - | none => - checkSubtype source expected { val := .TVoid, source := source } - pure { val := .Block init' label, source := source } - | some last => - have := List.mem_of_getLast? _lastResult - let last' ← checkStmtExpr last expected - pure { val := .Block (init' ++ [last']) label, source := source } + checkBlock exprMd stmts label expected source (by rw [h_node]) | .IfThenElse cond thenBr elseBr => - -- Push `expected` into both branches (rather than going through the synth - -- rule + Sub at the boundary). Without an else branch, fall back to - -- subsumption of TVoid against `expected`. - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let thenBr' ← checkStmtExpr thenBr expected - let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) - if elseBr.isNone then - checkSubtype source expected { val := .TVoid, source := source } - pure { val := .IfThenElse cond' thenBr' elseBr', source := source } - | .Hole det none => - -- Untyped hole in check mode: record the expected type on the node so - -- downstream passes don't have to infer it again. Subsumption is trivial - -- (Unknown <: T always holds). - pure { val := .Hole det (some expected), source := source } + checkIfThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) + | .Hole det none => pure (checkHoleNone det expected source) | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd checkSubtype source expected actual pure e' - termination_by (exprMd, 1) + termination_by (exprMd, 3) decreasing_by all_goals first | (apply Prod.Lex.left; term_by_mem) | (try subst_eqs; apply Prod.Lex.right; decide) + | (try subst h_node; apply Prod.Lex.right; decide) + | (apply Prod.Lex.right; decide) + +-- ### Literals + +/-- Rule **Lit-Int**: `Γ ⊢ LiteralInt n ⇒ TInt`. -/ +def synthLitInt (v : Int) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralInt v, { val := .TInt, source := source }) + +/-- Rule **Lit-Bool**: `Γ ⊢ LiteralBool b ⇒ TBool`. -/ +def synthLitBool (v : Bool) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralBool v, { val := .TBool, source := source }) + +/-- Rule **Lit-String**: `Γ ⊢ LiteralString s ⇒ TString`. -/ +def synthLitString (v : String) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralString v, { val := .TString, source := source }) + +/-- Rule **Lit-Decimal**: `Γ ⊢ LiteralDecimal d ⇒ TReal`. -/ +def synthLitDecimal (v : Decimal) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralDecimal v, { val := .TReal, source := source }) + +-- ### Variables + +/-- Rule **Var-Local**: `Γ(x) = T ⊢ Var (.Local x) ⇒ T`. Resolves `ref` against + the lexical scope and reads its declared type. -/ +def synthVarLocal (ref : Identifier) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let ref' ← resolveRef ref source + let ty ← getVarType ref + pure (.Var (.Local ref'), ty) + +/-- Rule **Var-Declare**: extends the surrounding scope with `x : T` and + synthesizes `TVoid` (the declaration itself produces no value). -/ +def synthVarDeclare (param : Parameter) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let ty' ← resolveHighType param.type + let name' ← defineNameCheckDup param.name (.var param.name ty') + pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) + +/-- Rule **Var-Field**: `Γ ⊢ e ⇒ _, Γ(f) = T_f ⊢ Var (.Field e f) ⇒ T_f`. + `f` is looked up against the type of `e` (or the enclosing instance type + for `self.f`); the typing rule itself is path-agnostic. -/ +def synthVarField (exprMd : StmtExprMd) + (target : StmtExprMd) (fieldName : Identifier) (source : Option FileRange) + (h : exprMd.val = .Var (.Field target fieldName)) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName source + let ty ← getVarType fieldName' + pure (.Var (.Field target' fieldName'), ty) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +-- ### Control flow + +/-- Rules **If-NoElse** / **If-Synth**: `cond` is checked against `TBool`. + With no else branch, the construct is a statement — `thenBr` is checked + against `TVoid` and the result is `TVoid`, so `if c then 5` is rejected. + With an else branch, the then-branch's synthesized type is returned; the + two branches are *not* compared against each other, since a statement- + position `if` often pairs a value branch with `return`/`exit`/`assert`. -/ +def synthIfThenElse (exprMd : StmtExprMd) + (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) + (h : exprMd.val = .IfThenElse cond thenBr elseBr) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let voidTy : HighTypeMd := { val := .TVoid, source := exprMd.source } + match elseBr with + | none => + let thenBr' ← checkStmtExpr thenBr voidTy + pure (.IfThenElse cond' thenBr' none, voidTy) + | some e => + let (thenBr', thenTy) ← synthStmtExpr thenBr + let (elseBr', _) ← synthStmtExpr e + pure (.IfThenElse cond' thenBr' (some elseBr'), thenTy) + termination_by (exprMd, 1) + decreasing_by + all_goals first + | (apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + try omega) + | (apply Prod.Lex.right; decide) + +/-- Rules **Block-Synth** / **Block-Synth-Empty**: non-last statements are + synthesized but their types discarded (the lax rule, matching + Java/Python/JS expression-statement semantics); the last statement's type + becomes the block's type, or `TVoid` for an empty block. The block opens + a fresh nested scope. -/ +def synthBlock (exprMd : StmtExprMd) + (stmts : List StmtExprMd) (label : Option String) + (h : exprMd.val = .Block stmts label) : + ResolveM (StmtExpr × HighTypeMd) := do + withScope do + let results ← stmts.mapM synthStmtExpr + let stmts' := results.map (·.1) + let lastTy := match results.getLast? with + | some (_, ty) => ty + | none => { val := .TVoid, source := exprMd.source } + pure (.Block stmts' label, lastTy) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + have := List.sizeOf_lt_of_mem ‹_ ∈ stmts› + omega + +/-- Rule **While**: `cond ⇐ TBool`, each invariant `⇐ TBool`, optional + `decreases` is resolved without a type check (intended target is numeric), + body is synthesized; the construct itself synthesizes `TVoid`. -/ +def synthWhile (exprMd : StmtExprMd) + (cond : StmtExprMd) (invs : List StmtExprMd) + (dec : Option StmtExprMd) (body : StmtExprMd) + (h : exprMd.val = .While cond invs dec body) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let invs' ← invs.attach.mapM (fun a => have := a.property; do + checkStmtExpr a.val { val := .TBool, source := a.val.source }) + let dec' ← dec.attach.mapM (fun a => have := a.property; do + let (e', _) ← synthStmtExpr a.val; pure e') + let (body', _) ← synthStmtExpr body + pure (.While cond' invs' dec' body', { val := .TVoid, source := exprMd.source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try (have := List.sizeOf_lt_of_mem ‹_ ∈ invs›) + try simp_all + omega + +/-- Rule **Exit**: `Γ ⊢ Exit target ⇒ TVoid`. -/ +def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.Exit target, { val := .TVoid, source := source }) + +/-- Rules **Return-None** / **Return-Some** / **Return-Void-Error** / + **Return-Multi-Error**: matches the optional return value against the + enclosing procedure's declared outputs (`expectedReturnTypes`). `none` + means "no enclosing procedure" — e.g. resolving a constant initializer — + and skips all `Return` checks. -/ +def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) + (val : Option StmtExprMd) + (h : exprMd.val = .Return val) : + ResolveM (StmtExpr × HighTypeMd) := do + let expected := (← get).expectedReturnTypes + let val' ← val.attach.mapM (fun a => have := a.property; do + match expected with + | some [singleOutput] => checkStmtExpr a.val singleOutput + | _ => let (e', _) ← synthStmtExpr a.val; pure e') + -- Arity/shape diagnostics independent of the value's own type. + match val, expected with + | none, some [] => pure () + | none, some [_] => pure () -- Dafny-style early exit + | none, some _ => pure () -- multi-output: bare return is fine + | some _, some [] => + let diag := diagnosticFromSource source + "void procedure cannot return a value" + modify fun s => { s with errors := s.errors.push diag } + | some _, some [_] => pure () -- value already checked above + | some _, some _ => + let diag := diagnosticFromSource source + "multi-output procedure cannot use 'return e'; assign to named outputs instead" + modify fun s => { s with errors := s.errors.push diag } + | _, none => pure () -- no enclosing procedure + pure (.Return val', { val := .TVoid, source := source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + simp_all + omega + +/-- Rules **Block-Check** / **Block-Check-Empty**: pushes `expected` into the + *last* statement rather than comparing the block's synthesized type at the + boundary. Errors fire at the offending subexpression, and `T` keeps + propagating through nested `Block` / `IfThenElse` / `Hole` / `Quantifier`. + Empty blocks reduce to a subsumption check of `TVoid` against `expected`. -/ +def checkBlock (exprMd : StmtExprMd) + (stmts : List StmtExprMd) (label : Option String) + (expected : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .Block stmts label) : ResolveM StmtExprMd := do + withScope do + let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do + have : s ∈ stmts := List.dropLast_subset stmts hMem + let (s', _) ← synthStmtExpr s; pure s') + match _lastResult: stmts.getLast? with + | none => + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .Block init' label, source := source } + | some last => + have := List.mem_of_getLast? _lastResult + let last' ← checkStmtExpr last expected + pure { val := .Block (init' ++ [last']) label, source := source } + termination_by (exprMd, 0) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try (have := List.sizeOf_lt_of_mem ‹_ ∈ stmts›) + try simp_all + omega + +/-- Rules **If-Check** / **If-Check-NoElse**: pushes `expected` into both + branches (rather than going through If-Synth + Sub at the boundary). + Errors fire at the offending branch instead of the surrounding `if`. + Without an else branch, the construct can only succeed when `T` admits + `TVoid`. -/ +def checkIfThenElse (exprMd : StmtExprMd) + (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) + (expected : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .IfThenElse cond thenBr elseBr) : ResolveM StmtExprMd := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let thenBr' ← checkStmtExpr thenBr expected + let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) + if elseBr.isNone then + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .IfThenElse cond' thenBr' elseBr', source := source } + termination_by (exprMd, 0) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +-- ### Verification statements + +/-- Rule **Assert**: `cond` is checked against `TBool`; the construct + synthesizes `TVoid`. -/ +def synthAssert (exprMd : StmtExprMd) + (condExpr : StmtExprMd) (summary : Option String) (source : Option FileRange) + (h : exprMd.val = .Assert ⟨condExpr, summary⟩) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } + pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +/-- Rule **Assume**: `cond` is checked against `TBool`; the construct + synthesizes `TVoid`. -/ +def synthAssume (exprMd : StmtExprMd) + (cond : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Assume cond) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + pure (.Assume cond', { val := .TVoid, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +-- ### Assignment + +/-- Rule **Assign**: each target's declared type `T_i` (from `Local`, + `Field`, or fresh `Declare`) is collapsed into a tuple `ExpectedTy` + (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) + and checked against the RHS's synthesized type. When the RHS is a + statement (`TVoid`) — `while`, `return`, … — all checks are skipped: + there's no value to assign. -/ +def synthAssign (exprMd : StmtExprMd) + (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Assign targets value) : + ResolveM (StmtExpr × HighTypeMd) := do + let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do + let ⟨vv, vs⟩ := v + match vv with + | .Local ref => + let ref' ← resolveRef ref source + pure (⟨.Local ref', vs⟩ : VariableMd) + | .Field target fieldName => + let (target', _) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName source + pure (⟨.Field target' fieldName', vs⟩ : VariableMd) + | .Declare param => + let ty' ← resolveHighType param.type + let name' ← defineNameCheckDup param.name (.var param.name ty') + pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) + let (value', valueTy) ← synthStmtExpr value + let targetType (t : VariableMd) : ResolveM HighTypeMd := do + match t.val with + | .Local ref => getVarType ref + | .Declare param => pure param.type + | .Field _ fieldName => getVarType fieldName + if valueTy.val != HighType.TVoid then + let targetTys ← targets'.mapM targetType + let expectedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + checkSubtype source expectedTy valueTy + pure (.Assign targets' value', valueTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) + omega + +-- ### Calls + +/-- Rules **Static-Call** / **Static-Call-Multi**: callee is resolved against + the expected kinds (parameter, static procedure, datatype constructor, + constant); each argument is synthesized and checked against the + corresponding parameter type. The result type is the (possibly + multi-valued) declared output type from `getCallInfo`. -/ +def synthStaticCall (exprMd : StmtExprMd) + (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .StaticCall callee args) : + ResolveM (StmtExpr × HighTypeMd) := do + let callee' ← resolveRef callee source + (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) + let results ← args.mapM synthStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + for ((a, aTy), paramTy) in (args'.zip argTypes).zip paramTypes do + checkSubtype a.source paramTy aTy + pure (.StaticCall callee' args', retTy) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + have := List.sizeOf_lt_of_mem ‹_ ∈ args› + omega + +/-- Rule **Instance-Call**: target is synthesized; callee resolves to an + instance or static procedure; arguments are checked pairwise against the + callee's parameter types after dropping `self`. -/ +def synthInstanceCall (exprMd : StmtExprMd) + (target : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) + (source : Option FileRange) + (h : exprMd.val = .InstanceCall target callee args) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let callee' ← resolveRef callee source + (expected := #[.instanceProcedure, .staticProcedure]) + let results ← args.mapM synthStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] + for ((a, aTy), paramTy) in (args'.zip argTypes).zip callParamTypes do + checkSubtype a.source paramTy aTy + pure (.InstanceCall target' callee' args', retTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try (have := List.sizeOf_lt_of_mem ‹_ ∈ args›) + try simp_all + omega + +-- ### Primitive operations + +/-- Rules **Op-Bool** / **Op-Cmp** / **Op-Eq** / **Op-Arith** / **Op-Concat**: + each operator family has its own argument-type discipline and result + type. Arguments are synthesized first, then the per-family check fires + (`⇐ TBool` for booleans, `Numeric` for arithmetic/comparison, consistency + `~` for equality, `⇐ TString` for concatenation). The result type is + `TBool` for booleans/comparisons/equality, the head argument's type for + arithmetic, `TString` for concatenation. -/ +def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) + (op : Operation) (args : List StmtExprMd) (source : Option FileRange) + (h_expr : expr = .PrimitiveOp op args) + (h : exprMd.val = .PrimitiveOp op args) : + ResolveM (StmtExpr × HighTypeMd) := do + let _ := h_expr -- carries the constructor identity for `expr` in diagnostics + let results ← args.mapM synthStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let resultTy := match op with + | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies + | .Lt | .Leq | .Gt | .Geq => HighType.TBool + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT => + match argTypes.head? with + | some headTy => headTy.val + | none => HighType.TInt + | .StrConcat => HighType.TString + match op with + | .And | .Or | .AndThen | .OrElse | .Not | .Implies => + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TBool, source := a.source } aTy + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => + let ctx := (← get).typeContext + for (a, aTy) in args'.zip argTypes do + unless isNumeric ctx aTy do + typeMismatch a.source (some expr) "expected a numeric type" aTy + | .Eq | .Neq => + match argTypes with + | [lhsTy, rhsTy] => + let ctx := (← get).typeContext + unless isConsistent ctx lhsTy rhsTy do + let diag := diagnosticFromSource source + s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + modify fun s => { s with errors := s.errors.push diag } + | _ => pure () + | .StrConcat => + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TString, source := a.source } aTy + pure (.PrimitiveOp op args', { val := resultTy, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + have := List.sizeOf_lt_of_mem ‹_ ∈ args› + omega + +-- ### Object forms + +/-- Rules **New-Ok** / **New-Fallback**: when `ref` resolves to a composite or + datatype, the type is `UserDefined ref`; otherwise `Unknown` (suppresses + cascading errors after the kind diagnostic has already fired). -/ +def synthNew (ref : Identifier) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let ref' ← resolveRef ref source + (expected := #[.compositeType, .datatypeDefinition]) + let s ← get + let kindOk : Bool := match s.scope.get? ref.text with + | some (_, node) => node.kind == .unresolved || + (#[ResolvedNodeKind.compositeType, .datatypeDefinition].contains node.kind) + | none => true + let ty := if kindOk then { val := HighType.UserDefined ref', source := source } + else { val := HighType.Unknown, source := source } + pure (.New ref', ty) + +/-- Rule **AsType**: `target` is resolved but not checked against `T` — the + cast is the user's claim. The synthesized type is `T`. -/ +def synthAsType (exprMd : StmtExprMd) + (target : StmtExprMd) (ty : HighTypeMd) + (h : exprMd.val = .AsType target ty) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let ty' ← resolveHighType ty + pure (.AsType target' ty', ty') + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **IsType**: `target` is resolved; the synthesized type is `TBool`. -/ +def synthIsType (exprMd : StmtExprMd) + (target : StmtExprMd) (ty : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .IsType target ty) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let ty' ← resolveHighType ty + pure (.IsType target' ty', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **RefEq**: both operands must be reference types (`UserDefined` or + `Unknown`). Reference equality is meaningless on primitives. -/ +def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) + (lhs rhs : StmtExprMd) (source : Option FileRange) + (h_expr : expr = .ReferenceEquals lhs rhs) + (h : exprMd.val = .ReferenceEquals lhs rhs) : + ResolveM (StmtExpr × HighTypeMd) := do + let _ := h_expr + let (lhs', lhsTy) ← synthStmtExpr lhs + let (rhs', rhsTy) ← synthStmtExpr rhs + let ctx := (← get).typeContext + unless isReference ctx lhsTy do + typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy + unless isReference ctx rhsTy do + typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy + pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **PureFieldUpdate**: `target` is synthesized, `f` resolved against + `T_t` (or the enclosing instance type), and `newVal` checked against the + field's declared type. The synthesized type is `T_t` — updating a field + on a pure type produces a new value of the same type. -/ +def synthPureFieldUpdate (exprMd : StmtExprMd) + (target : StmtExprMd) (fieldName : Identifier) (newVal : StmtExprMd) + (h : exprMd.val = .PureFieldUpdate target fieldName newVal) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', targetTy) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName target.source + let fieldTy ← getVarType fieldName' + let newVal' ← checkStmtExpr newVal fieldTy + pure (.PureFieldUpdate target' fieldName' newVal', targetTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +-- ### Verification expressions + +/-- Rule **Quantifier**: opens a fresh scope, binds `x : T`, resolves the + optional trigger, and checks the body against `TBool`. The construct + itself synthesizes `TBool` since a quantifier is a proposition. -/ +def synthQuantifier (exprMd : StmtExprMd) + (mode : QuantifierMode) (param : Parameter) + (trigger : Option StmtExprMd) (body : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Quantifier mode param trigger body) : + ResolveM (StmtExpr × HighTypeMd) := do + withScope do + let paramTy' ← resolveHighType param.type + let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') + let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do + let (e', _) ← synthStmtExpr pv.val; pure e') + let body' ← checkStmtExpr body { val := .TBool, source := body.source } + pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +/-- Rule **Assigned**: `name` is synthesized; the construct synthesizes + `TBool`. -/ +def synthAssigned (exprMd : StmtExprMd) + (name : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Assigned name) : + ResolveM (StmtExpr × HighTypeMd) := do + let (name', _) ← synthStmtExpr name + pure (.Assigned name', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **Old**: `Γ ⊢ v ⇒ T ⊢ Old v ⇒ T`. -/ +def synthOld (exprMd : StmtExprMd) + (val : StmtExprMd) + (h : exprMd.val = .Old val) : + ResolveM (StmtExpr × HighTypeMd) := do + let (val', valTy) ← synthStmtExpr val + pure (.Old val', valTy) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **Fresh**: `v` is synthesized and must have a reference type + (`UserDefined` or `Unknown`). The construct itself synthesizes `TBool`. -/ +def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) + (val : StmtExprMd) (source : Option FileRange) + (h_expr : expr = .Fresh val) + (h : exprMd.val = .Fresh val) : + ResolveM (StmtExpr × HighTypeMd) := do + let _ := h_expr + let (val', valTy) ← synthStmtExpr val + unless isReference (← get).typeContext valTy do + typeMismatch val'.source (some expr) "expected a reference type" valTy + pure (.Fresh val', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **ProveBy**: `v` and `proof` are both synthesized; the construct's + type is `v`'s type — `proof` is a hint for downstream verification. -/ +def synthProveBy (exprMd : StmtExprMd) + (val proof : StmtExprMd) + (h : exprMd.val = .ProveBy val proof) : + ResolveM (StmtExpr × HighTypeMd) := do + let (val', valTy) ← synthStmtExpr val + let (proof', _) ← synthStmtExpr proof + pure (.ProveBy val' proof', valTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +-- ### Self reference + +/-- Rules **This-Inside** / **This-Outside**: when `instanceTypeName` is set + (we're inside an instance method), `This` synthesizes `UserDefined T`; + otherwise an error is emitted and the type collapses to `Unknown`. -/ +def synthThis (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let s ← get + match s.instanceTypeName with + | some typeName => + let typeId : Identifier := + match s.scope.get? typeName with + | some (uid, _) => { text := typeName, uniqueId := some uid, source := source } + | none => { text := typeName, source := source } + pure (.This, { val := .UserDefined typeId, source := source }) + | none => + let diag := diagnosticFromSource source "'this' is not allowed outside instance methods" + modify fun s => { s with errors := s.errors.push diag } + pure (.This, { val := .Unknown, source := source }) + +-- ### Untyped forms + +/-- Rule **Abstract**: synthesizes `Unknown`. -/ +def synthAbstract (source : Option FileRange) : StmtExpr × HighTypeMd := + (.Abstract, { val := .Unknown, source := source }) + +/-- Rule **All**: synthesizes `Unknown`. -/ +def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := + (.All, { val := .Unknown, source := source }) + +-- ### ContractOf + +/-- Rules **ContractOf-Bool** / **ContractOf-Set** / **ContractOf-Error**: + `fn` must be a direct identifier reference resolving to a procedure; + anything else is ill-formed (a contract belongs to a *named* procedure). + Pre/postconditions are propositions (`TBool`); reads/modifies are sets of + heap references with element type `Unknown` for now. -/ +def synthContractOf (exprMd : StmtExprMd) + (ty : ContractType) (fn : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .ContractOf ty fn) : + ResolveM (StmtExpr × HighTypeMd) := do + let (fn', _) ← synthStmtExpr fn + let s ← get + let fnIsProcRef : Bool := match fn'.val with + | .Var (.Local ref) => + match s.scope.get? ref.text with + | some (_, node) => + node.kind == .staticProcedure || + node.kind == .instanceProcedure || + node.kind == .unresolved + | none => true -- unresolved name already reported + | _ => false + unless fnIsProcRef do + let diag := diagnosticFromSource fn.source + "'contractOf' expected a procedure reference" + modify fun s => { s with errors := s.errors.push diag } + let resultTy : HighType := match ty with + | .Precondition | .PostCondition => .TBool + | .Reads | .Modifies => .TSet { val := .Unknown, source := none } + pure (.ContractOf ty fn', { val := resultTy, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +-- ### Holes + +/-- Rules **Hole-Some** / **Hole-None-Synth**: a typed hole synthesizes its + annotation; an untyped hole in synth position synthesizes `Unknown`. -/ +def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + match type with + | some ty => + let ty' ← resolveHighType ty + pure (.Hole det ty', ty') + | none => pure (.Hole det none, { val := .Unknown, source := source }) + +/-- Rule **Hole-None-Check**: an untyped hole in check mode records the + expected type on the node so downstream passes don't have to infer it + again. The subsumption check is trivial (`Unknown <: T` always holds), so + this rule never fails — it just preserves the type information available + at the check-mode boundary. -/ +def checkHoleNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : + StmtExprMd := + { val := .Hole det (some expected), source := source } + end /-- Resolve a statement expression, discarding the synthesized type. From b0ffaf5ec299044ca665c55105fc67863c7795e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 15:30:24 -0400 Subject: [PATCH 050/128] better if-then-else typing discipline --- docs/verso/LaurelDoc.lean | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 140a250407..a53f161f60 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -302,11 +302,12 @@ remainder of the enclosing scope. ### Control flow -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow \mathsf{TVoid}}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no -value when `cond` is false; without this, `x : int := if c then 5` would type-check -spuriously. +value when `cond` is false; the then-branch is checked against +{name Strata.Laurel.HighType.TVoid}`TVoid` so `x : int := if c then 5` is rejected at the +branch rather than slipping through to a downstream subsumption. $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t} \quad \text{([⇒] If)}` From 887c8889e57b33ffcd420543bcaf6f4763bd00b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 16:38:23 -0400 Subject: [PATCH 051/128] if then else type synthesis --- Strata/Languages/Laurel/Laurel.lean | 37 +++++++++++++++++++ Strata/Languages/Laurel/Resolution.lean | 15 +++++--- .../Laurel/ResolutionTypeCheckTests.lean | 21 +++++++++++ docs/verso/LaurelDoc.lean | 15 +++++--- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index ff67dafe1b..4dd9b7a0f9 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -558,6 +558,43 @@ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := def isConsistentSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := isConsistent ctx sub sup || isSubtype ctx sub sup +/-- BFS through `extendingMap` starting from `name` and stopping at the first + type that is also in `targetAncestors`. Used by `joinTypes` to find a + common ancestor between two composites; `visited` cuts off cycles. -/ +partial def TypeContext.firstCommonAncestor (ctx : TypeContext) + (name : String) (targetAncestors : Std.HashSet String) : Option String := + let rec go (frontier : List String) (visited : Std.HashSet String) : Option String := + match frontier with + | [] => none + | n :: rest => + if visited.contains n then go rest visited + else if targetAncestors.contains n then some n + else + let parents := (ctx.extendingMap.get? n).getD [] + go (rest ++ parents) (visited.insert n) + go [name] {} + +/-- Least upper bound for the if-then-else synthesis rule. When `a` and `b` + are subtype-related, returns the larger; for unrelated composites, walks + `extending` chains for the first common ancestor. When no common + supertype exists (e.g. unrelated primitives, or a value branch paired + with a `TVoid` `return`/`exit`), falls back to `a` — the enclosing + context's `checkSubtype` then surfaces any mismatch against the + then-branch's type, preserving the historical statement-form behavior. -/ +def joinTypes (ctx : TypeContext) (a b : HighTypeMd) : HighTypeMd := + if isConsistentSubtype ctx a b then b + else if isConsistentSubtype ctx b a then a + else + let a' := ctx.unfold a + let b' := ctx.unfold b + match a'.val, b'.val with + | .UserDefined aName, .UserDefined bName => + match ctx.firstCommonAncestor aName.text (ctx.ancestors bName.text) with + | some name => + { val := .UserDefined { text := name, source := none }, source := a.source } + | none => a + | _, _ => a + def HighType.isBool : HighType → Bool | TBool => true | _ => false diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 80982bfa59..75bbbabb0d 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -713,9 +713,13 @@ def synthVarField (exprMd : StmtExprMd) /-- Rules **If-NoElse** / **If-Synth**: `cond` is checked against `TBool`. With no else branch, the construct is a statement — `thenBr` is checked against `TVoid` and the result is `TVoid`, so `if c then 5` is rejected. - With an else branch, the then-branch's synthesized type is returned; the - two branches are *not* compared against each other, since a statement- - position `if` often pairs a value branch with `return`/`exit`/`assert`. -/ + With an else branch, the result type is the join (LUB) of the two + branches' synthesized types, so `if c then new Left else new Right` + synthesizes the common ancestor `Top` rather than committing to one + branch arbitrarily. When no common supertype exists (e.g. a value branch + paired with a `TVoid` `return`/`exit`), `joinTypes` falls back to the + then-branch's type and the enclosing context's check surfaces any + mismatch downstream. -/ def synthIfThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : @@ -728,8 +732,9 @@ def synthIfThenElse (exprMd : StmtExprMd) pure (.IfThenElse cond' thenBr' none, voidTy) | some e => let (thenBr', thenTy) ← synthStmtExpr thenBr - let (elseBr', _) ← synthStmtExpr e - pure (.IfThenElse cond' thenBr' (some elseBr'), thenTy) + let (elseBr', elseTy) ← synthStmtExpr e + let ctx := (← get).typeContext + pure (.IfThenElse cond' thenBr' (some elseBr'), joinTypes ctx thenTy elseTy) termination_by (exprMd, 1) decreasing_by all_goals first diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index b78f3b22df..c674bf0fe4 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -194,4 +194,25 @@ procedure test() opaque { #guard_msgs (error, drop all) in #eval testInputWithOffset "UserDefinedCrossType" userDefinedCrossType 170 processResolution +/-! ## If-then-else branch join + +When the two branches have different but subtype-related types, the construct +synthesizes their join (least upper bound) — not the then-branch arbitrarily. +So `if c then new Left else new Right`, with `Left, Right <: Top`, synthesizes +`Top` and an assignment to a `Left`-typed variable is rejected. -/ + +def ifBranchJoinToCommonAncestor := r" +composite Top { } +composite Left extends Top { } +composite Right extends Top { } +procedure test(c: bool) opaque { + var x: Top := if c then new Left else new Right; + var y: Left := if c then new Left else new Right +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'Left', got 'Top' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "IfBranchJoinToCommonAncestor" ifBranchJoinToCommonAncestor 198 processResolution + end Laurel diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index a53f161f60..e87db76d31 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -309,12 +309,15 @@ value when `cond` is false; the then-branch is checked against {name Strata.Laurel.HighType.TVoid}`TVoid` so `x : int := if c then 5` is rejected at the branch rather than slipping through to a downstream subsumption. -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t} \quad \text{([⇒] If)}` - -Picks the then-branch type arbitrarily; the two branches are *not* compared, since a -statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The -enclosing context's check (\[⇐\] Sub, or a containing `checkSubtype` like an assignment) -provides the actual check downstream. +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If)}` + +The result is the join (least upper bound) of the two branch types, so +`if c then small else big` synthesizes the common supertype rather than committing to one +branch arbitrarily. The join walks `extending` chains for composites; when no common +supertype exists (e.g. a value branch paired with a `TVoid` `return`/`exit`), it falls +back to `T_t` and the enclosing context's check (\[⇐\] Sub, or a containing +`checkSubtype` like an assignment) surfaces any mismatch downstream against the +then-branch's type. $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` From 8ad24ac4c8fae712afd7ff546b2a4bcd7f246611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 16:40:58 -0400 Subject: [PATCH 052/128] move test to appropriate location --- .../Examples/Objects/T9_IfBranchJoin.lean | 35 +++++++++++++++++++ .../Laurel/ResolutionTypeCheckTests.lean | 21 ----------- 2 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean b/StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean new file mode 100644 index 0000000000..9149d2e647 --- /dev/null +++ b/StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean @@ -0,0 +1,35 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +import StrataTest.Util.TestDiagnostics +import StrataTest.Languages.Laurel.TestExamples + +open StrataTest.Util + +namespace Strata +namespace Laurel + +/- +When the two branches of an `if/else` have different but subtype-related +types, the construct synthesizes their join (least upper bound) — not the +then-branch arbitrarily. So `if c then new Left else new Right`, with +`Left, Right <: Top`, synthesizes `Top`. Storing it in a `Top`-typed +variable succeeds, but storing it in a `Left`-typed variable is rejected. +-/ + +def program := r" +composite Top { } +composite Left extends Top { } +composite Right extends Top { } +procedure test(c: bool) opaque { + var x: Top := if c then new Left else new Right; + var y: Left := if c then new Left else new Right +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'Left', got 'Top' +}; +" + +#guard_msgs (drop info) in +#eval testInputWithOffset "IfBranchJoin" program 22 processLaurelFile diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index c674bf0fe4..b78f3b22df 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -194,25 +194,4 @@ procedure test() opaque { #guard_msgs (error, drop all) in #eval testInputWithOffset "UserDefinedCrossType" userDefinedCrossType 170 processResolution -/-! ## If-then-else branch join - -When the two branches have different but subtype-related types, the construct -synthesizes their join (least upper bound) — not the then-branch arbitrarily. -So `if c then new Left else new Right`, with `Left, Right <: Top`, synthesizes -`Top` and an assignment to a `Left`-typed variable is rejected. -/ - -def ifBranchJoinToCommonAncestor := r" -composite Top { } -composite Left extends Top { } -composite Right extends Top { } -procedure test(c: bool) opaque { - var x: Top := if c then new Left else new Right; - var y: Left := if c then new Left else new Right -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'Left', got 'Top' -}; -" - -#guard_msgs (error, drop all) in -#eval testInputWithOffset "IfBranchJoinToCommonAncestor" ifBranchJoinToCommonAncestor 198 processResolution - end Laurel From 4f7b6bac817e0971394d32da2c8bd972680f6b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 17:03:17 -0400 Subject: [PATCH 053/128] very strict dereference comparison --- Strata/Languages/Laurel/Resolution.lean | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 75bbbabb0d..01443dee39 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -1152,6 +1152,10 @@ def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy unless isReference ctx rhsTy do typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy + unless isConsistent ctx lhsTy rhsTy do + let diag := diagnosticFromSource source + s!"'{expr.constrName}' operands have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + modify fun s => { s with errors := s.errors.push diag } pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) termination_by (exprMd, 1) decreasing_by From 59a846173e03efe44915ceee1cb3fceb038f4d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 17:04:00 -0400 Subject: [PATCH 054/128] consistent references when comparing --- docs/verso/LaurelDoc.lean | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index e87db76d31..34b5772920 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -453,14 +453,15 @@ $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsT $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` -$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` +$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r \quad T_l \sim T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` `isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` or {name Strata.Laurel.HighType.Unknown}`Unknown` -type. Reference equality is meaningless on primitives. Compatibility between `T_l` and -`T_r` (e.g. rejecting `Cat === Dog` for unrelated user-defined types) is delegated to -future tightening of `<:` — today, two distinct user-defined names already mismatch -structurally, so the check would only fire under stronger subtyping. +type. Reference equality is meaningless on primitives. The operands must also be +consistent under `~` (Siek–Taha consistency), matching the rule applied by +{name Strata.Laurel.Operation.Eq}`==`: two distinct user-defined types like `Cat` and +`Dog` are rejected, while either side being `Unknown` is accepted as a gradual escape +hatch. $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` From 8a950f8d97c497fea2ea9421626cbf6ddf274ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 09:33:07 -0400 Subject: [PATCH 055/128] fix assign by creating a checking rule --- Strata/Languages/Laurel/Resolution.lean | 56 ++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 01443dee39..ddaf0df040 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -640,6 +640,8 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE checkBlock exprMd stmts label expected source (by rw [h_node]) | .IfThenElse cond thenBr elseBr => checkIfThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) + | .Assign targets value => + checkAssign exprMd targets value expected source (by rw [h_node]) | .Hole det none => pure (checkHoleNone det expected source) | _ => -- Subsumption fallback: synth then check `actual <: expected`. @@ -933,7 +935,10 @@ def synthAssume (exprMd : StmtExprMd) (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) and checked against the RHS's synthesized type. When the RHS is a statement (`TVoid`) — `while`, `return`, … — all checks are skipped: - there's no value to assign. -/ + there's no value to assign. The construct synthesizes the RHS's type, + so that expression-position assignments like `x ++ (y := s)` see a + string in the second operand; statement-position uses are accommodated + by `checkAssign`, which accepts `TVoid` as the expected type. -/ def synthAssign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : @@ -975,6 +980,55 @@ def synthAssign (exprMd : StmtExprMd) try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) omega +/-- Rule **Assign-Check**: an assignment in statement position (checked + against `TVoid`) discards its RHS value, so the synthesized type is not + compared against `expected`. This lets `b := 1` appear as the last + statement of a block in an else-less `if` (whose branch is checked + against `TVoid`) without firing a subsumption error against the RHS's + type. For non-`TVoid` expected types, falls back to subsumption. -/ +def checkAssign (exprMd : StmtExprMd) + (targets : List VariableMd) (value : StmtExprMd) + (expected : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .Assign targets value) : ResolveM StmtExprMd := do + let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do + let ⟨vv, vs⟩ := v + match vv with + | .Local ref => + let ref' ← resolveRef ref source + pure (⟨.Local ref', vs⟩ : VariableMd) + | .Field target fieldName => + let (target', _) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName source + pure (⟨.Field target' fieldName', vs⟩ : VariableMd) + | .Declare param => + let ty' ← resolveHighType param.type + let name' ← defineNameCheckDup param.name (.var param.name ty') + pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) + let (value', valueTy) ← synthStmtExpr value + let targetType (t : VariableMd) : ResolveM HighTypeMd := do + match t.val with + | .Local ref => getVarType ref + | .Declare param => pure param.type + | .Field _ fieldName => getVarType fieldName + if valueTy.val != HighType.TVoid then + let targetTys ← targets'.mapM targetType + let assignedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + checkSubtype source assignedTy valueTy + unless expected.val matches .TVoid do + checkSubtype source expected valueTy + pure { val := .Assign targets' value', source := source } + termination_by (exprMd, 0) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) + omega + -- ### Calls /-- Rules **Static-Call** / **Static-Call-Multi**: callee is resolved against From fd75ea594b00ea4241cf46f4f8bdfcc5d8f9ba5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 10:05:43 -0400 Subject: [PATCH 056/128] documentation is moved to in-code docstrings --- Strata/Languages/Laurel/Laurel.lean | 38 +++- Strata/Languages/Laurel/Resolution.lean | 205 +++++++++++++----- docs/verso/LaurelDoc.lean | 262 ++++++++---------------- 3 files changed, 272 insertions(+), 233 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index 4dd9b7a0f9..ceab6a3025 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -530,9 +530,14 @@ partial def TypeContext.ancestors (ctx : TypeContext) (name : String) : Std.Hash go acc' (parents ++ rest) go {} [name] -/-- Subtyping. Walks `extending` chains for composites, unfolds aliases, and - unwraps constrained types to their base before falling back to structural - equality via `highEq`. -/ +/-- Pure subtyping `<:`. Walks the `extending` chain for `CompositeType` + (via `TypeContext.ancestors`), unfolds `TypeAlias` to its target, and + unwraps `ConstrainedType` to its base (both via `TypeContext.unfold`), + then falls back to structural equality via `highEq`. + + Used together with `isConsistent` to form `isConsistentSubtype`, which + is what the bidirectional checker invokes at every check-mode boundary + (rule `[⇐] Sub`). -/ def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := let sub' := ctx.unfold sub let sup' := ctx.unfold sup @@ -543,9 +548,13 @@ def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := (ctx.ancestors subName.text).contains supName.text || highEq sub' sup' | _, _ => highEq sub' sup' -/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the - dynamic type and is consistent with everything; otherwise structural - equality after unfolding aliases / constrained types. -/ +/-- Consistency `~` (Siek–Taha): the symmetric gradual relation. `Unknown` + is the dynamic type and is consistent with everything; otherwise + structural equality after unfolding aliases / constrained types. + + Used directly by `[⇒] Op-Eq`, where the operand types must be mutually + consistent (no subtype direction is privileged), and as one half of + `isConsistentSubtype`. -/ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := let a' := ctx.unfold a let b' := ctx.unfold b @@ -554,7 +563,22 @@ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := | _, _ => highEq a' b' /-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice - this collapses to `sub ~ sup ∨ sub <: sup`. -/ + this collapses to `sub ~ sup ∨ sub <: sup` — the standard collapse. + + Used by rule `[⇐] Sub` (and every bespoke check rule). That single + choice is what makes the system *gradual*: an expression of type + `Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely + into any typed slot, and any expression flows freely into a slot of + type `Unknown`. Strict checking is applied between fully-known types + only. + + A previous iteration was synth-only with two *bivariantly-compatible* + wildcards: `Unknown` and `UserDefined`. The `UserDefined` carve-out was + load-bearing: no assignment, call argument, or comparison involving a + user type was ever rejected. The bidirectional design retires that + carve-out — user-defined types are now a regular participant in `<:`, + with `isSubtype` walking inheritance chains and unwrapping aliases + and constrained types to deliver real checking on user-defined code. -/ def isConsistentSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := isConsistent ctx sub sup || isSubtype ctx sub sup diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index ddaf0df040..970e32d0ad 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -560,8 +560,20 @@ mutual -- ### Dispatch -/-- Synth-mode resolution: resolve `e` and synthesize its `HighType`. - Each constructor delegates to its rule's helper. -/ +/-- Synth-mode resolution: resolve `e` and synthesize its `HighType`, + written `Γ ⊢ e ⇒ T`. Each constructor delegates to its rule's helper. + + Synthesis returns a type inferred from the expression itself; checking + (`checkStmtExpr`) verifies that the expression has a given expected + type. Each construct picks a mode based on whether its type is + determined locally (synth) or by context (check). Synth rules invoke + check on subexpressions whose expected type is known (e.g. + `cond ⇐ TBool` in `IfThenElse`); `checkStmtExpr` falls back to + `synthStmtExpr` via subsumption (rule `[⇐] Sub`). The two functions + are mutually recursive, with termination on a lexicographic measure + `(exprMd, tag)` — tag `0` for check, `1` for synth — so that + subsumption (which calls synth on the *same* expression) can decrease + via `Prod.Lex.right`. -/ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match h_node: exprMd with | AstNode.mk expr source => @@ -628,10 +640,22 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | (apply Prod.Lex.right; decide) /-- Check-mode resolution (rule **Sub** at the boundary): resolve `e` and - verify its type is a consistent subtype of `expected`. Bidirectional rules - for individual constructs push `expected` into subexpressions; everything - else falls back to subsumption (synth, then `isConsistentSubtype actual - expected`). -/ + verify its type is a consistent subtype of `expected`, written + `Γ ⊢ e ⇐ T`. Bidirectional rules for individual constructs (`Block`, + `IfThenElse`, `Assign`, `Hole`) push `expected` into subexpressions + rather than bouncing through synthesis, which keeps error messages + localized and lets the expected type propagate through nested control + flow. Everything else falls back to subsumption — synthesize, then + verify `isConsistentSubtype actual expected`. + + The right principle for new call sites is: when the position has a + known expected type (`TBool` for conditions, numeric for `decreases`, + the declared output for a constant initializer or a functional body), + use `checkStmtExpr`. When it doesn't, use `resolveStmtExpr` (a thin + wrapper that calls `synthStmtExpr` and discards the synthesized type, + used at sites where typing is not enforced — verification annotations, + modifies/reads clauses). `synthStmtExpr` itself is mostly an internal + interface used by other rules. -/ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do match h_node: exprMd with | AstNode.mk expr source => @@ -714,14 +738,19 @@ def synthVarField (exprMd : StmtExprMd) /-- Rules **If-NoElse** / **If-Synth**: `cond` is checked against `TBool`. With no else branch, the construct is a statement — `thenBr` is checked - against `TVoid` and the result is `TVoid`, so `if c then 5` is rejected. + against `TVoid` and the result is `TVoid`, so `x : int := if c then 5` + is rejected at the branch rather than slipping through to a downstream + subsumption. + With an else branch, the result type is the join (LUB) of the two - branches' synthesized types, so `if c then new Left else new Right` - synthesizes the common ancestor `Top` rather than committing to one - branch arbitrarily. When no common supertype exists (e.g. a value branch - paired with a `TVoid` `return`/`exit`), `joinTypes` falls back to the - then-branch's type and the enclosing context's check surfaces any - mismatch downstream. -/ + branches' synthesized types, so `if c then small else big` synthesizes + the common supertype rather than committing to one branch arbitrarily; + `if c then new Left else new Right` synthesizes the common ancestor. + When no common supertype exists (e.g. a value branch paired with a + `TVoid` `return`/`exit`), `joinTypes` falls back to the then-branch's + type and the enclosing context's check (`[⇐] Sub`, or a containing + `checkSubtype` like an assignment) surfaces any mismatch downstream + against the then-branch's type. -/ def synthIfThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : @@ -747,11 +776,14 @@ def synthIfThenElse (exprMd : StmtExprMd) try omega) | (apply Prod.Lex.right; decide) -/-- Rules **Block-Synth** / **Block-Synth-Empty**: non-last statements are - synthesized but their types discarded (the lax rule, matching - Java/Python/JS expression-statement semantics); the last statement's type +/-- Rules **Block-Synth** / **Block-Synth-Empty**: each statement is resolved + in the scope produced by its predecessor and may itself extend it + (`Var (.Declare …)` does); non-last statements are synthesized but their + types discarded (the lax rule, matching Java/Python/JS where `f(x);` is + normal even when `f` returns a value — trade-off: `5;` is silently + accepted, flagging it belongs to a lint). The last statement's type becomes the block's type, or `TVoid` for an empty block. The block opens - a fresh nested scope. -/ + a fresh nested scope, so bindings introduced inside don't escape. -/ def synthBlock (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (h : exprMd.val = .Block stmts label) : @@ -772,8 +804,9 @@ def synthBlock (exprMd : StmtExprMd) omega /-- Rule **While**: `cond ⇐ TBool`, each invariant `⇐ TBool`, optional - `decreases` is resolved without a type check (intended target is numeric), - body is synthesized; the construct itself synthesizes `TVoid`. -/ + `decreases` is resolved without a type check today (the intended target + is a numeric type), body is synthesized; the construct itself + synthesizes `TVoid`. -/ def synthWhile (exprMd : StmtExprMd) (cond : StmtExprMd) (invs : List StmtExprMd) (dec : Option StmtExprMd) (body : StmtExprMd) @@ -802,9 +835,22 @@ def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTy /-- Rules **Return-None** / **Return-Some** / **Return-Void-Error** / **Return-Multi-Error**: matches the optional return value against the - enclosing procedure's declared outputs (`expectedReturnTypes`). `none` - means "no enclosing procedure" — e.g. resolving a constant initializer — - and skips all `Return` checks. -/ + enclosing procedure's declared outputs. The expected output types are + threaded through `ResolveState.expectedReturnTypes`, set from + `proc.outputs` by `resolveProcedure` / `resolveInstanceProcedure` for + the duration of the body; `none` means "no enclosing procedure" — e.g. + resolving a constant initializer — and skips all `Return` checks. + + A bare `return;` is allowed in any context. In a single-output procedure + it acts as a Dafny-style early exit — the output parameter retains + whatever was last assigned to it. In a single-output procedure, `return e` + is checked against the declared output type (closing the prior soundness + gap where `return 0` in a `bool`-returning procedure went uncaught). + + Multi-output procedures use named-output assignment (`r := …` on the + declared output parameters); `return e` syntactically takes a single + `Option StmtExpr` and cannot carry multiple values, so it is flagged with + a diagnostic pointing users at the named-output convention. -/ def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) (val : Option StmtExprMd) (h : exprMd.val = .Return val) : @@ -841,9 +887,11 @@ def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) /-- Rules **Block-Check** / **Block-Check-Empty**: pushes `expected` into the *last* statement rather than comparing the block's synthesized type at the - boundary. Errors fire at the offending subexpression, and `T` keeps - propagating through nested `Block` / `IfThenElse` / `Hole` / `Quantifier`. - Empty blocks reduce to a subsumption check of `TVoid` against `expected`. -/ + boundary. Errors fire at the offending subexpression, and `expected` + keeps propagating through nested `Block` / `IfThenElse` / `Hole` / + `Quantifier`. Empty blocks reduce to a subsumption check of `TVoid` + against `expected` — the same check `[⇐] Block-Empty` performs when + `T` admits `TVoid`. -/ def checkBlock (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (expected : HighTypeMd) (source : Option FileRange) @@ -873,8 +921,9 @@ def checkBlock (exprMd : StmtExprMd) /-- Rules **If-Check** / **If-Check-NoElse**: pushes `expected` into both branches (rather than going through If-Synth + Sub at the boundary). Errors fire at the offending branch instead of the surrounding `if`. - Without an else branch, the construct can only succeed when `T` admits - `TVoid`. -/ + Without an else branch, the construct can only succeed when `expected` + admits `TVoid` — the same subsumption check `[⇐] Block-Empty` performs + for an empty block. -/ def checkIfThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) @@ -933,12 +982,16 @@ def synthAssume (exprMd : StmtExprMd) /-- Rule **Assign**: each target's declared type `T_i` (from `Local`, `Field`, or fresh `Declare`) is collapsed into a tuple `ExpectedTy` (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) - and checked against the RHS's synthesized type. When the RHS is a - statement (`TVoid`) — `while`, `return`, … — all checks are skipped: - there's no value to assign. The construct synthesizes the RHS's type, - so that expression-position assignments like `x ++ (y := s)` see a - string in the second operand; statement-position uses are accommodated - by `checkAssign`, which accepts `TVoid` as the expected type. -/ + and checked against the RHS's synthesized type. Both single- and + multi-target forms collapse into one tuple-vs-tuple check: when the RHS + is a `MultiValuedExpr`, both arity and per-position type mismatches + surface in a single diagnostic of shape *"expected '(int, int, int)', + got '(int, string)'"*. When the RHS is `TVoid` (a side-effecting + statement: `while`, `return`, …), all checks are skipped — there's no + value to assign. The construct synthesizes the RHS's type, so that + expression-position assignments like `x ++ (y := s)` see a string in + the second operand; statement-position uses are accommodated by + `checkAssign`, which accepts `TVoid` as the expected type. -/ def synthAssign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : @@ -1090,11 +1143,17 @@ def synthInstanceCall (exprMd : StmtExprMd) /-- Rules **Op-Bool** / **Op-Cmp** / **Op-Eq** / **Op-Arith** / **Op-Concat**: each operator family has its own argument-type discipline and result - type. Arguments are synthesized first, then the per-family check fires - (`⇐ TBool` for booleans, `Numeric` for arithmetic/comparison, consistency - `~` for equality, `⇐ TString` for concatenation). The result type is - `TBool` for booleans/comparisons/equality, the head argument's type for - arithmetic, `TString` for concatenation. -/ + type. Arguments are synthesized first, then the per-family check fires: + `⇐ TBool` for booleans, `Numeric` (consistent with `TInt`, `TReal`, or + `TFloat64`) for arithmetic/comparison, consistency `~` for equality + (symmetric — no subtype direction is privileged), `⇐ TString` for + concatenation. The result type is `TBool` for + booleans/comparisons/equality, the head argument's type for arithmetic + ("result is the type of the first argument" handles `int + int → int`, + `real + real → real`, etc. without unification — known relaxation: + `int + real` passes since each operand individually passes `Numeric`; + a proper fix needs numeric promotion or unification), `TString` for + concatenation. -/ def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) (op : Operation) (args : List StmtExprMd) (source : Option FileRange) (h_expr : expr = .PrimitiveOp op args) @@ -1161,7 +1220,9 @@ def synthNew (ref : Identifier) (source : Option FileRange) : pure (.New ref', ty) /-- Rule **AsType**: `target` is resolved but not checked against `T` — the - cast is the user's claim. The synthesized type is `T`. -/ + cast is the user's claim. The synthesized type is `T`. + + `IsType` is the runtime test counterpart and synthesizes `TBool`. -/ def synthAsType (exprMd : StmtExprMd) (target : StmtExprMd) (ty : HighTypeMd) (h : exprMd.val = .AsType target ty) : @@ -1192,7 +1253,12 @@ def synthIsType (exprMd : StmtExprMd) omega /-- Rule **RefEq**: both operands must be reference types (`UserDefined` or - `Unknown`). Reference equality is meaningless on primitives. -/ + `Unknown`) — reference equality is meaningless on primitives. The + operands must also be mutually consistent (the symmetric `isConsistent`), + so `Cat === Dog` is rejected when `Cat` and `Dog` are unrelated + user-defined types, while `Cat === Animal` is accepted when `Cat` + extends `Animal` (the gradual `Unknown` wildcard makes either side + flow freely against the other). -/ def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) (lhs rhs : StmtExprMd) (source : Option FileRange) (h_expr : expr = .ReferenceEquals lhs rhs) @@ -1242,9 +1308,11 @@ def synthPureFieldUpdate (exprMd : StmtExprMd) -- ### Verification expressions -/-- Rule **Quantifier**: opens a fresh scope, binds `x : T`, resolves the - optional trigger, and checks the body against `TBool`. The construct - itself synthesizes `TBool` since a quantifier is a proposition. -/ +/-- Rule **Quantifier**: opens a fresh scope, binds `x : T` (in scope only + for the body and trigger), resolves the optional trigger, and checks + the body against `TBool` since a quantifier is a proposition. Without + that body check, `forall x: int :: x + 1` would be silently accepted. + The construct itself synthesizes `TBool`. -/ def synthQuantifier (exprMd : StmtExprMd) (mode : QuantifierMode) (param : Parameter) (trigger : Option StmtExprMd) (body : StmtExprMd) (source : Option FileRange) @@ -1296,7 +1364,9 @@ def synthOld (exprMd : StmtExprMd) omega /-- Rule **Fresh**: `v` is synthesized and must have a reference type - (`UserDefined` or `Unknown`). The construct itself synthesizes `TBool`. -/ + (`UserDefined` or `Unknown`) — `Fresh` only makes sense on + heap-allocated references, so `fresh(5)` is rejected. The construct + itself synthesizes `TBool`. -/ def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) (val : StmtExprMd) (source : Option FileRange) (h_expr : expr = .Fresh val) @@ -1334,8 +1404,13 @@ def synthProveBy (exprMd : StmtExprMd) -- ### Self reference /-- Rules **This-Inside** / **This-Outside**: when `instanceTypeName` is set - (we're inside an instance method), `This` synthesizes `UserDefined T`; - otherwise an error is emitted and the type collapses to `Unknown`. -/ + (we're inside an instance method, populated on `ResolveState` by + `resolveInstanceProcedure` for the duration of an instance method body), + `This` synthesizes `UserDefined T`. With it, `this.field` and + instance-method dispatch synthesize real types instead of being + wildcarded through `Unknown`. Otherwise an error is emitted ("'this' + is not allowed outside instance methods") and the type collapses to + `Unknown` to suppress cascading errors. -/ def synthThis (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let s ← get @@ -1364,10 +1439,25 @@ def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := -- ### ContractOf /-- Rules **ContractOf-Bool** / **ContractOf-Set** / **ContractOf-Error**: - `fn` must be a direct identifier reference resolving to a procedure; - anything else is ill-formed (a contract belongs to a *named* procedure). - Pre/postconditions are propositions (`TBool`); reads/modifies are sets of - heap references with element type `Unknown` for now. -/ + `ContractOf ty fn` extracts a procedure's contract clause as a value: + its preconditions (`Precondition`), postconditions (`PostCondition`), + reads set (`Reads`), or modifies set (`Modifies`). `fn` must be a + direct identifier reference resolving to a procedure — a contract + belongs to a *named* procedure, not an arbitrary expression. Anything + else fires the diagnostic *"'contractOf' expected a procedure + reference"* and the construct synthesizes `Unknown` to suppress + cascading errors. + + `Precondition` and `PostCondition` are propositions, hence `TBool`. + `Reads` and `Modifies` are sets of heap-allocated locations — + composite/datatype references and fields. The element type is left as + `Unknown` for now since the rule doesn't yet recover it from `fn`'s + declared modifies/reads clauses. + + The constructor is reserved for future use — Laurel's grammar has no + `contractOf` production today, and the translator emits "not yet + implemented" for it. The typing rule exists so resolution remains + exhaustive over `StmtExpr`. -/ def synthContractOf (exprMd : StmtExprMd) (ty : ContractType) (fn : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .ContractOf ty fn) : @@ -1412,9 +1502,18 @@ def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange /-- Rule **Hole-None-Check**: an untyped hole in check mode records the expected type on the node so downstream passes don't have to infer it - again. The subsumption check is trivial (`Unknown <: T` always holds), so - this rule never fails — it just preserves the type information available - at the check-mode boundary. -/ + again. The subsumption check is trivial (`Unknown <: T` always holds), + so this rule never fails — it just preserves the type information + available at the check-mode boundary instead of discarding it. + + A separate `InferHoleTypes` pass still runs after resolution to + annotate holes that ended up in synth-only positions. When that pass + encounters a hole whose type was already set (by `[⇐] Hole-None` or by + a user-written `?: T`), it checks the resolution-time and + inference-time types for consistency under `~`; a disagreement fires + the diagnostic *"hole annotated with 'T_resolution' but context + expects 'T_inference'"*, surfacing what would otherwise be a silent + overwrite. -/ def checkHoleNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : StmtExprMd := { val := .Hole det (some expected), source := source } diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 34b5772920..7583d4d079 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -165,70 +165,31 @@ There are two operations on expressions, written here in standard bidirectional ``` Synthesis returns a type inferred from the expression itself; checking verifies that the -expression has a given expected type. Each construct picks a mode based on whether its type -is determined locally (synth) or by context (check). The two judgments are connected by a -single change-of-direction rule, *subsumption*: +expression has a given expected type. Each construct picks a mode based on whether its +type is determined locally (synth) or by context (check). The two judgments are connected +by a single change-of-direction rule, *subsumption*: $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` -Subsumption is the *only* place the checker switches from check to synth mode. It fires as -the default fallback in -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct without a bespoke -check rule: synthesize the expression's type, then verify the result is a subtype of the -expected type. Bespoke check rules push the expected type *into* subexpressions instead of -bouncing through synthesis, which keeps error messages localized and lets the expected type -propagate through nested control flow. - -`synthStmtExpr` and `checkStmtExpr` are mutually recursive: synth rules invoke check on -subexpressions whose expected type is known (e.g. `cond ⇐ TBool` in -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), and `checkStmtExpr` falls back to -`synthStmtExpr` via \[⇐\] Sub. Termination uses a lexicographic measure `(exprMd, tag)` -where the tag is `0` for synth and `1` for check; any descent into a strict subterm -decreases via `Prod.Lex.left`, while \[⇐\] Sub calls synth on the *same* expression and -decreases via -`Prod.Lex.right`. This is the standard well-founded encoding for bidirectional systems. - -There is also a thin `resolveStmtExpr` wrapper that calls `synthStmtExpr` and discards the -synthesized type. It's used at sites where typing is not enforced (verification annotations, -modifies/reads clauses). The right principle for new call sites is: when the position has a -known expected type ({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for -`decreases`, the declared output for a constant initializer or a functional body), use -`checkStmtExpr`. When it doesn't, use `resolveStmtExpr`. `synthStmtExpr` itself is mostly an -internal interface used by other rules. +The two judgments are implemented as +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`: + +{docstring Strata.Laurel.synthStmtExpr} + +{docstring Strata.Laurel.checkStmtExpr} ### Gradual typing -The relation `<:` (used in \[⇐\] Sub) is built from three Lean functions: - -- `isSubtype` — pure subtyping. Walks the `extending` chain for - {name Strata.Laurel.CompositeType}`CompositeType` (via - {name Strata.Laurel.TypeContext.ancestors}`TypeContext.ancestors`), unfolds - {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps - {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base (both via - {name Strata.Laurel.TypeContext.unfold}`TypeContext.unfold`), then falls back to - structural equality via {name Strata.Laurel.highEq}`highEq`. -- `isConsistent` — the symmetric gradual relation `~` (Siek–Taha): - {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type and is consistent with - everything; otherwise structural equality. -- `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this - is the standard collapse of `∃R. T ~ R ∧ R <: U`. - -\[⇐\] Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what -makes the system *gradual*: an expression of type -{name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) -flows freely into any typed slot, and any expression flows freely into a slot of type -{name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between -fully-known types only. The symmetric `isConsistent` is used directly by \[⇒\] Op-Eq, where -the operand types must be mutually consistent (no subtype direction is privileged). - -A previous iteration was synth-only with two *bivariantly-compatible* wildcards: -{name Strata.Laurel.HighType.Unknown}`Unknown` and -{name Strata.Laurel.HighType.UserDefined}`UserDefined`. The -{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no -assignment, call argument, or comparison involving a user type was ever rejected. The -bidirectional design retires that carve-out — user-defined types are now a regular -participant in `<:`, with `isSubtype` walking inheritance chains and unwrapping aliases -and constrained types to deliver real checking on user-defined code. +The relation `<:` (used in \[⇐\] Sub) is built from three Lean functions — +{name Strata.Laurel.isSubtype}`isSubtype`, {name Strata.Laurel.isConsistent}`isConsistent`, +and {name Strata.Laurel.isConsistentSubtype}`isConsistentSubtype`: + +{docstring Strata.Laurel.isSubtype} + +{docstring Strata.Laurel.isConsistent} + +{docstring Strata.Laurel.isConsistentSubtype} Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This includes {name Strata.Laurel.StmtExpr.Return}`Return`, @@ -270,64 +231,64 @@ suffix is dropped in favor of the prefix. - *ContractOf* — \[⇒\] ContractOf-Bool, \[⇒\] ContractOf-Set, \[⇒\] ContractOf-Error - *Holes* — \[⇒\] Hole-Some, \[⇒\] Hole-None, \[⇐\] Hole-None +Each LaTeX rule below is followed by the docstring of the helper that implements it +(grouped when one helper covers multiple rules). + ### Subsumption $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` -Fallback in `checkStmtExpr` whenever no bespoke check rule applies. +Fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` whenever no bespoke check +rule applies. ### Literals $$`\frac{}{\Gamma \vdash \mathsf{LiteralInt}\;n \Rightarrow \mathsf{TInt}} \quad \text{([⇒] Lit-Int)}` +{docstring Strata.Laurel.synthLitInt} + $$`\frac{}{\Gamma \vdash \mathsf{LiteralBool}\;b \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Lit-Bool)}` +{docstring Strata.Laurel.synthLitBool} + $$`\frac{}{\Gamma \vdash \mathsf{LiteralString}\;s \Rightarrow \mathsf{TString}} \quad \text{([⇒] Lit-String)}` +{docstring Strata.Laurel.synthLitString} + $$`\frac{}{\Gamma \vdash \mathsf{LiteralDecimal}\;d \Rightarrow \mathsf{TReal}} \quad \text{([⇒] Lit-Decimal)}` +{docstring Strata.Laurel.synthLitDecimal} + ### Variables $$`\frac{\Gamma(x) = T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Local}\;x) \Rightarrow T} \quad \text{([⇒] Var-Local)}` +{docstring Strata.Laurel.synthVarLocal} + $$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Field}\;e\;f) \Rightarrow T_f} \quad \text{([⇒] Var-Field)}` -Resolution looks `f` up against the type of `e` (or the enclosing instance type for -`self.f`); the typing rule itself is path-agnostic. +{docstring Strata.Laurel.synthVarField} $$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. +{docstring Strata.Laurel.synthVarDeclare} + ### Control flow $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow \mathsf{TVoid}}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` -The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no -value when `cond` is false; the then-branch is checked against -{name Strata.Laurel.HighType.TVoid}`TVoid` so `x : int := if c then 5` is rejected at the -branch rather than slipping through to a downstream subsumption. - $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If)}` -The result is the join (least upper bound) of the two branch types, so -`if c then small else big` synthesizes the common supertype rather than committing to one -branch arbitrarily. The join walks `extending` chains for composites; when no common -supertype exists (e.g. a value branch paired with a `TVoid` `return`/`exit`), it falls -back to `T_t` and the enclosing context's check (\[⇐\] Sub, or a containing -`checkSubtype` like an assignment) surfaces any mismatch downstream against the -then-branch's type. +{docstring Strata.Laurel.synthIfThenElse} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}` -Check mode pushes `T` into both branches (rather than going through \[⇒\] If + \[⇐\] Sub at -the boundary). Errors fire at the offending branch instead of the surrounding `if`. -Without an else branch, the construct can only succeed when `T` admits -{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `\[⇐\] Block-Empty` -performs for an empty block. +{docstring Strata.Laurel.checkIfThenElse} $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` @@ -336,79 +297,56 @@ predecessor and may itself extend it (`Var (.Declare …)` does); `s_n` is typed `Γ_{n-1}`. Bindings introduced inside the block don't escape — `Γ` is what surrounds the block. -Non-last statements are synthesized but their types discarded (the lax rule). This matches -Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` -is silently accepted; flagging it belongs to a lint. - $$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` -$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` +{docstring Strata.Laurel.synthBlock} -Pushes `T` into the *last* statement rather than comparing the block's synthesized type at -the boundary. Errors fire at the offending subexpression, and `T` keeps propagating through -nested {name Strata.Laurel.StmtExpr.Block}`Block` / -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / -{name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. +$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` $$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` +{docstring Strata.Laurel.checkBlock} + $$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` -`Return` matches the optional return value against the enclosing procedure's declared -outputs. The expected output types are threaded through -{name Strata.Laurel.ResolveState}`ResolveState`'s `expectedReturnTypes`, set from -`proc.outputs` by {name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of -the body. `none` means "no enclosing procedure" — e.g. resolving a constant initializer — -and skips all `Return` checks. +{docstring Strata.Laurel.synthExit} $$`\frac{}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None)}` -A bare `return;` is allowed in any context. In a single-output procedure it acts as a -Dafny-style early exit — the output parameter retains whatever was last assigned to it. - $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T] \quad \Gamma \vdash e \Leftarrow T}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-Some)}` -In a single-output procedure, the value is checked against the declared output type. This -closes the prior soundness gap where `return 0` in a `bool`-returning procedure went -uncaught. - $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “void procedure cannot return a value”}} \quad \text{([⇒] Return-Void-Error)}` $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}` -Multi-output procedures use named-output assignment (`r := …` on the declared output -parameters). `return e` syntactically takes a single -{name Strata.Laurel.StmtExpr.Return}`Option StmtExpr`, so it cannot carry multiple values; -flagging it points users at the named-output convention. +{docstring Strata.Laurel.synthReturn} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Rightarrow \_}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}` -`dec` (the optional decreases clause) is resolved without a type check today; the intended -target is a numeric type. +{docstring Strata.Laurel.synthWhile} ### Verification statements $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}` +{docstring Strata.Laurel.synthAssert} + $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}` +{docstring Strata.Laurel.synthAssume} + ### Assignment $$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Rightarrow T_e \quad T_e <: \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assign)}` where `ExpectedTy = T_1` if `|targets| = 1` and `MultiValuedExpr [T_1; …; T_n]` otherwise. - The target's declared type `T_i` comes from the variable's scope entry (for {name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) -or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. Both -single- and multi-target forms collapse into one tuple-vs-tuple check: when the RHS is a -{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`, both arity and per-position -type mismatches surface in a single diagnostic of shape *"expected '(int, int, int)', got -'(int, string)'"*. When the RHS is {name Strata.Laurel.HighType.TVoid}`TVoid` (a -side-effecting statement: `while`, `return`, …), all checks are skipped — there's no value -to assign. +or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. + +{docstring Strata.Laurel.synthAssign} + +{docstring Strata.Laurel.checkAssign} ### Calls @@ -416,8 +354,12 @@ $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T_1; \ldots; T_n],\; n \ne 1 \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Static-Call-Multi)}` +{docstring Strata.Laurel.synthStaticCall} + $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_ \quad \Gamma(\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{InstanceCall}\;\mathit{target}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Instance-Call)}` +{docstring Strata.Laurel.synthInstanceCall} + ### Primitive operations `Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, @@ -430,106 +372,88 @@ $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \mathit $$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad T_l \sim T_r \quad \mathit{op} \in \{\mathsf{Eq}, \mathsf{Neq}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;[\mathit{lhs}; \mathit{rhs}] \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Eq)}` -`~` is the consistency relation `isConsistent` — symmetric, with the -{name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. - $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \Gamma \vdash \mathit{args}.\mathsf{head} \Rightarrow T \quad \mathit{op} \in \{\mathsf{Neg}, \mathsf{Add}, \mathsf{Sub}, \mathsf{Mul}, \mathsf{Div}, \mathsf{Mod}, \mathsf{DivT}, \mathsf{ModT}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Op-Arith)}` -"Result is the type of the first argument" handles `int + int → int`, `real + real → real`, -etc. without unification. Known relaxation: `int + real` passes (each operand individually -passes `Numeric`); a proper fix needs numeric promotion or unification. - $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TString} \quad \mathit{op} = \mathsf{StrConcat}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TString}} \quad \text{([⇒] Op-Concat)}` +{docstring Strata.Laurel.synthPrimitiveOp} + ### Object forms $$`\frac{\Gamma(\mathit{ref}) \text{ is a composite or datatype } T}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] New-Ok)}` $$`\frac{\Gamma(\mathit{ref}) \text{ is not a composite or datatype}}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] New-Fallback)}` +{docstring Strata.Laurel.synthNew} + $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsType}\;\mathit{target}\;T \Rightarrow T} \quad \text{([⇒] AsType)}` -`target` is resolved but not checked against `T` — the cast is the user's claim. +{docstring Strata.Laurel.synthAsType} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` +{docstring Strata.Laurel.synthIsType} + $$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r \quad T_l \sim T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` `isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` -or {name Strata.Laurel.HighType.Unknown}`Unknown` -type. Reference equality is meaningless on primitives. The operands must also be -consistent under `~` (Siek–Taha consistency), matching the rule applied by -{name Strata.Laurel.Operation.Eq}`==`: two distinct user-defined types like `Cat` and -`Dog` are rejected, while either side being `Unknown` is accepted as a gradual escape -hatch. +or {name Strata.Laurel.HighType.Unknown}`Unknown` type. `~` is the consistency relation +{name Strata.Laurel.isConsistent}`isConsistent` — symmetric, with the +{name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. + +{docstring Strata.Laurel.synthRefEq} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` -`f` is resolved against `T_t` (or the enclosing instance type) and `newVal` is checked -against the field's declared type. +{docstring Strata.Laurel.synthPureFieldUpdate} ### Verification expressions $$`\frac{\Gamma, x : T \vdash \mathit{body} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Quantifier}\;\mathit{mode}\;\langle x, T\rangle\;\mathit{trig}\;\mathit{body} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Quantifier)}` -The bound variable `x : T` is introduced in scope only for the body (and trigger). The body -is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a -proposition; without this, `forall x: int :: x + 1` would be silently accepted. +{docstring Strata.Laurel.synthQuantifier} $$`\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assigned}\;\mathit{name} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Assigned)}` +{docstring Strata.Laurel.synthAssigned} + $$`\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old)}` +{docstring Strata.Laurel.synthOld} + $$`\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}` -`isReference T` is the same predicate as in {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals`. -{name Strata.Laurel.StmtExpr.Fresh}`Fresh` only makes sense on heap-allocated references; -`fresh(5)` is rejected. +{docstring Strata.Laurel.synthFresh} $$`\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy)}` +{docstring Strata.Laurel.synthProveBy} + ### Self reference $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{some}\;T}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] This-Inside)}` $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{none}}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{Unknown}\;\;[\text{emits “‘this’ is not allowed outside instance methods”}]} \quad \text{([⇒] This-Outside)}` -`Γ.instanceTypeName` is the -{name Strata.Laurel.ResolveState}`ResolveState` field set by -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of -an instance method body. With it, `this.field` and instance-method dispatch synthesize real -types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}`Unknown`. +{docstring Strata.Laurel.synthThis} ### Untyped forms $$`\frac{}{\Gamma \vdash \mathsf{Abstract}\;/\;\mathsf{All}\;\ldots \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Abstract / All)}` -### ContractOf +{docstring Strata.Laurel.synthAbstract} -`ContractOf ty fn` extracts a procedure's contract clause as a value: its preconditions -(`Precondition`), postconditions (`PostCondition`), reads set (`Reads`), or modifies set -(`Modifies`). `fn` must be a direct identifier reference to a procedure — a contract belongs -to a *named* procedure, not an arbitrary expression. +{docstring Strata.Laurel.synthAll} + +### ContractOf $$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Precondition}\;\mathit{fn} \Rightarrow \mathsf{TBool} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{PostCondition}\;\mathit{fn} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] ContractOf-Bool)}` $$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Reads}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{Modifies}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown}} \quad \text{([⇒] ContractOf-Set)}` -`Precondition` and `PostCondition` are propositions, hence -{name Strata.Laurel.HighType.TBool}`TBool`. `Reads` and `Modifies` are sets of heap-allocated -locations — composite/datatype references and fields. The element type is left as -{name Strata.Laurel.HighType.Unknown}`Unknown` for now since the rule doesn't yet recover it -from `fn`'s declared modifies/reads clauses. - $$`\frac{\mathit{fn} \text{ is not a procedure reference}}{\Gamma \vdash \mathsf{ContractOf}\;\ldots\;\mathit{fn} \rightsquigarrow \text{error: “‘contractOf’ expected a procedure reference”}} \quad \text{([⇒] ContractOf-Error)}` -When `fn` doesn't resolve to a procedure (e.g. it's an arbitrary expression, or resolves to -a constant/variable), the diagnostic fires and the construct synthesizes -{name Strata.Laurel.HighType.Unknown}`Unknown` to suppress cascading errors. - -The constructor is reserved for future use — Laurel's grammar has no `contractOf` -production today, and the translator emits "not yet implemented" for it. The typing rule -exists so resolution remains exhaustive over `StmtExpr`. +{docstring Strata.Laurel.synthContractOf} ### Holes @@ -537,19 +461,11 @@ $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T) \Rightarrow T} \qu $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-None)}` +{docstring Strata.Laurel.synthHole} + $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapsto\;\; \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}` -In check mode, an untyped hole records the expected type `T` on the node directly. The -subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it -just preserves the type information that's available at the check-mode boundary instead of -discarding it. - -A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended -up in synth-only positions. When that pass encounters a hole whose type was already set -(by \[⇐\] Hole-None or by a user-written `?: T`), it checks the resolution-time and -inference-time types for consistency under `~`; a disagreement fires the diagnostic -*"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what -would otherwise be a silent overwrite. +{docstring Strata.Laurel.checkHoleNone} ## Future structural changes From fbb5c81c0620bb9f05728dc3de0a643e15584506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 14:16:41 -0400 Subject: [PATCH 057/128] namespace scoping to make code less verbose --- Strata/Languages/Laurel/Resolution.lean | 307 ++++++++++++------------ docs/verso/LaurelDoc.lean | 90 +++---- 2 files changed, 201 insertions(+), 196 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 970e32d0ad..080142d15c 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -38,7 +38,7 @@ Walks the AST under `ResolveM`, a state monad over `ResolveState`. Phase 1: - opens fresh nested scopes via `withScope` for blocks, quantifiers, procedure bodies, and constrained-type constraint/witness expressions, - synthesizes a `HighType` for every `StmtExpr` and checks it (via - `checkStmtExpr` for fresh subexpressions, or `checkSubtype` when a type is + `Check.resolveStmtExpr` for fresh subexpressions, or `checkSubtype` when a type is already in hand) on assignments, call arguments, condition positions, functional bodies, and constant initializers. @@ -466,7 +466,7 @@ private def typeMismatch (source : Option FileRange) (construct : Option StmtExp /-- Type-level subtype check: emits the standard "expected/got" diagnostic when `actual` is not a consistent subtype of `expected`. Used at sites where the actual type is already in hand (assignment, call args, body vs declared - output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ + output) — equivalent to `Check.resolveStmtExpr e expected` but without re-synthesizing. -/ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do let ctx := (← get).typeContext unless isConsistentSubtype ctx actual expected do @@ -532,26 +532,28 @@ Each typing rule from the Laurel manual is implemented as its own helper inside the mutual block below. Helpers are grouped by section to mirror the *Typing rules* index in `LaurelDoc.lean`: -- Literals — `synthLitInt`, `synthLitBool`, `synthLitString`, `synthLitDecimal` -- Variables — `synthVarLocal`, `synthVarField`, `synthVarDeclare` -- Control flow — `synthIfThenElse`, `synthBlock`, `synthWhile`, `synthExit`, - `synthReturn`, `checkBlock`, `checkIfThenElse` -- Verification statements — `synthAssert`, `synthAssume` -- Assignment — `synthAssign` -- Calls — `synthStaticCall`, `synthInstanceCall` -- Primitive operations — `synthPrimitiveOp` -- Object forms — `synthNew`, `synthAsType`, `synthIsType`, `synthRefEq`, - `synthPureFieldUpdate` -- Verification expressions — `synthQuantifier`, `synthAssigned`, `synthOld`, - `synthFresh`, `synthProveBy` -- Self reference — `synthThis` -- Untyped forms — `synthAbstract`, `synthAll` -- ContractOf — `synthContractOf` -- Holes — `synthHole`, `checkHoleNone` - -The dispatch functions `synthStmtExpr` and `checkStmtExpr` simply pattern-match +- Literals — `Synth.litInt`, `Synth.litBool`, `Synth.litString`, `Synth.litDecimal` +- Variables — `Synth.varLocal`, `Synth.varField`, `Synth.varDeclare` +- Control flow — `Synth.ifThenElse`, `Synth.block`, `Synth.while`, `Synth.exit`, + `Synth.return`, `Check.block`, `Check.ifThenElse` +- Verification statements — `Synth.assert`, `Synth.assume` +- Assignment — `Synth.assign`, `Check.assign` +- Calls — `Synth.staticCall`, `Synth.instanceCall` +- Primitive operations — `Synth.primitiveOp` +- Object forms — `Synth.new`, `Synth.asType`, `Synth.isType`, `Synth.refEq`, + `Synth.pureFieldUpdate` +- Verification expressions — `Synth.quantifier`, `Synth.assigned`, `Synth.old`, + `Synth.fresh`, `Synth.proveBy` +- Self reference — `Synth.this` +- Untyped forms — `Synth.abstract`, `Synth.all` +- ContractOf — `Synth.contractOf` +- Holes — `Synth.hole`, `Check.holeNone` + +The dispatch functions `Synth.resolveStmtExpr` and `Check.resolveStmtExpr` simply pattern-match on the constructor and delegate to the corresponding helper. -/ +namespace Resolution + -- The `h : exprMd.val = .Foo args ...` parameters on the recursive helpers -- look unused to the linter, but each one is referenced by that helper's -- `decreasing_by` tactic to relate `sizeOf args` to `sizeOf exprMd`. @@ -564,74 +566,74 @@ mutual written `Γ ⊢ e ⇒ T`. Each constructor delegates to its rule's helper. Synthesis returns a type inferred from the expression itself; checking - (`checkStmtExpr`) verifies that the expression has a given expected + (`Check.resolveStmtExpr`) verifies that the expression has a given expected type. Each construct picks a mode based on whether its type is determined locally (synth) or by context (check). Synth rules invoke check on subexpressions whose expected type is known (e.g. - `cond ⇐ TBool` in `IfThenElse`); `checkStmtExpr` falls back to - `synthStmtExpr` via subsumption (rule `[⇐] Sub`). The two functions + `cond ⇐ TBool` in `IfThenElse`); `Check.resolveStmtExpr` falls back to + `Synth.resolveStmtExpr` via subsumption (rule `[⇐] Sub`). The two functions are mutually recursive, with termination on a lexicographic measure `(exprMd, tag)` — tag `0` for check, `1` for synth — so that subsumption (which calls synth on the *same* expression) can decrease via `Prod.Lex.right`. -/ -def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do +def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match h_node: exprMd with | AstNode.mk expr source => let (val', ty) ← match h_expr: expr with | .IfThenElse cond thenBr elseBr => - synthIfThenElse exprMd cond thenBr elseBr (by rw [h_node]) + Synth.ifThenElse exprMd cond thenBr elseBr (by rw [h_node]) | .Block stmts label => - synthBlock exprMd stmts label (by rw [h_node]) + Synth.block exprMd stmts label (by rw [h_node]) | .While cond invs dec body => - synthWhile exprMd cond invs dec body (by rw [h_node]) - | .Exit target => pure (synthExit target source) + Synth.while exprMd cond invs dec body (by rw [h_node]) + | .Exit target => pure (Synth.exit target source) | .Return val => - synthReturn exprMd source val (by rw [h_node]) - | .LiteralInt v => pure (synthLitInt v source) - | .LiteralBool v => pure (synthLitBool v source) - | .LiteralString v => pure (synthLitString v source) - | .LiteralDecimal v => pure (synthLitDecimal v source) - | .Var (.Local ref) => synthVarLocal ref source - | .Var (.Declare param) => synthVarDeclare param source + Synth.return exprMd source val (by rw [h_node]) + | .LiteralInt v => pure (Synth.litInt v source) + | .LiteralBool v => pure (Synth.litBool v source) + | .LiteralString v => pure (Synth.litString v source) + | .LiteralDecimal v => pure (Synth.litDecimal v source) + | .Var (.Local ref) => Synth.varLocal ref source + | .Var (.Declare param) => Synth.varDeclare param source | .Var (.Field target fieldName) => - synthVarField exprMd target fieldName source (by rw [h_node]) + Synth.varField exprMd target fieldName source (by rw [h_node]) | .Assign targets value => - synthAssign exprMd targets value source (by rw [h_node]) + Synth.assign exprMd targets value source (by rw [h_node]) | .PureFieldUpdate target fieldName newVal => - synthPureFieldUpdate exprMd target fieldName newVal (by rw [h_node]) + Synth.pureFieldUpdate exprMd target fieldName newVal (by rw [h_node]) | .StaticCall callee args => - synthStaticCall exprMd callee args source (by rw [h_node]) + Synth.staticCall exprMd callee args source (by rw [h_node]) | .PrimitiveOp op args => - synthPrimitiveOp exprMd expr op args source h_expr (by rw [h_node]) - | .New ref => synthNew ref source - | .This => synthThis source + Synth.primitiveOp exprMd expr op args source h_expr (by rw [h_node]) + | .New ref => Synth.new ref source + | .This => Synth.this source | .ReferenceEquals lhs rhs => - synthRefEq exprMd expr lhs rhs source h_expr (by rw [h_node]) + Synth.refEq exprMd expr lhs rhs source h_expr (by rw [h_node]) | .AsType target ty => - synthAsType exprMd target ty (by rw [h_node]) + Synth.asType exprMd target ty (by rw [h_node]) | .IsType target ty => - synthIsType exprMd target ty source (by rw [h_node]) + Synth.isType exprMd target ty source (by rw [h_node]) | .InstanceCall target callee args => - synthInstanceCall exprMd target callee args source (by rw [h_node]) + Synth.instanceCall exprMd target callee args source (by rw [h_node]) | .Quantifier mode param trigger body => - synthQuantifier exprMd mode param trigger body source (by rw [h_node]) + Synth.quantifier exprMd mode param trigger body source (by rw [h_node]) | .Assigned name => - synthAssigned exprMd name source (by rw [h_node]) + Synth.assigned exprMd name source (by rw [h_node]) | .Old val => - synthOld exprMd val (by rw [h_node]) + Synth.old exprMd val (by rw [h_node]) | .Fresh val => - synthFresh exprMd expr val source h_expr (by rw [h_node]) + Synth.fresh exprMd expr val source h_expr (by rw [h_node]) | .Assert ⟨condExpr, summary⟩ => - synthAssert exprMd condExpr summary source (by rw [h_node]) + Synth.assert exprMd condExpr summary source (by rw [h_node]) | .Assume cond => - synthAssume exprMd cond source (by rw [h_node]) + Synth.assume exprMd cond source (by rw [h_node]) | .ProveBy val proof => - synthProveBy exprMd val proof (by rw [h_node]) + Synth.proveBy exprMd val proof (by rw [h_node]) | .ContractOf ty fn => - synthContractOf exprMd ty fn source (by rw [h_node]) - | .Abstract => pure (synthAbstract source) - | .All => pure (synthAll source) - | .Hole det type => synthHole det type source + Synth.contractOf exprMd ty fn source (by rw [h_node]) + | .Abstract => pure (Synth.abstract source) + | .All => pure (Synth.all source) + | .Hole det type => Synth.hole det type source return ({ val := val', source := source }, ty) termination_by (exprMd, 2) decreasing_by all_goals first @@ -651,25 +653,25 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := The right principle for new call sites is: when the position has a known expected type (`TBool` for conditions, numeric for `decreases`, the declared output for a constant initializer or a functional body), - use `checkStmtExpr`. When it doesn't, use `resolveStmtExpr` (a thin - wrapper that calls `synthStmtExpr` and discards the synthesized type, + use `Check.resolveStmtExpr`. When it doesn't, use `resolveStmtExpr` (a thin + wrapper that calls `Synth.resolveStmtExpr` and discards the synthesized type, used at sites where typing is not enforced — verification annotations, - modifies/reads clauses). `synthStmtExpr` itself is mostly an internal + modifies/reads clauses). `Synth.resolveStmtExpr` itself is mostly an internal interface used by other rules. -/ -def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do +def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do match h_node: exprMd with | AstNode.mk expr source => match h_expr: expr with | .Block stmts label => - checkBlock exprMd stmts label expected source (by rw [h_node]) + Check.block exprMd stmts label expected source (by rw [h_node]) | .IfThenElse cond thenBr elseBr => - checkIfThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) + Check.ifThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) | .Assign targets value => - checkAssign exprMd targets value expected source (by rw [h_node]) - | .Hole det none => pure (checkHoleNone det expected source) + Check.assign exprMd targets value expected source (by rw [h_node]) + | .Hole det none => pure (Check.holeNone det expected source) | _ => -- Subsumption fallback: synth then check `actual <: expected`. - let (e', actual) ← synthStmtExpr exprMd + let (e', actual) ← Synth.resolveStmtExpr exprMd checkSubtype source expected actual pure e' termination_by (exprMd, 3) @@ -682,26 +684,26 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE -- ### Literals /-- Rule **Lit-Int**: `Γ ⊢ LiteralInt n ⇒ TInt`. -/ -def synthLitInt (v : Int) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litInt (v : Int) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralInt v, { val := .TInt, source := source }) /-- Rule **Lit-Bool**: `Γ ⊢ LiteralBool b ⇒ TBool`. -/ -def synthLitBool (v : Bool) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litBool (v : Bool) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralBool v, { val := .TBool, source := source }) /-- Rule **Lit-String**: `Γ ⊢ LiteralString s ⇒ TString`. -/ -def synthLitString (v : String) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litString (v : String) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralString v, { val := .TString, source := source }) /-- Rule **Lit-Decimal**: `Γ ⊢ LiteralDecimal d ⇒ TReal`. -/ -def synthLitDecimal (v : Decimal) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litDecimal (v : Decimal) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralDecimal v, { val := .TReal, source := source }) -- ### Variables /-- Rule **Var-Local**: `Γ(x) = T ⊢ Var (.Local x) ⇒ T`. Resolves `ref` against the lexical scope and reads its declared type. -/ -def synthVarLocal (ref : Identifier) (source : Option FileRange) : +def Synth.varLocal (ref : Identifier) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ref' ← resolveRef ref source let ty ← getVarType ref @@ -709,7 +711,7 @@ def synthVarLocal (ref : Identifier) (source : Option FileRange) : /-- Rule **Var-Declare**: extends the surrounding scope with `x : T` and synthesizes `TVoid` (the declaration itself produces no value). -/ -def synthVarDeclare (param : Parameter) (source : Option FileRange) : +def Synth.varDeclare (param : Parameter) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') @@ -718,11 +720,11 @@ def synthVarDeclare (param : Parameter) (source : Option FileRange) : /-- Rule **Var-Field**: `Γ ⊢ e ⇒ _, Γ(f) = T_f ⊢ Var (.Field e f) ⇒ T_f`. `f` is looked up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -/ -def synthVarField (exprMd : StmtExprMd) +def Synth.varField (exprMd : StmtExprMd) (target : StmtExprMd) (fieldName : Identifier) (source : Option FileRange) (h : exprMd.val = .Var (.Field target fieldName)) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source let ty ← getVarType fieldName' pure (.Var (.Field target' fieldName'), ty) @@ -751,19 +753,19 @@ def synthVarField (exprMd : StmtExprMd) type and the enclosing context's check (`[⇐] Sub`, or a containing `checkSubtype` like an assignment) surfaces any mismatch downstream against the then-branch's type. -/ -def synthIfThenElse (exprMd : StmtExprMd) +def Synth.ifThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } let voidTy : HighTypeMd := { val := .TVoid, source := exprMd.source } match elseBr with | none => - let thenBr' ← checkStmtExpr thenBr voidTy + let thenBr' ← Check.resolveStmtExpr thenBr voidTy pure (.IfThenElse cond' thenBr' none, voidTy) | some e => - let (thenBr', thenTy) ← synthStmtExpr thenBr - let (elseBr', elseTy) ← synthStmtExpr e + let (thenBr', thenTy) ← Synth.resolveStmtExpr thenBr + let (elseBr', elseTy) ← Synth.resolveStmtExpr e let ctx := (← get).typeContext pure (.IfThenElse cond' thenBr' (some elseBr'), joinTypes ctx thenTy elseTy) termination_by (exprMd, 1) @@ -784,12 +786,12 @@ def synthIfThenElse (exprMd : StmtExprMd) accepted, flagging it belongs to a lint). The last statement's type becomes the block's type, or `TVoid` for an empty block. The block opens a fresh nested scope, so bindings introduced inside don't escape. -/ -def synthBlock (exprMd : StmtExprMd) +def Synth.block (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (h : exprMd.val = .Block stmts label) : ResolveM (StmtExpr × HighTypeMd) := do withScope do - let results ← stmts.mapM synthStmtExpr + let results ← stmts.mapM Synth.resolveStmtExpr let stmts' := results.map (·.1) let lastTy := match results.getLast? with | some (_, ty) => ty @@ -807,17 +809,17 @@ def synthBlock (exprMd : StmtExprMd) `decreases` is resolved without a type check today (the intended target is a numeric type), body is synthesized; the construct itself synthesizes `TVoid`. -/ -def synthWhile (exprMd : StmtExprMd) +def Synth.while (exprMd : StmtExprMd) (cond : StmtExprMd) (invs : List StmtExprMd) (dec : Option StmtExprMd) (body : StmtExprMd) (h : exprMd.val = .While cond invs dec body) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } let invs' ← invs.attach.mapM (fun a => have := a.property; do - checkStmtExpr a.val { val := .TBool, source := a.val.source }) + Check.resolveStmtExpr a.val { val := .TBool, source := a.val.source }) let dec' ← dec.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') - let (body', _) ← synthStmtExpr body + let (e', _) ← Synth.resolveStmtExpr a.val; pure e') + let (body', _) ← Synth.resolveStmtExpr body pure (.While cond' invs' dec' body', { val := .TVoid, source := exprMd.source }) termination_by (exprMd, 1) decreasing_by @@ -830,7 +832,7 @@ def synthWhile (exprMd : StmtExprMd) omega /-- Rule **Exit**: `Γ ⊢ Exit target ⇒ TVoid`. -/ -def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.exit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := (.Exit target, { val := .TVoid, source := source }) /-- Rules **Return-None** / **Return-Some** / **Return-Void-Error** / @@ -851,15 +853,15 @@ def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTy declared output parameters); `return e` syntactically takes a single `Option StmtExpr` and cannot carry multiple values, so it is flagged with a diagnostic pointing users at the named-output convention. -/ -def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) +def Synth.return (exprMd : StmtExprMd) (source : Option FileRange) (val : Option StmtExprMd) (h : exprMd.val = .Return val) : ResolveM (StmtExpr × HighTypeMd) := do let expected := (← get).expectedReturnTypes let val' ← val.attach.mapM (fun a => have := a.property; do match expected with - | some [singleOutput] => checkStmtExpr a.val singleOutput - | _ => let (e', _) ← synthStmtExpr a.val; pure e') + | some [singleOutput] => Check.resolveStmtExpr a.val singleOutput + | _ => let (e', _) ← Synth.resolveStmtExpr a.val; pure e') -- Arity/shape diagnostics independent of the value's own type. match val, expected with | none, some [] => pure () @@ -892,21 +894,21 @@ def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) `Quantifier`. Empty blocks reduce to a subsumption check of `TVoid` against `expected` — the same check `[⇐] Block-Empty` performs when `T` admits `TVoid`. -/ -def checkBlock (exprMd : StmtExprMd) +def Check.block (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Block stmts label) : ResolveM StmtExprMd := do withScope do let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do have : s ∈ stmts := List.dropLast_subset stmts hMem - let (s', _) ← synthStmtExpr s; pure s') + let (s', _) ← Synth.resolveStmtExpr s; pure s') match _lastResult: stmts.getLast? with | none => checkSubtype source expected { val := .TVoid, source := source } pure { val := .Block init' label, source := source } | some last => have := List.mem_of_getLast? _lastResult - let last' ← checkStmtExpr last expected + let last' ← Check.resolveStmtExpr last expected pure { val := .Block (init' ++ [last']) label, source := source } termination_by (exprMd, 0) decreasing_by @@ -924,13 +926,13 @@ def checkBlock (exprMd : StmtExprMd) Without an else branch, the construct can only succeed when `expected` admits `TVoid` — the same subsumption check `[⇐] Block-Empty` performs for an empty block. -/ -def checkIfThenElse (exprMd : StmtExprMd) +def Check.ifThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : ResolveM StmtExprMd := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let thenBr' ← checkStmtExpr thenBr expected - let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } + let thenBr' ← Check.resolveStmtExpr thenBr expected + let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => Check.resolveStmtExpr e expected) if elseBr.isNone then checkSubtype source expected { val := .TVoid, source := source } pure { val := .IfThenElse cond' thenBr' elseBr', source := source } @@ -947,11 +949,11 @@ def checkIfThenElse (exprMd : StmtExprMd) /-- Rule **Assert**: `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ -def synthAssert (exprMd : StmtExprMd) +def Synth.assert (exprMd : StmtExprMd) (condExpr : StmtExprMd) (summary : Option String) (source : Option FileRange) (h : exprMd.val = .Assert ⟨condExpr, summary⟩) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } + let cond' ← Check.resolveStmtExpr condExpr { val := .TBool, source := condExpr.source } pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) termination_by (exprMd, 1) decreasing_by @@ -963,11 +965,11 @@ def synthAssert (exprMd : StmtExprMd) /-- Rule **Assume**: `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ -def synthAssume (exprMd : StmtExprMd) +def Synth.assume (exprMd : StmtExprMd) (cond : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assume cond) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } pure (.Assume cond', { val := .TVoid, source := source }) termination_by (exprMd, 1) decreasing_by @@ -991,8 +993,8 @@ def synthAssume (exprMd : StmtExprMd) value to assign. The construct synthesizes the RHS's type, so that expression-position assignments like `x ++ (y := s)` see a string in the second operand; statement-position uses are accommodated by - `checkAssign`, which accepts `TVoid` as the expected type. -/ -def synthAssign (exprMd : StmtExprMd) + `Check.assign`, which accepts `TVoid` as the expected type. -/ +def Synth.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : ResolveM (StmtExpr × HighTypeMd) := do @@ -1003,14 +1005,14 @@ def synthAssign (exprMd : StmtExprMd) let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← synthStmtExpr value + let (value', valueTy) ← Synth.resolveStmtExpr value let targetType (t : VariableMd) : ResolveM HighTypeMd := do match t.val with | .Local ref => getVarType ref @@ -1039,7 +1041,7 @@ def synthAssign (exprMd : StmtExprMd) statement of a block in an else-less `if` (whose branch is checked against `TVoid`) without firing a subsumption error against the RHS's type. For non-`TVoid` expected types, falls back to subsumption. -/ -def checkAssign (exprMd : StmtExprMd) +def Check.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : ResolveM StmtExprMd := do @@ -1050,14 +1052,14 @@ def checkAssign (exprMd : StmtExprMd) let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← synthStmtExpr value + let (value', valueTy) ← Synth.resolveStmtExpr value let targetType (t : VariableMd) : ResolveM HighTypeMd := do match t.val with | .Local ref => getVarType ref @@ -1089,13 +1091,13 @@ def checkAssign (exprMd : StmtExprMd) constant); each argument is synthesized and checked against the corresponding parameter type. The result type is the (possibly multi-valued) declared output type from `getCallInfo`. -/ -def synthStaticCall (exprMd : StmtExprMd) +def Synth.staticCall (exprMd : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) (h : exprMd.val = .StaticCall callee args) : ResolveM (StmtExpr × HighTypeMd) := do let callee' ← resolveRef callee source (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let results ← args.mapM synthStmtExpr + let results ← args.mapM Synth.resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -1113,15 +1115,15 @@ def synthStaticCall (exprMd : StmtExprMd) /-- Rule **Instance-Call**: target is synthesized; callee resolves to an instance or static procedure; arguments are checked pairwise against the callee's parameter types after dropping `self`. -/ -def synthInstanceCall (exprMd : StmtExprMd) +def Synth.instanceCall (exprMd : StmtExprMd) (target : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) (h : exprMd.val = .InstanceCall target callee args) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let callee' ← resolveRef callee source (expected := #[.instanceProcedure, .staticProcedure]) - let results ← args.mapM synthStmtExpr + let results ← args.mapM Synth.resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -1154,13 +1156,13 @@ def synthInstanceCall (exprMd : StmtExprMd) `int + real` passes since each operand individually passes `Numeric`; a proper fix needs numeric promotion or unification), `TString` for concatenation. -/ -def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) +def Synth.primitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) (op : Operation) (args : List StmtExprMd) (source : Option FileRange) (h_expr : expr = .PrimitiveOp op args) (h : exprMd.val = .PrimitiveOp op args) : ResolveM (StmtExpr × HighTypeMd) := do let _ := h_expr -- carries the constructor identity for `expr` in diagnostics - let results ← args.mapM synthStmtExpr + let results ← args.mapM Synth.resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let resultTy := match op with @@ -1206,7 +1208,7 @@ def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) /-- Rules **New-Ok** / **New-Fallback**: when `ref` resolves to a composite or datatype, the type is `UserDefined ref`; otherwise `Unknown` (suppresses cascading errors after the kind diagnostic has already fired). -/ -def synthNew (ref : Identifier) (source : Option FileRange) : +def Synth.new (ref : Identifier) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ref' ← resolveRef ref source (expected := #[.compositeType, .datatypeDefinition]) @@ -1223,11 +1225,11 @@ def synthNew (ref : Identifier) (source : Option FileRange) : cast is the user's claim. The synthesized type is `T`. `IsType` is the runtime test counterpart and synthesizes `TBool`. -/ -def synthAsType (exprMd : StmtExprMd) +def Synth.asType (exprMd : StmtExprMd) (target : StmtExprMd) (ty : HighTypeMd) (h : exprMd.val = .AsType target ty) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let ty' ← resolveHighType ty pure (.AsType target' ty', ty') termination_by (exprMd, 1) @@ -1238,11 +1240,11 @@ def synthAsType (exprMd : StmtExprMd) omega /-- Rule **IsType**: `target` is resolved; the synthesized type is `TBool`. -/ -def synthIsType (exprMd : StmtExprMd) +def Synth.isType (exprMd : StmtExprMd) (target : StmtExprMd) (ty : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .IsType target ty) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let ty' ← resolveHighType ty pure (.IsType target' ty', { val := .TBool, source := source }) termination_by (exprMd, 1) @@ -1259,14 +1261,14 @@ def synthIsType (exprMd : StmtExprMd) user-defined types, while `Cat === Animal` is accepted when `Cat` extends `Animal` (the gradual `Unknown` wildcard makes either side flow freely against the other). -/ -def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) +def Synth.refEq (exprMd : StmtExprMd) (expr : StmtExpr) (lhs rhs : StmtExprMd) (source : Option FileRange) (h_expr : expr = .ReferenceEquals lhs rhs) (h : exprMd.val = .ReferenceEquals lhs rhs) : ResolveM (StmtExpr × HighTypeMd) := do let _ := h_expr - let (lhs', lhsTy) ← synthStmtExpr lhs - let (rhs', rhsTy) ← synthStmtExpr rhs + let (lhs', lhsTy) ← Synth.resolveStmtExpr lhs + let (rhs', rhsTy) ← Synth.resolveStmtExpr rhs let ctx := (← get).typeContext unless isReference ctx lhsTy do typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy @@ -1289,14 +1291,14 @@ def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) `T_t` (or the enclosing instance type), and `newVal` checked against the field's declared type. The synthesized type is `T_t` — updating a field on a pure type produces a new value of the same type. -/ -def synthPureFieldUpdate (exprMd : StmtExprMd) +def Synth.pureFieldUpdate (exprMd : StmtExprMd) (target : StmtExprMd) (fieldName : Identifier) (newVal : StmtExprMd) (h : exprMd.val = .PureFieldUpdate target fieldName newVal) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', targetTy) ← synthStmtExpr target + let (target', targetTy) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName target.source let fieldTy ← getVarType fieldName' - let newVal' ← checkStmtExpr newVal fieldTy + let newVal' ← Check.resolveStmtExpr newVal fieldTy pure (.PureFieldUpdate target' fieldName' newVal', targetTy) termination_by (exprMd, 1) decreasing_by @@ -1313,7 +1315,7 @@ def synthPureFieldUpdate (exprMd : StmtExprMd) the body against `TBool` since a quantifier is a proposition. Without that body check, `forall x: int :: x + 1` would be silently accepted. The construct itself synthesizes `TBool`. -/ -def synthQuantifier (exprMd : StmtExprMd) +def Synth.quantifier (exprMd : StmtExprMd) (mode : QuantifierMode) (param : Parameter) (trigger : Option StmtExprMd) (body : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Quantifier mode param trigger body) : @@ -1322,8 +1324,8 @@ def synthQuantifier (exprMd : StmtExprMd) let paramTy' ← resolveHighType param.type let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do - let (e', _) ← synthStmtExpr pv.val; pure e') - let body' ← checkStmtExpr body { val := .TBool, source := body.source } + let (e', _) ← Synth.resolveStmtExpr pv.val; pure e') + let body' ← Check.resolveStmtExpr body { val := .TBool, source := body.source } pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) termination_by (exprMd, 1) decreasing_by @@ -1336,11 +1338,11 @@ def synthQuantifier (exprMd : StmtExprMd) /-- Rule **Assigned**: `name` is synthesized; the construct synthesizes `TBool`. -/ -def synthAssigned (exprMd : StmtExprMd) +def Synth.assigned (exprMd : StmtExprMd) (name : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assigned name) : ResolveM (StmtExpr × HighTypeMd) := do - let (name', _) ← synthStmtExpr name + let (name', _) ← Synth.resolveStmtExpr name pure (.Assigned name', { val := .TBool, source := source }) termination_by (exprMd, 1) decreasing_by @@ -1350,11 +1352,11 @@ def synthAssigned (exprMd : StmtExprMd) omega /-- Rule **Old**: `Γ ⊢ v ⇒ T ⊢ Old v ⇒ T`. -/ -def synthOld (exprMd : StmtExprMd) +def Synth.old (exprMd : StmtExprMd) (val : StmtExprMd) (h : exprMd.val = .Old val) : ResolveM (StmtExpr × HighTypeMd) := do - let (val', valTy) ← synthStmtExpr val + let (val', valTy) ← Synth.resolveStmtExpr val pure (.Old val', valTy) termination_by (exprMd, 1) decreasing_by @@ -1367,13 +1369,13 @@ def synthOld (exprMd : StmtExprMd) (`UserDefined` or `Unknown`) — `Fresh` only makes sense on heap-allocated references, so `fresh(5)` is rejected. The construct itself synthesizes `TBool`. -/ -def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) +def Synth.fresh (exprMd : StmtExprMd) (expr : StmtExpr) (val : StmtExprMd) (source : Option FileRange) (h_expr : expr = .Fresh val) (h : exprMd.val = .Fresh val) : ResolveM (StmtExpr × HighTypeMd) := do let _ := h_expr - let (val', valTy) ← synthStmtExpr val + let (val', valTy) ← Synth.resolveStmtExpr val unless isReference (← get).typeContext valTy do typeMismatch val'.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) @@ -1386,12 +1388,12 @@ def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) /-- Rule **ProveBy**: `v` and `proof` are both synthesized; the construct's type is `v`'s type — `proof` is a hint for downstream verification. -/ -def synthProveBy (exprMd : StmtExprMd) +def Synth.proveBy (exprMd : StmtExprMd) (val proof : StmtExprMd) (h : exprMd.val = .ProveBy val proof) : ResolveM (StmtExpr × HighTypeMd) := do - let (val', valTy) ← synthStmtExpr val - let (proof', _) ← synthStmtExpr proof + let (val', valTy) ← Synth.resolveStmtExpr val + let (proof', _) ← Synth.resolveStmtExpr proof pure (.ProveBy val' proof', valTy) termination_by (exprMd, 1) decreasing_by @@ -1411,7 +1413,7 @@ def synthProveBy (exprMd : StmtExprMd) wildcarded through `Unknown`. Otherwise an error is emitted ("'this' is not allowed outside instance methods") and the type collapses to `Unknown` to suppress cascading errors. -/ -def synthThis (source : Option FileRange) : +def Synth.this (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let s ← get match s.instanceTypeName with @@ -1429,11 +1431,11 @@ def synthThis (source : Option FileRange) : -- ### Untyped forms /-- Rule **Abstract**: synthesizes `Unknown`. -/ -def synthAbstract (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.abstract (source : Option FileRange) : StmtExpr × HighTypeMd := (.Abstract, { val := .Unknown, source := source }) /-- Rule **All**: synthesizes `Unknown`. -/ -def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.all (source : Option FileRange) : StmtExpr × HighTypeMd := (.All, { val := .Unknown, source := source }) -- ### ContractOf @@ -1458,11 +1460,11 @@ def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := `contractOf` production today, and the translator emits "not yet implemented" for it. The typing rule exists so resolution remains exhaustive over `StmtExpr`. -/ -def synthContractOf (exprMd : StmtExprMd) +def Synth.contractOf (exprMd : StmtExprMd) (ty : ContractType) (fn : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .ContractOf ty fn) : ResolveM (StmtExpr × HighTypeMd) := do - let (fn', _) ← synthStmtExpr fn + let (fn', _) ← Synth.resolveStmtExpr fn let s ← get let fnIsProcRef : Bool := match fn'.val with | .Var (.Local ref) => @@ -1492,7 +1494,7 @@ def synthContractOf (exprMd : StmtExprMd) /-- Rules **Hole-Some** / **Hole-None-Synth**: a typed hole synthesizes its annotation; an untyped hole in synth position synthesizes `Unknown`. -/ -def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : +def Synth.hole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do match type with | some ty => @@ -1514,16 +1516,19 @@ def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange the diagnostic *"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what would otherwise be a silent overwrite. -/ -def checkHoleNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : +def Check.holeNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : StmtExprMd := { val := .Hole det (some expected), source := source } -end +end -- mutual +end Resolution + +open Resolution /-- Resolve a statement expression, discarding the synthesized type. Use when only the resolved expression is needed (invariants, decreases, etc.). -/ private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do - let (e', _) ← synthStmtExpr e; pure e' + let (e', _) ← Synth.resolveStmtExpr e; pure e' /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do @@ -1535,7 +1540,7 @@ def resolveParameter (param : Parameter) : ResolveM Parameter := do def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do match body with | .Transparent b => - let (b', ty) ← synthStmtExpr b + let (b', ty) ← Synth.resolveStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => let posts' ← posts.mapM (·.mapM resolveStmtExpr) @@ -1656,8 +1661,8 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do -- in scope when resolving the constraint and witness expressions. let (valueName', constraint', witness') ← withScope do let valueName' ← defineNameCheckDup ct.valueName (.quantifierVar ct.valueName base') - let (constraint', _) ← synthStmtExpr ct.constraint - let (witness', _) ← synthStmtExpr ct.witness + let (constraint', _) ← Synth.resolveStmtExpr ct.constraint + let (witness', _) ← Synth.resolveStmtExpr ct.witness return (valueName', constraint', witness') return .Constrained { name := ctName', base := base', valueName := valueName', constraint := constraint', witness := witness' } @@ -1683,7 +1688,7 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM (checkStmtExpr · ty') + let init' ← c.initializer.mapM (Check.resolveStmtExpr · ty') let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 7583d4d079..b9d1070f42 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -160,8 +160,8 @@ mismatches against the surrounding context become diagnostics. The implementatio There are two operations on expressions, written here in standard bidirectional notation: ``` -Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) -Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) +Γ ⊢ e ⇒ T -- "e synthesizes T" (Synth.resolveStmtExpr) +Γ ⊢ e ⇐ T -- "e checks against T" (Check.resolveStmtExpr) ``` Synthesis returns a type inferred from the expression itself; checking verifies that the @@ -172,12 +172,12 @@ by a single change-of-direction rule, *subsumption*: $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` The two judgments are implemented as -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`: +{name Strata.Laurel.Resolution.Synth.resolveStmtExpr}`Synth.resolveStmtExpr` and +{name Strata.Laurel.Resolution.Check.resolveStmtExpr}`Check.resolveStmtExpr`: -{docstring Strata.Laurel.synthStmtExpr} +{docstring Strata.Laurel.Resolution.Synth.resolveStmtExpr} -{docstring Strata.Laurel.checkStmtExpr} +{docstring Strata.Laurel.Resolution.Check.resolveStmtExpr} ### Gradual typing @@ -238,43 +238,43 @@ Each LaTeX rule below is followed by the docstring of the helper that implements $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` -Fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` whenever no bespoke check +Fallback in {name Strata.Laurel.Resolution.Check.resolveStmtExpr}`Check.resolveStmtExpr` whenever no bespoke check rule applies. ### Literals $$`\frac{}{\Gamma \vdash \mathsf{LiteralInt}\;n \Rightarrow \mathsf{TInt}} \quad \text{([⇒] Lit-Int)}` -{docstring Strata.Laurel.synthLitInt} +{docstring Strata.Laurel.Resolution.Synth.litInt} $$`\frac{}{\Gamma \vdash \mathsf{LiteralBool}\;b \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Lit-Bool)}` -{docstring Strata.Laurel.synthLitBool} +{docstring Strata.Laurel.Resolution.Synth.litBool} $$`\frac{}{\Gamma \vdash \mathsf{LiteralString}\;s \Rightarrow \mathsf{TString}} \quad \text{([⇒] Lit-String)}` -{docstring Strata.Laurel.synthLitString} +{docstring Strata.Laurel.Resolution.Synth.litString} $$`\frac{}{\Gamma \vdash \mathsf{LiteralDecimal}\;d \Rightarrow \mathsf{TReal}} \quad \text{([⇒] Lit-Decimal)}` -{docstring Strata.Laurel.synthLitDecimal} +{docstring Strata.Laurel.Resolution.Synth.litDecimal} ### Variables $$`\frac{\Gamma(x) = T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Local}\;x) \Rightarrow T} \quad \text{([⇒] Var-Local)}` -{docstring Strata.Laurel.synthVarLocal} +{docstring Strata.Laurel.Resolution.Synth.varLocal} $$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Field}\;e\;f) \Rightarrow T_f} \quad \text{([⇒] Var-Field)}` -{docstring Strata.Laurel.synthVarField} +{docstring Strata.Laurel.Resolution.Synth.varField} $$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. -{docstring Strata.Laurel.synthVarDeclare} +{docstring Strata.Laurel.Resolution.Synth.varDeclare} ### Control flow @@ -282,13 +282,13 @@ $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vda $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If)}` -{docstring Strata.Laurel.synthIfThenElse} +{docstring Strata.Laurel.Resolution.Synth.ifThenElse} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}` -{docstring Strata.Laurel.checkIfThenElse} +{docstring Strata.Laurel.Resolution.Check.ifThenElse} $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` @@ -299,17 +299,17 @@ block. $$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` -{docstring Strata.Laurel.synthBlock} +{docstring Strata.Laurel.Resolution.Synth.block} $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` $$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` -{docstring Strata.Laurel.checkBlock} +{docstring Strata.Laurel.Resolution.Check.block} $$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` -{docstring Strata.Laurel.synthExit} +{docstring Strata.Laurel.Resolution.Synth.exit} $$`\frac{}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None)}` @@ -319,21 +319,21 @@ $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Ret $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}` -{docstring Strata.Laurel.synthReturn} +{docstring Strata.Laurel.Resolution.Synth.return} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Rightarrow \_}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}` -{docstring Strata.Laurel.synthWhile} +{docstring Strata.Laurel.Resolution.Synth.while} ### Verification statements $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}` -{docstring Strata.Laurel.synthAssert} +{docstring Strata.Laurel.Resolution.Synth.assert} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}` -{docstring Strata.Laurel.synthAssume} +{docstring Strata.Laurel.Resolution.Synth.assume} ### Assignment @@ -344,9 +344,9 @@ The target's declared type `T_i` comes from the variable's scope entry (for {name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. -{docstring Strata.Laurel.synthAssign} +{docstring Strata.Laurel.Resolution.Synth.assign} -{docstring Strata.Laurel.checkAssign} +{docstring Strata.Laurel.Resolution.Check.assign} ### Calls @@ -354,11 +354,11 @@ $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T_1; \ldots; T_n],\; n \ne 1 \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Static-Call-Multi)}` -{docstring Strata.Laurel.synthStaticCall} +{docstring Strata.Laurel.Resolution.Synth.staticCall} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_ \quad \Gamma(\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{InstanceCall}\;\mathit{target}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Instance-Call)}` -{docstring Strata.Laurel.synthInstanceCall} +{docstring Strata.Laurel.Resolution.Synth.instanceCall} ### Primitive operations @@ -376,7 +376,7 @@ $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \Gamma $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TString} \quad \mathit{op} = \mathsf{StrConcat}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TString}} \quad \text{([⇒] Op-Concat)}` -{docstring Strata.Laurel.synthPrimitiveOp} +{docstring Strata.Laurel.Resolution.Synth.primitiveOp} ### Object forms @@ -384,15 +384,15 @@ $$`\frac{\Gamma(\mathit{ref}) \text{ is a composite or datatype } T}{\Gamma \vda $$`\frac{\Gamma(\mathit{ref}) \text{ is not a composite or datatype}}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] New-Fallback)}` -{docstring Strata.Laurel.synthNew} +{docstring Strata.Laurel.Resolution.Synth.new} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsType}\;\mathit{target}\;T \Rightarrow T} \quad \text{([⇒] AsType)}` -{docstring Strata.Laurel.synthAsType} +{docstring Strata.Laurel.Resolution.Synth.asType} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` -{docstring Strata.Laurel.synthIsType} +{docstring Strata.Laurel.Resolution.Synth.isType} $$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r \quad T_l \sim T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` @@ -401,33 +401,33 @@ or {name Strata.Laurel.HighType.Unknown}`Unknown` type. `~` is the consistency r {name Strata.Laurel.isConsistent}`isConsistent` — symmetric, with the {name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. -{docstring Strata.Laurel.synthRefEq} +{docstring Strata.Laurel.Resolution.Synth.refEq} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` -{docstring Strata.Laurel.synthPureFieldUpdate} +{docstring Strata.Laurel.Resolution.Synth.pureFieldUpdate} ### Verification expressions $$`\frac{\Gamma, x : T \vdash \mathit{body} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Quantifier}\;\mathit{mode}\;\langle x, T\rangle\;\mathit{trig}\;\mathit{body} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Quantifier)}` -{docstring Strata.Laurel.synthQuantifier} +{docstring Strata.Laurel.Resolution.Synth.quantifier} $$`\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assigned}\;\mathit{name} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Assigned)}` -{docstring Strata.Laurel.synthAssigned} +{docstring Strata.Laurel.Resolution.Synth.assigned} $$`\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old)}` -{docstring Strata.Laurel.synthOld} +{docstring Strata.Laurel.Resolution.Synth.old} $$`\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}` -{docstring Strata.Laurel.synthFresh} +{docstring Strata.Laurel.Resolution.Synth.fresh} $$`\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy)}` -{docstring Strata.Laurel.synthProveBy} +{docstring Strata.Laurel.Resolution.Synth.proveBy} ### Self reference @@ -435,15 +435,15 @@ $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{some}\;T}{\Gamma \vdash \mat $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{none}}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{Unknown}\;\;[\text{emits “‘this’ is not allowed outside instance methods”}]} \quad \text{([⇒] This-Outside)}` -{docstring Strata.Laurel.synthThis} +{docstring Strata.Laurel.Resolution.Synth.this} ### Untyped forms $$`\frac{}{\Gamma \vdash \mathsf{Abstract}\;/\;\mathsf{All}\;\ldots \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Abstract / All)}` -{docstring Strata.Laurel.synthAbstract} +{docstring Strata.Laurel.Resolution.Synth.abstract} -{docstring Strata.Laurel.synthAll} +{docstring Strata.Laurel.Resolution.Synth.all} ### ContractOf @@ -453,7 +453,7 @@ $$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma $$`\frac{\mathit{fn} \text{ is not a procedure reference}}{\Gamma \vdash \mathsf{ContractOf}\;\ldots\;\mathit{fn} \rightsquigarrow \text{error: “‘contractOf’ expected a procedure reference”}} \quad \text{([⇒] ContractOf-Error)}` -{docstring Strata.Laurel.synthContractOf} +{docstring Strata.Laurel.Resolution.Synth.contractOf} ### Holes @@ -461,11 +461,11 @@ $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T) \Rightarrow T} \qu $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-None)}` -{docstring Strata.Laurel.synthHole} +{docstring Strata.Laurel.Resolution.Synth.hole} $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapsto\;\; \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}` -{docstring Strata.Laurel.checkHoleNone} +{docstring Strata.Laurel.Resolution.Check.holeNone} ## Future structural changes @@ -504,7 +504,7 @@ just wasted work and a maintenance hazard. `InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that \[⇐\] Hole-None writes the expected type during resolution for holes in check-mode positions, the post-pass only needs to handle holes in synth-only positions (e.g. call -arguments resolved through `synthStmtExpr` instead of `checkStmtExpr`). As more constructs +arguments resolved through `Synth.resolveStmtExpr` instead of `Check.resolveStmtExpr`). As more constructs gain bespoke check rules, fewer holes will reach `InferHoleTypes`; eventually the pass can be deleted entirely. From 2a3536af78c8c7c4500aecd8007baa5620da796e Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 18:21:08 +0000 Subject: [PATCH 058/128] Add type checking to Laurel resolution pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change resolveStmtExpr to return (StmtExprMd × HighTypeMd) - Add type checks for: - Boolean conditions in if/while/assert/assume - Numeric operands in arithmetic/comparison operations - Boolean operands in logical operations - Argument types matching parameter types in static calls - Argument types matching parameter types in instance calls - Assignment value type matching target type - Function body type matching declared output type - Report type mismatches as diagnostics (compilation continues) - Handle cascading errors: Unknown types are compatible with everything, UserDefined types skip strict checking (subtype relationships not tracked), void types skip assignment checks (statements don't produce values) Closes #1120 --- Strata/Languages/Laurel/Resolution.lean | 345 +++++++++++++++++------- 1 file changed, 253 insertions(+), 92 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 16bcf1333f..287382d3f9 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -326,7 +326,14 @@ def resolveHighType (ty : HighTypeMd) : ResolveM HighTypeMd := do | .UserDefined ref => let ref' ← resolveRef ref ty.source (expected := #[.compositeType, .constrainedType, .datatypeDefinition, .typeAlias]) - pure (.UserDefined ref') + -- If the reference resolved to the wrong kind, treat the type as Unknown to avoid cascading errors + let s ← get + let kindOk : Bool := match s.scope.get? ref.text with + | some (_, node) => node.kind == .unresolved || + (#[ResolvedNodeKind.compositeType, .constrainedType, .datatypeDefinition, .typeAlias].contains node.kind) + | none => true -- unresolved references already reported + if kindOk then pure (HighType.UserDefined ref') + else pure HighType.Unknown | .TTypedField vt => let vt' ← resolveHighType vt pure (.TTypedField vt') @@ -353,40 +360,119 @@ def resolveHighType (ty : HighTypeMd) : ResolveM HighTypeMd := do | other => pure other return { val := val', source := ty.source } -def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM StmtExprMd := do +/-- Emit a type mismatch diagnostic. -/ +private def typeMismatch (source : Option FileRange) (expected : String) (actual : HighTypeMd) : ResolveM Unit := do + let actualStr := toString (formatHighTypeVal actual.val) + let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" + modify fun s => { s with errors := s.errors.push diag } + +/-- Check that a type is boolean, emitting a diagnostic if not. -/ +private def checkBool (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do + match ty.val with + | .TBool | .Unknown => pure () + | .UserDefined _ => pure () -- constrained types may wrap bool + | _ => typeMismatch source "bool" ty + +/-- Check that a type is numeric (int, real, or float64), emitting a diagnostic if not. -/ +private def checkNumeric (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do + match ty.val with + | .TInt | .TReal | .TFloat64 | .Unknown => pure () + | .UserDefined _ => pure () -- constrained types may wrap numeric types + | _ => typeMismatch source "a numeric type" ty + +/-- Check that two types are compatible, emitting a diagnostic if not. + UserDefined types are always considered compatible with each other since + subtype relationships (inheritance) are not tracked during resolution. -/ +private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do + match expected.val, actual.val with + | .Unknown, _ => pure () + | _, .Unknown => pure () + | _, .MultiValuedExpr _ => pure () -- arity mismatch already reported separately + | .UserDefined _, _ => pure () -- subtype relationships not tracked here + | _, .UserDefined _ => pure () -- subtype relationships not tracked here + | _, _ => + if !highEq expected actual then + let expectedStr := toString (formatHighTypeVal expected.val) + let actualStr := toString (formatHighTypeVal actual.val) + let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" + modify fun s => { s with errors := s.errors.push diag } + +/-- Get the type of a resolved variable reference from scope. -/ +private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do + let s ← get + match s.scope.get? ref.text with + | some (_, node) => pure node.getType + | none => pure { val := .Unknown, source := ref.source } + +/-- Get the call return type and parameter types for a callee from scope. -/ +private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List HighTypeMd) := do + let s ← get + match s.scope.get? callee.text with + | some (_, .staticProcedure proc) => + let retTy := match proc.outputs with + | [singleOutput] => singleOutput.type + | outputs => { val := .MultiValuedExpr (outputs.map (·.type)), source := none } + pure (retTy, proc.inputs.map (·.type)) + | some (_, .instanceProcedure _ proc) => + let retTy := match proc.outputs with + | [singleOutput] => singleOutput.type + | outputs => { val := .MultiValuedExpr (outputs.map (·.type)), source := none } + pure (retTy, proc.inputs.map (·.type)) + | some (_, .datatypeConstructor t _) => + -- Testers (e.g. "Color..isRed") return Bool; constructors return the type + if (callee.text.splitOn "..is").length > 1 then + pure ({ val := .TBool, source := callee.source }, []) + else + pure ({ val := .UserDefined t, source := callee.source }, []) + | some (_, .parameter p) => pure (p.type, []) + | some (_, .constant c) => pure (c.type, []) + | _ => pure ({ val := .Unknown, source := callee.source }, []) + +def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match _: exprMd with | AstNode.mk expr source => - let val' ← match _: expr with + let (val', ty) ← match _: expr with | .IfThenElse cond thenBr elseBr => - let cond' ← resolveStmtExpr cond - let thenBr' ← resolveStmtExpr thenBr - let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - pure (.IfThenElse cond' thenBr' elseBr') + let (cond', condTy) ← resolveStmtExpr cond + checkBool cond'.source condTy + let (thenBr', thenTy) ← resolveStmtExpr thenBr + let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + pure (.IfThenElse cond' thenBr' elseBr', thenTy) | .Block stmts label => withScope do - let stmts' ← stmts.mapM resolveStmtExpr - pure (.Block stmts' label) + let results ← stmts.mapM resolveStmtExpr + let stmts' := results.map (·.1) + let lastTy := match results.getLast? with + | some (_, ty) => ty + | none => { val := .TVoid, source := source } + pure (.Block stmts' label, lastTy) | .While cond invs dec body => - let cond' ← resolveStmtExpr cond - let invs' ← invs.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - let dec' ← dec.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - let body' ← resolveStmtExpr body - pure (.While cond' invs' dec' body') - | .Exit target => pure (.Exit target) + let (cond', condTy) ← resolveStmtExpr cond + checkBool cond'.source condTy + let invs' ← invs.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + let dec' ← dec.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + let (body', _) ← resolveStmtExpr body + pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) + | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) | .Return val => do - let val' ← val.attach.mapM (fun a => have := a.property; resolveStmtExpr a.val) - pure (.Return val') - | .LiteralInt v => pure (.LiteralInt v) - | .LiteralBool v => pure (.LiteralBool v) - | .LiteralString v => pure (.LiteralString v) - | .LiteralDecimal v => pure (.LiteralDecimal v) + let val' ← val.attach.mapM (fun a => have := a.property; do + let (e', _) ← resolveStmtExpr a.val; pure e') + pure (.Return val', { val := .TVoid, source := source }) + | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) + | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) + | .LiteralString v => pure (.LiteralString v, { val := .TString, source := source }) + | .LiteralDecimal v => pure (.LiteralDecimal v, { val := .TReal, source := source }) | .Var (.Local ref) => let ref' ← resolveRef ref source - pure (.Var (.Local ref')) + let ty ← getVarType ref + pure (.Var (.Local ref'), ty) | .Var (.Declare param) => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (.Var (.Declare ⟨name', ty'⟩)) + pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) | .Assign targets value => let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do let ⟨vv, vs⟩ := v @@ -395,14 +481,14 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM StmtExprMd := do let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let value' ← resolveStmtExpr value + let (value', valueTy) ← resolveStmtExpr value -- Check that LHS target count matches the number of outputs from the RHS. -- This fires for procedure calls (which can have multiple outputs). -- Functions always have exactly 1 output in the model, so single-target function calls pass trivially. @@ -424,84 +510,144 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM StmtExprMd := do let diag := diagnosticFromSource source s!"Assignment target count mismatch: {targets'.length} targets but right-hand side produces {expectedOutputCount} values" modify fun s => { s with errors := s.errors.push diag } - pure (.Assign targets' value') + -- Type check: for single-target assignments, check value type matches target type + -- Skip when value type is void (RHS is a statement like while/return that doesn't produce a value) + if targets'.length == 1 && valueTy.val != HighType.TVoid then + if let some target := targets'.head? then + let targetTy := match target.val with + | .Local ref => do + let s ← get + match s.scope.get? ref.text with + | some (_, node) => pure node.getType + | none => pure { val := HighType.Unknown, source := ref.source : HighTypeMd } + | .Declare param => pure param.type + | .Field _ fieldName => do + let s ← get + match s.scope.get? fieldName.text with + | some (_, node) => pure node.getType + | none => pure { val := HighType.Unknown, source := fieldName.source : HighTypeMd } + let tTy ← targetTy + checkAssignable source tTy valueTy + pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - pure (.Var (.Field target' fieldName')) + let ty ← getVarType fieldName + pure (.Var (.Field target' fieldName'), ty) | .PureFieldUpdate target fieldName newVal => - let target' ← resolveStmtExpr target + let (target', targetTy) ← resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let newVal' ← resolveStmtExpr newVal - pure (.PureFieldUpdate target' fieldName' newVal') + let (newVal', _) ← resolveStmtExpr newVal + pure (.PureFieldUpdate target' fieldName' newVal', targetTy) | .StaticCall callee args => let callee' ← resolveRef callee source (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let args' ← args.mapM resolveStmtExpr - pure (.StaticCall callee' args') + let results ← args.mapM resolveStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + -- Check argument types match parameter types + for (argTy, paramTy) in argTypes.zip paramTypes do + checkAssignable source paramTy argTy + pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => - let args' ← args.mapM resolveStmtExpr - pure (.PrimitiveOp op args') + let results ← args.mapM resolveStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let resultTy := match op with + | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies + | .Lt | .Leq | .Gt | .Geq => HighType.TBool + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT => + match argTypes.head? with + | some headTy => headTy.val + | none => HighType.TInt + | .StrConcat => HighType.TString + -- Type check operands + match op with + | .And | .Or | .AndThen | .OrElse | .Not | .Implies => + for aTy in argTypes do checkBool source aTy + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => + for aTy in argTypes do checkNumeric source aTy + | .Eq | .Neq | .StrConcat => pure () + pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source (expected := #[.compositeType, .datatypeDefinition]) - pure (.New ref') - | .This => pure .This + -- If the reference resolved to the wrong kind, use Unknown type to avoid cascading errors + let s ← get + let kindOk : Bool := match s.scope.get? ref.text with + | some (_, node) => node.kind == .unresolved || + (#[ResolvedNodeKind.compositeType, .datatypeDefinition].contains node.kind) + | none => true + let ty := if kindOk then { val := HighType.UserDefined ref', source := source } + else { val := HighType.Unknown, source := source } + pure (.New ref', ty) + | .This => pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => - let lhs' ← resolveStmtExpr lhs - let rhs' ← resolveStmtExpr rhs - pure (.ReferenceEquals lhs' rhs') + let (lhs', _) ← resolveStmtExpr lhs + let (rhs', _) ← resolveStmtExpr rhs + pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let ty' ← resolveHighType ty - pure (.AsType target' ty') + pure (.AsType target' ty', ty') | .IsType target ty => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let ty' ← resolveHighType ty - pure (.IsType target' ty') + pure (.IsType target' ty', { val := .TBool, source := source }) | .InstanceCall target callee args => - let target' ← resolveStmtExpr target + let (target', _) ← resolveStmtExpr target let callee' ← resolveRef callee source (expected := #[.instanceProcedure, .staticProcedure]) - let args' ← args.mapM resolveStmtExpr - pure (.InstanceCall target' callee' args') + let results ← args.mapM resolveStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + -- Check argument types match parameter types (skip first param which is 'self') + let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] + for (argTy, paramTy) in argTypes.zip callParamTypes do + checkAssignable source paramTy argTy + pure (.InstanceCall target' callee' args', retTy) | .Quantifier mode param trigger body => withScope do let paramTy' ← resolveHighType param.type let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') - let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; resolveStmtExpr pv.val) - let body' ← resolveStmtExpr body - pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body') + let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do + let (e', _) ← resolveStmtExpr pv.val; pure e') + let (body', _) ← resolveStmtExpr body + pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) | .Assigned name => - let name' ← resolveStmtExpr name - pure (.Assigned name') + let (name', _) ← resolveStmtExpr name + pure (.Assigned name', { val := .TBool, source := source }) | .Old val => - let val' ← resolveStmtExpr val - pure (.Old val') + let (val', valTy) ← resolveStmtExpr val + pure (.Old val', valTy) | .Fresh val => - let val' ← resolveStmtExpr val - pure (.Fresh val') + let (val', _) ← resolveStmtExpr val + pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => - let cond' ← resolveStmtExpr condExpr - pure (.Assert { condition := cond', summary }) + let (cond', condTy) ← resolveStmtExpr condExpr + checkBool cond'.source condTy + pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) | .Assume cond => - let cond' ← resolveStmtExpr cond - pure (.Assume cond') + let (cond', condTy) ← resolveStmtExpr cond + checkBool cond'.source condTy + pure (.Assume cond', { val := .TVoid, source := source }) | .ProveBy val proof => - let val' ← resolveStmtExpr val - let proof' ← resolveStmtExpr proof - pure (.ProveBy val' proof') + let (val', valTy) ← resolveStmtExpr val + let (proof', _) ← resolveStmtExpr proof + pure (.ProveBy val' proof', valTy) | .ContractOf ty fn => - let fn' ← resolveStmtExpr fn - pure (.ContractOf ty fn') - | .Abstract => pure .Abstract - | .All => pure .All + let (fn', _) ← resolveStmtExpr fn + pure (.ContractOf ty fn', { val := .Unknown, source := source }) + | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) + | .All => pure (.All, { val := .Unknown, source := source }) | .Hole det type => match type with | some ty => let ty' ← resolveHighType ty - pure (.Hole det ty') - | none => pure (.Hole det none) - return { val := val', source := source } + pure (.Hole det ty', ty') + | none => pure (.Hole det none, { val := .Unknown, source := source }) + return ({ val := val', source := source }, ty) termination_by exprMd decreasing_by all_goals term_by_mem @@ -511,21 +657,21 @@ def resolveParameter (param : Parameter) : ResolveM Parameter := do let name' ← defineNameCheckDup param.name (.parameter ⟨param.name, ty'⟩) return ⟨name', ty'⟩ -/-- Resolve a procedure body. -/ -def resolveBody (body : Body) : ResolveM Body := do +/-- Resolve a procedure body. Returns the resolved body and its type. -/ +def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do match body with | .Transparent b => - let b' ← resolveStmtExpr b - return .Transparent b' + let (b', ty) ← resolveStmtExpr b + return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM resolveStmtExpr) - let impl' ← impl.mapM resolveStmtExpr - let mods' ← mods.mapM resolveStmtExpr - return .Opaque posts' impl' mods' + let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let impl' ← impl.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let mods' ← mods.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM resolveStmtExpr) - return .Abstract posts' - | .External => return .External + let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + return (.Abstract posts', { val := .TVoid, source := none }) + | .External => return (.External, { val := .TVoid, source := none }) /-- Resolve a procedure: resolve its name, then resolve params, contracts, and body in a new scope. -/ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do @@ -533,14 +679,22 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) - let dec' ← proc.decreases.mapM resolveStmtExpr - let body' ← resolveBody proc.body + let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" modify fun s => { s with errors := s.errors.push diag } - let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr + -- Check body type matches declared output type for functional procedures with transparent bodies + if proc.isFunctional && body'.isTransparent then + match proc.outputs with + | [singleOutput] => + -- Only check when body produces a value (not void from return/while/assign) + if bodyTy.val != HighType.TVoid then + checkAssignable proc.name.source singleOutput.type bodyTy + | _ => pure () + let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -566,14 +720,21 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) - let dec' ← proc.decreases.mapM resolveStmtExpr - let body' ← resolveBody proc.body + let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" modify fun s => { s with errors := s.errors.push diag } - let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr + -- Check body type matches declared output type for functional procedures with transparent bodies + if proc.isFunctional && body'.isTransparent then + match proc.outputs with + | [singleOutput] => + if bodyTy.val != HighType.TVoid then + checkAssignable proc.name.source singleOutput.type bodyTy + | _ => pure () + let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, @@ -615,8 +776,8 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do -- in scope when resolving the constraint and witness expressions. let (valueName', constraint', witness') ← withScope do let valueName' ← defineNameCheckDup ct.valueName (.quantifierVar ct.valueName base') - let constraint' ← resolveStmtExpr ct.constraint - let witness' ← resolveStmtExpr ct.witness + let (constraint', _) ← resolveStmtExpr ct.constraint + let (witness', _) ← resolveStmtExpr ct.witness return (valueName', constraint', witness') return .Constrained { name := ctName', base := base', valueName := valueName', constraint := constraint', witness := witness' } @@ -642,7 +803,7 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM resolveStmtExpr + let init' ← c.initializer.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } From 39b4f6c22c5e02c5d14548cf6f00f327a77e4e45 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 18:58:54 +0000 Subject: [PATCH 059/128] Fix type checking: skip TCore types in assignability check TCore is a pass-through type from Core that should not be checked during Laurel resolution. Without this, two identical TCore types (e.g. 'Core Any') would fail highEq (which has no TCore case) and produce spurious 'Type mismatch' diagnostics. --- Strata/Languages/Laurel/Resolution.lean | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 287382d3f9..43d8866d0d 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -390,6 +390,8 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) | _, .MultiValuedExpr _ => pure () -- arity mismatch already reported separately | .UserDefined _, _ => pure () -- subtype relationships not tracked here | _, .UserDefined _ => pure () -- subtype relationships not tracked here + | .TCore _, _ => pure () -- pass-through Core types not checked during resolution + | _, .TCore _ => pure () -- pass-through Core types not checked during resolution | _, _ => if !highEq expected actual then let expectedStr := toString (formatHighTypeVal expected.val) From bdae7ebfd1d2c457df8f4482d658e0ece0b3ab72 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:22:28 +0000 Subject: [PATCH 060/128] Simplify assignment arity check to use valueTy directly Derive expected output count from the RHS type (MultiValuedExpr gives the arity, otherwise 1) instead of re-looking up the procedure. This ensures LHS and RHS arity always match for assignments. --- Strata/Languages/Laurel/Resolution.lean | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 43d8866d0d..d87f97cd73 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -491,24 +491,11 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) let (value', valueTy) ← resolveStmtExpr value - -- Check that LHS target count matches the number of outputs from the RHS. - -- This fires for procedure calls (which can have multiple outputs). - -- Functions always have exactly 1 output in the model, so single-target function calls pass trivially. - let expectedOutputCount ← match value'.val with - | .StaticCall callee _ => do - let s ← get - match s.scope.get? callee.text with - | some (_, .staticProcedure proc) => pure proc.outputs.length - | some (_, .instanceProcedure _ proc) => pure proc.outputs.length - | _ => pure 1 - | .InstanceCall _ callee _ => do - let s ← get - match s.scope.get? callee.text with - | some (_, .instanceProcedure _ proc) => pure proc.outputs.length - | some (_, .staticProcedure proc) => pure proc.outputs.length - | _ => pure 1 - | _ => pure 1 - if targets'.length != expectedOutputCount then + -- Check that LHS target count matches the RHS arity (derived from the value type). + let expectedOutputCount := match valueTy.val with + | .MultiValuedExpr tys => tys.length + | _ => 1 + if valueTy.val != HighType.TVoid && targets'.length != expectedOutputCount then let diag := diagnosticFromSource source s!"Assignment target count mismatch: {targets'.length} targets but right-hand side produces {expectedOutputCount} values" modify fun s => { s with errors := s.errors.push diag } From f5302f971e4f977f625dd761942f1a548f83771d Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:26:32 +0000 Subject: [PATCH 061/128] Add tests for type checking error diagnostics in resolution pass Tests confirm that the following type errors are reported: - Non-boolean condition in if/assert/assume/while - Non-boolean operand in logical operators (&&) - Non-numeric operand in comparisons (<) - Assignment type mismatch (int := bool) - Function return type mismatch - Static call argument type mismatch --- .../Laurel/ResolutionTypeCheckTests.lean | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean new file mode 100644 index 0000000000..01ccd40708 --- /dev/null +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -0,0 +1,149 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +/- +Tests that the resolution pass detects type checking errors — e.g. using an int +where a bool is expected, or passing the wrong type to a procedure. +-/ + +import StrataTest.Util.TestDiagnostics +import Strata.DDM.Elab +import Strata.DDM.BuiltinDialects.Init +import Strata.Languages.Laurel.Grammar.LaurelGrammar +import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator +import Strata.Languages.Laurel.Resolution + +open StrataTest.Util +open Strata +open Strata.Elab (parseStrataProgramFromDialect) + +namespace Strata.Laurel + +/-- Run only parsing + resolution and return diagnostics (no SMT verification). -/ +private def processResolution (input : Lean.Parser.InputContext) : IO (Array Diagnostic) := do + let dialects := Strata.Elab.LoadedDialects.ofDialects! #[initDialect, Laurel] + let strataProgram ← parseStrataProgramFromDialect dialects Laurel.name input + let uri := Strata.Uri.file input.fileName + match Laurel.TransM.run uri (Laurel.parseProgram strataProgram) with + | .error e => throw (IO.userError s!"Translation errors: {e}") + | .ok program => + let result := resolve program + let files := Map.insert Map.empty uri input.fileMap + return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray + +/-! ## Non-boolean condition in if-then-else -/ + +def ifCondNotBool := r" +function foo(x: int): int { + if x then 1 else 0 +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "IfCondNotBool" ifCondNotBool 39 processResolution + +/-! ## Non-boolean condition in assert -/ + +def assertCondNotBool := r" +procedure baz() opaque { + var x: int := 42; + assert x +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssertCondNotBool" assertCondNotBool 49 processResolution + +/-! ## Non-boolean condition in assume -/ + +def assumeCondNotBool := r" +procedure qux() opaque { + var x: int := 42; + assume x +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssumeCondNotBool" assumeCondNotBool 59 processResolution + +/-! ## Non-boolean operand in logical and -/ + +def logicalAndNotBool := r" +function foo(x: int, y: bool): bool { + x && y +//^^^^^^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "LogicalAndNotBool" logicalAndNotBool 69 processResolution + +/-! ## Assignment type mismatch -/ + +def assignTypeMismatch := r" +procedure foo() opaque { + var x: int := true +//^^^^^^^^^^^^^^^^^^ error: expected 'int', but got 'bool' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssignTypeMismatch" assignTypeMismatch 79 processResolution + +/-! ## Function return type mismatch -/ + +def returnTypeMismatch := r" +function foo(): int { +// ^^^ error: expected 'int', but got 'bool' + true +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "ReturnTypeMismatch" returnTypeMismatch 89 processResolution + +/-! ## Static call argument type mismatch -/ + +def callArgTypeMismatch := r" +function bar(x: int): int { x }; +function foo(): int { + bar(true) +//^^^^^^^^^ error: expected 'int', but got 'bool' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "CallArgTypeMismatch" callArgTypeMismatch 99 processResolution + +/-! ## Non-boolean condition in while loop -/ + +def whileCondNotBool := r" +procedure wh() opaque { + var x: int := 1; + while (x) { } +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "WhileCondNotBool" whileCondNotBool 109 processResolution + +/-! ## Non-numeric operand in comparison -/ + +def comparisonNotNumeric := r" +function cmp(x: string, y: int): bool { + x < y +//^^^^^ error: expected a numeric type, but got 'string' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "ComparisonNotNumeric" comparisonNotNumeric 121 processResolution + +end Laurel From 7d00faed7089f4493e04a4e8d1266c6f14610ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 09:44:39 -0400 Subject: [PATCH 062/128] add explanations about the typechecking routine added --- Strata/Languages/Laurel/Resolution.lean | 77 +++++++-- docs/verso/LaurelDoc.lean | 215 ++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 14 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 4bfa2d39dc..e7155a7ca8 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -13,24 +13,73 @@ import Strata.Languages.Python.PythonLaurelCorePrelude /-! # Name Resolution Pass -Assigns a unique numeric ID to every definition and reference node in a -Laurel program, then resolves references to their definitions. +Turns a freshly parsed Laurel `Program` (where every `Identifier` has +`uniqueId := none`) into a program where every definition has a fresh numeric +ID and every reference points to the ID of the definition it names. The pass +also synthesizes a `HighType` for every `StmtExpr` and emits diagnostics for +unresolved names, duplicate definitions, kind mismatches (e.g. using a +constant where a type is expected), and type mismatches. + +The entry point is `resolve`. It returns a `ResolutionResult` containing the +resolved program, a `SemanticModel` (the `refToDef` map and ID counters), and +the accumulated diagnostics. ## Design -The resolution pass operates in two phases: +The resolution pass operates in two phases. ### Phase 1: ID Assignment and Reference Resolution -Walks the AST, assigning fresh unique IDs to all definition nodes and -resolving references by looking up names in the current lexical scope. -After this phase, every definition and reference node has its `id` field -filled in. + +Walks the AST under `ResolveM`, a state monad over `ResolveState`. Phase 1: +- assigns fresh unique IDs to all definition nodes via `defineNameCheckDup`, +- resolves references by looking up names in the current lexical scope via + `resolveRef` (and `resolveFieldRef` for fields, which uses the target's + declared type to build a qualified lookup key), +- opens fresh nested scopes via `withScope` for blocks, quantifiers, + procedure bodies, and constrained-type constraint/witness expressions, +- synthesizes a `HighType` for every `StmtExpr` and runs the type-checking + helpers (`checkBool`, `checkNumeric`, `checkAssignable`, `checkComparable`) + on assignments, call arguments, condition positions, functional bodies, and + constant initializers. + +Before any bodies are walked, `preRegisterTopLevel` registers every top-level +name (types and their constructors / testers / destructors / instance +procedures / fields, constants, static procedures) into scope with a +placeholder `ResolvedNode`. The placeholders are overwritten with real nodes +as each definition is fully resolved. This is what allows declaration order to +not matter inside a Laurel program. + +When a reference fails to resolve, or a `UserDefined` type reference resolves +to the wrong kind, Phase 1 records the name as `ResolvedNode.unresolved` (or +the type as `HighType.Unknown`) and continues. Both are treated as wildcards +by the type checker, so subsequent uses do not produce cascading errors. + +After this phase, every definition and reference node has its `uniqueId` +field filled in. ### Phase 2: Build refToDef Map + Walks the *resolved* AST (where all definitions already have their UUIDs) -and builds a map from each definition's ID to its `ResolvedNode`. Because this -happens after Phase 1, the `ResolvedNode` values in the map contain the fully -resolved sub-trees (e.g. a procedure's parameters already have their IDs). +and builds a map from each definition's ID to its `ResolvedNode`. Because +this happens after Phase 1, the `ResolvedNode` values in the map contain the +fully resolved sub-trees (e.g. a procedure's parameters already have their +IDs). + +### Scopes + +Three forms of scope are maintained on `ResolveState`: +- `scope` — the current lexical scope, mapping name → `(uniqueId, ResolvedNode)`, + saved and restored by `withScope`. +- `currentScopeNames` — names defined at the current nesting level only, used + by `defineNameCheckDup` to detect duplicates. +- `typeScopes` — per-composite-type scopes mapping field names to scope + entries. Built by `resolveTypeDefinition` *before* descending into instance + procedures (and inheriting from `extending` parents), so that field + references inside method bodies can be resolved. +- `instanceTypeName` — when resolving inside an instance procedure, the + owning composite type's name. Used by `resolveFieldRef` as a fallback so + that a bare `self.field` reference resolves through the type scope when + `self` has type `Any`. ### Definition nodes (introduce a name into scope) - `Variable.Declare` — local variable declaration (in `Assign` targets or `Var`) @@ -51,10 +100,10 @@ resolved sub-trees (e.g. a procedure's parameters already have their IDs). - `StmtExpr.Exit` — exit a labelled block - `HighType.UserDefined` — type reference -Each of these nodes carries an `id : Nat` field (defaulting to `0`). -The ID assignment pass fills in unique values. The resolution pass then -builds a map from reference IDs to `ResolvedNode` values describing the -definition each reference resolves to. +Each of these nodes carries a `uniqueId : Option Nat` field (defaulting to +`none`). Phase 1 fills in unique values; Phase 2 then builds a map from +reference IDs to `ResolvedNode` values describing the definition each +reference resolves to. -/ namespace Strata.Laurel diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4d153eb439..ef6014580d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -146,6 +146,221 @@ A Laurel program consists of procedures, global variables, type definitions, and {docstring Strata.Laurel.Program} +# Type checking + +Type checking runs as part of the resolution pass, in `resolveStmtExpr`. Resolution +synthesizes a {name Strata.Laurel.HighType}`HighType` for every {name Strata.Laurel.StmtExpr}`StmtExpr` +bottom-up and emits diagnostics when the synthesized type clashes with what its context +requires. + +## Type system at a glance + +The checker is *synthesis-only* (no inference, no subtyping) over a flat type lattice, with +three _wildcard_ types that disable checking: + +- {name Strata.Laurel.HighType.Unknown}`Unknown` — synthesized when a name fails to resolve, + when a {name Strata.Laurel.HighType.UserDefined}`UserDefined` reference resolves to the + wrong kind, or for constructs whose result type isn't tracked + ({name Strata.Laurel.StmtExpr.This}`This`, + {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, + {name Strata.Laurel.StmtExpr.All}`All`, + {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf`, untyped + {name Strata.Laurel.StmtExpr.Hole}`Hole`). It is compatible with everything in both + directions (acts like _any_). +- {name Strata.Laurel.HighType.UserDefined}`UserDefined _` — also treated bivariantly. + Subtype/inheritance relationships aren't tracked here, and a + {name Strata.Laurel.HighType.UserDefined}`UserDefined` may be a constrained type wrapping a + primitive, so it's accepted wherever a primitive is expected. +- {name Strata.Laurel.HighType.TCore}`TCore _` — pass-through types from the Core language; + never checked. + +Everything else ({name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`, +{name Strata.Laurel.HighType.TBool}`TBool`, +{name Strata.Laurel.HighType.TString}`TString`, +{name Strata.Laurel.HighType.TVoid}`TVoid`, +{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [..]`) is compared by +*structural equality* via {name Strata.Laurel.highEq}`highEq`. There is no implicit numeric +promotion: {name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, and +{name Strata.Laurel.HighType.TFloat64}`TFloat64` are siblings, not a chain. + +{name Strata.Laurel.HighType.TVoid}`TVoid` marks expressions that produce no value +({name Strata.Laurel.StmtExpr.Return}`Return`, +{name Strata.Laurel.StmtExpr.Exit}`Exit`, +{name Strata.Laurel.StmtExpr.While}`While`, +{name Strata.Laurel.StmtExpr.Assert}`Assert`, +{name Strata.Laurel.StmtExpr.Assume}`Assume`, +{name Strata.Laurel.Variable.Declare}`Var Declare`, opaque/abstract/external bodies). +{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr tys` models the result of a +procedure call with multiple outputs. + +## Checking judgments + +Four helper checks fire from context positions: + +- `checkBool` — accepts {name Strata.Laurel.HighType.TBool}`TBool`, + {name Strata.Laurel.HighType.Unknown}`Unknown`, or any + {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by + {name Strata.Laurel.StmtExpr.IfThenElse}`if`/{name Strata.Laurel.StmtExpr.While}`while` + conditions, logical primitive ops, + {name Strata.Laurel.StmtExpr.Assert}`Assert`, and + {name Strata.Laurel.StmtExpr.Assume}`Assume`. +- `checkNumeric` — accepts {name Strata.Laurel.HighType.TInt}`TInt`, + {name Strata.Laurel.HighType.TReal}`TReal`, + {name Strata.Laurel.HighType.TFloat64}`TFloat64`, + {name Strata.Laurel.HighType.Unknown}`Unknown`, or any + {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by arithmetic and ordering + primitive ops. +- `checkAssignable expected actual` — accepts equality under + {name Strata.Laurel.highEq}`highEq`, *or* either side being + {name Strata.Laurel.HighType.Unknown}`Unknown` / + {name Strata.Laurel.HighType.UserDefined}`UserDefined` / + {name Strata.Laurel.HighType.TCore}`TCore`. Used by assignment, call arguments, functional + body vs. declared output, and constant initializers. +- `checkComparable` — same wildcards as `checkAssignable`, but with a symmetric error message. + Used for the operands of {name Strata.Laurel.Operation.Eq}`==` and + {name Strata.Laurel.Operation.Neq}`!=`. + +The {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out in `checkBool` and +`checkNumeric` is conservative on purpose: a constrained type might wrap a +{name Strata.Laurel.HighType.TBool}`bool` or a numeric type. + +## Synthesis rules + +Literals synthesize their obvious primitive types: integers give +{name Strata.Laurel.HighType.TInt}`TInt`, booleans +{name Strata.Laurel.HighType.TBool}`TBool`, strings +{name Strata.Laurel.HighType.TString}`TString`, decimals +{name Strata.Laurel.HighType.TReal}`TReal`. Variable and field references take their type +from scope; a {name Strata.Laurel.Variable.Declare}`Var (.Declare p)` synthesizes +{name Strata.Laurel.HighType.TVoid}`TVoid` because it is a declaration statement. + +Control flow: +- {name Strata.Laurel.StmtExpr.IfThenElse}`if c then t else e_1; …; e_n` — `c` is checked + against bool; the result type is the _then_-branch type. Else-branch types are discarded. +- {name Strata.Laurel.StmtExpr.Block}`Block [s_1; …; s_n]` — the type is the last + statement's type, or {name Strata.Laurel.HighType.TVoid}`TVoid` if empty. This is what makes + a transparent functional body usable as a value. +- {name Strata.Laurel.StmtExpr.While}`While`, + {name Strata.Laurel.StmtExpr.Exit}`Exit`, + {name Strata.Laurel.StmtExpr.Return}`Return _`, + {name Strata.Laurel.StmtExpr.Assert}`Assert`, + {name Strata.Laurel.StmtExpr.Assume}`Assume` — all synthesize + {name Strata.Laurel.HighType.TVoid}`TVoid`. The condition positions of + {name Strata.Laurel.StmtExpr.While}`While`, + {name Strata.Laurel.StmtExpr.Assert}`Assert`, and + {name Strata.Laurel.StmtExpr.Assume}`Assume` enforce `checkBool`. + +Calls ({name Strata.Laurel.StmtExpr.StaticCall}`StaticCall`, +{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall`) synthesize each argument, then apply +`checkAssignable param arg` pairwise. +{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall` drops the first parameter (the +implicit `self`). The return type is determined as follows: +- procedure with one output → that output's type +- procedure with `n ≠ 1` outputs → + {name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [t_1, …, t_n]` +- datatype constructor whose name contains `..is` → + {name Strata.Laurel.HighType.TBool}`TBool` (testers) +- other datatype constructors → {name Strata.Laurel.HighType.UserDefined}`UserDefined T` +- parameters or constants in callee position → their declared type +- anything else → {name Strata.Laurel.HighType.Unknown}`Unknown` + +Primitive ops (see {name Strata.Laurel.Operation}`Operation`): +- {name Strata.Laurel.Operation.And}`And`, + {name Strata.Laurel.Operation.Or}`Or`, + {name Strata.Laurel.Operation.AndThen}`AndThen`, + {name Strata.Laurel.Operation.OrElse}`OrElse`, + {name Strata.Laurel.Operation.Not}`Not`, + {name Strata.Laurel.Operation.Implies}`Implies` — operands `checkBool`; result + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.Operation.Lt}`Lt`, + {name Strata.Laurel.Operation.Leq}`Leq`, + {name Strata.Laurel.Operation.Gt}`Gt`, + {name Strata.Laurel.Operation.Geq}`Geq` — operands `checkNumeric`; result + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.Operation.Eq}`Eq`, + {name Strata.Laurel.Operation.Neq}`Neq` — `checkComparable lhs rhs` (binary only); result + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.Operation.Neg}`Neg`, + {name Strata.Laurel.Operation.Add}`Add`, + {name Strata.Laurel.Operation.Sub}`Sub`, + {name Strata.Laurel.Operation.Mul}`Mul`, + {name Strata.Laurel.Operation.Div}`Div`, + {name Strata.Laurel.Operation.Mod}`Mod`, + {name Strata.Laurel.Operation.DivT}`DivT`, + {name Strata.Laurel.Operation.ModT}`ModT` — operands `checkNumeric`; result is the type of + the first argument. +- {name Strata.Laurel.Operation.StrConcat}`StrConcat` — no operand check; result + {name Strata.Laurel.HighType.TString}`TString`. + +The _result is the type of the first argument_ rule is how arithmetic handles +{name Strata.Laurel.HighType.TInt}`TInt` / {name Strata.Laurel.HighType.TReal}`TReal` / +{name Strata.Laurel.HighType.TFloat64}`TFloat64` without a unification step. A consequence: +`int + real` will not be flagged, since each operand individually passes `checkNumeric`. + +Other forms: +- {name Strata.Laurel.StmtExpr.New}`New T` synthesizes + {name Strata.Laurel.HighType.UserDefined}`UserDefined T`, falling back to + {name Strata.Laurel.HighType.Unknown}`Unknown` if `T` resolved to the wrong kind. +- {name Strata.Laurel.StmtExpr.AsType}`AsType e T` synthesizes `T`. + {name Strata.Laurel.StmtExpr.IsType}`IsType _ _` and + {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals` synthesize + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`, + {name Strata.Laurel.StmtExpr.Assigned}`Assigned`, + {name Strata.Laurel.StmtExpr.Fresh}`Fresh` synthesize + {name Strata.Laurel.HighType.TBool}`TBool`. +- {name Strata.Laurel.StmtExpr.Old}`Old e` and + {name Strata.Laurel.StmtExpr.ProveBy}`ProveBy val proof` propagate the type of their first + sub-expression. {name Strata.Laurel.StmtExpr.PureFieldUpdate}`PureFieldUpdate target …` + propagates the type of `target`. +- {name Strata.Laurel.StmtExpr.Hole}`Hole _ (some T)` synthesizes `T`. + {name Strata.Laurel.StmtExpr.Hole}`Hole _ none`, + {name Strata.Laurel.StmtExpr.This}`This`, + {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, + {name Strata.Laurel.StmtExpr.All}`All`, and + {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf` synthesize + {name Strata.Laurel.HighType.Unknown}`Unknown`. + +## Checking positions + +There is no separate checking mode — checking happens by synthesizing and then invoking one of +the four helpers above. The places that check: + +1. *Assignment.* Target count must equal RHS arity + ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr` length, else 1), suppressed + when RHS is {name Strata.Laurel.HighType.TVoid}`TVoid`. When single-target and arities + match, `checkAssignable target_ty value_ty` runs. +2. *Call arguments.* `checkAssignable param_ty arg_ty` for each pair (instance calls skip + `self`). +3. *Functional procedure body.* When a {name Strata.Laurel.Procedure}`Procedure` is + `isFunctional`, has a transparent body, exactly one output, and the body type is not + {name Strata.Laurel.HighType.TVoid}`TVoid`, `checkAssignable output_ty body_ty` runs. +4. *Constant initializer.* `checkAssignable declared_ty init_ty`, skipped when the + initializer is {name Strata.Laurel.HighType.TVoid}`TVoid`. + +## Summary + +In type-system terms, the checker is: + +- *monomorphic, structurally-equal, no-subtyping* over primitive types, +- with a *gradual / dynamic escape hatch* — {name Strata.Laurel.HighType.Unknown}`Unknown`, + {name Strata.Laurel.HighType.UserDefined}`UserDefined`, and + {name Strata.Laurel.HighType.TCore}`TCore` are bivariantly compatible with everything, so + unresolved names, user-defined types, and Core types never produce spurious mismatches, +- in *synthesis-only direction* (no contextual checking flowing into expressions), +- with *arity tracking via tuple types* + ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`) for multi-output + procedures, +- and *side-effecting expressions modeled as* + {name Strata.Laurel.HighType.TVoid}`TVoid` so blocks, returns, and loops compose cleanly. + +The wildcard carve-outs are the dominant design choice: the checker's behavior on +user-defined and unresolved-kind code is essentially _anything goes_, and strict checking +applies only between the built-in primitive types. + # Translation Pipeline Laurel programs are verified by translating them to Strata Core and then invoking the Core From 941f4aec63c01e1e2b147f34bbe67f7b416eef0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 11:54:47 -0400 Subject: [PATCH 063/128] bidirectional type checking first implementation : blocks --- Strata/Languages/Laurel/Resolution.lean | 155 ++++++++++++++++-------- 1 file changed, 102 insertions(+), 53 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index e7155a7ca8..b423e09304 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -423,6 +423,19 @@ private def typeMismatch (source : Option FileRange) (expected : String) (actual let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } +/-- Subtyping. Stub: structural equality via `highEq`. + TODO: To be replaced with a real check that walks `extending` chains for composites, unfolds aliases, and unwraps constrained types to their base. -/ +private def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup + +/-- Gradual consistency-subtyping (Siek–Taha style): `Unknown` is the dynamic + type and is consistent with everything in either direction. `TCore` is a + migration escape hatch and is bivariantly compatible for now. -/ +private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := + match sub.val, sup.val with + | .Unknown, _ | _, .Unknown => true + | .TCore _, _ | _, .TCore _ => true + | _, _ => isSubtype sub sup + /-- Check that a type is boolean, emitting a diagnostic if not. -/ private def checkBool (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do match ty.val with @@ -503,38 +516,41 @@ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List Hig | some (_, .constant c) => pure (c.type, []) | _ => pure ({ val := .Unknown, source := callee.source }, []) -def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do +def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match _: exprMd with | AstNode.mk expr source => let (val', ty) ← match _: expr with | .IfThenElse cond thenBr elseBr => - let (cond', condTy) ← resolveStmtExpr cond + let (cond', condTy) ← synthStmtExpr cond checkBool cond'.source condTy - let (thenBr', thenTy) ← resolveStmtExpr thenBr + let (thenBr', thenTy) ← synthStmtExpr thenBr let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') + let (e', _) ← synthStmtExpr a.val; pure e') pure (.IfThenElse cond' thenBr' elseBr', thenTy) | .Block stmts label => + -- Synth-mode block: non-last statements have their synthesized type discarded + -- (lax rule, matches Java/Python/JS expression-statement semantics). + -- The last statement's synthesized type becomes the block's type. withScope do - let results ← stmts.mapM resolveStmtExpr + let results ← stmts.mapM synthStmtExpr let stmts' := results.map (·.1) let lastTy := match results.getLast? with | some (_, ty) => ty | none => { val := .TVoid, source := source } pure (.Block stmts' label, lastTy) | .While cond invs dec body => - let (cond', condTy) ← resolveStmtExpr cond + let (cond', condTy) ← synthStmtExpr cond checkBool cond'.source condTy let invs' ← invs.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') + let (e', _) ← synthStmtExpr a.val; pure e') let dec' ← dec.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') - let (body', _) ← resolveStmtExpr body + let (e', _) ← synthStmtExpr a.val; pure e') + let (body', _) ← synthStmtExpr body pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) | .Return val => do let val' ← val.attach.mapM (fun a => have := a.property; do - let (e', _) ← resolveStmtExpr a.val; pure e') + let (e', _) ← synthStmtExpr a.val; pure e') pure (.Return val', { val := .TVoid, source := source }) | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) @@ -556,14 +572,14 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← resolveStmtExpr value + let (value', valueTy) ← synthStmtExpr value -- Check that LHS target count matches the RHS arity (derived from the value type). let expectedOutputCount := match valueTy.val with | .MultiValuedExpr tys => tys.length @@ -593,19 +609,19 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) checkAssignable source tTy valueTy pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source let ty ← getVarType fieldName pure (.Var (.Field target' fieldName'), ty) | .PureFieldUpdate target fieldName newVal => - let (target', targetTy) ← resolveStmtExpr target + let (target', targetTy) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let (newVal', _) ← resolveStmtExpr newVal + let (newVal', _) ← synthStmtExpr newVal pure (.PureFieldUpdate target' fieldName' newVal', targetTy) | .StaticCall callee args => let callee' ← resolveRef callee source (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let results ← args.mapM resolveStmtExpr + let results ← args.mapM synthStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -614,7 +630,7 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) checkAssignable source paramTy argTy pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => - let results ← args.mapM resolveStmtExpr + let results ← args.mapM synthStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let resultTy := match op with @@ -652,22 +668,22 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) pure (.New ref', ty) | .This => pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => - let (lhs', _) ← resolveStmtExpr lhs - let (rhs', _) ← resolveStmtExpr rhs + let (lhs', _) ← synthStmtExpr lhs + let (rhs', _) ← synthStmtExpr rhs pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let ty' ← resolveHighType ty pure (.AsType target' ty', ty') | .IsType target ty => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let ty' ← resolveHighType ty pure (.IsType target' ty', { val := .TBool, source := source }) | .InstanceCall target callee args => - let (target', _) ← resolveStmtExpr target + let (target', _) ← synthStmtExpr target let callee' ← resolveRef callee source (expected := #[.instanceProcedure, .staticProcedure]) - let results ← args.mapM resolveStmtExpr + let results ← args.mapM synthStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -681,32 +697,32 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let paramTy' ← resolveHighType param.type let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do - let (e', _) ← resolveStmtExpr pv.val; pure e') - let (body', _) ← resolveStmtExpr body + let (e', _) ← synthStmtExpr pv.val; pure e') + let (body', _) ← synthStmtExpr body pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) | .Assigned name => - let (name', _) ← resolveStmtExpr name + let (name', _) ← synthStmtExpr name pure (.Assigned name', { val := .TBool, source := source }) | .Old val => - let (val', valTy) ← resolveStmtExpr val + let (val', valTy) ← synthStmtExpr val pure (.Old val', valTy) | .Fresh val => - let (val', _) ← resolveStmtExpr val + let (val', _) ← synthStmtExpr val pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => - let (cond', condTy) ← resolveStmtExpr condExpr + let (cond', condTy) ← synthStmtExpr condExpr checkBool cond'.source condTy pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) | .Assume cond => - let (cond', condTy) ← resolveStmtExpr cond + let (cond', condTy) ← synthStmtExpr cond checkBool cond'.source condTy pure (.Assume cond', { val := .TVoid, source := source }) | .ProveBy val proof => - let (val', valTy) ← resolveStmtExpr val - let (proof', _) ← resolveStmtExpr proof + let (val', valTy) ← synthStmtExpr val + let (proof', _) ← synthStmtExpr proof pure (.ProveBy val' proof', valTy) | .ContractOf ty fn => - let (fn', _) ← resolveStmtExpr fn + let (fn', _) ← synthStmtExpr fn pure (.ContractOf ty fn', { val := .Unknown, source := source }) | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) | .All => pure (.All, { val := .Unknown, source := source }) @@ -721,8 +737,45 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) /-- Resolve a statement expression, discarding the synthesized type. Use when only the resolved expression is needed (invariants, decreases, etc.). -/ -private def resolveStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do - let (e', _) ← resolveStmtExpr e; pure e' +private def synthStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do + let (e', _) ← synthStmtExpr e; pure e' + +/-- Check-mode resolution: resolve `e` and verify its type is a consistent + subtype of `expected`. Bidirectional rules for individual constructs push + `expected` into subexpressions; everything else falls back to subsumption + (synth, then `isConsistentSubtype actual expected`). -/ +def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do + match _: exprMd with + | AstNode.mk expr source => + match _: expr with + | .Block stmts label => + -- Bespoke check rule: discard non-last statement types (lax), push + -- `expected` into the last statement. Empty block reduces to subsumption + -- of TVoid against `expected`. + -- The init traversal calls `synthStmtExpr`, a different function, so it + -- needs no termination proof; only the recursive `checkStmtExpr last` + -- call needs `last ∈ stmts`, supplied by `List.mem_of_getLast?`. + withScope do + let init' ← stmts.dropLast.mapM (fun s => do + let (s', _) ← synthStmtExpr s; pure s') + match _lastResult: stmts.getLast? with + | none => + let tvoid : HighTypeMd := { val := .TVoid, source := source } + unless isConsistentSubtype tvoid expected do + typeMismatch source (formatType expected) tvoid + pure { val := .Block init' label, source := source } + | some last => + have := List.mem_of_getLast? _lastResult + let last' ← checkStmtExpr last expected + pure { val := .Block (init' ++ [last']) label, source := source } + | _ => + -- Subsumption fallback: synth then check `actual <: expected`. + let (e', actual) ← synthStmtExpr exprMd + unless isConsistentSubtype actual expected do + typeMismatch source (formatType expected) actual + pure e' + termination_by exprMd + decreasing_by all_goals term_by_mem /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do @@ -734,15 +787,15 @@ def resolveParameter (param : Parameter) : ResolveM Parameter := do def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do match body with | .Transparent b => - let (b', ty) ← resolveStmtExpr b + let (b', ty) ← synthStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) - let impl' ← impl.mapM resolveStmtExprExpr - let mods' ← mods.mapM resolveStmtExprExpr + let posts' ← posts.mapM (·.mapM synthStmtExprExpr) + let impl' ← impl.mapM synthStmtExprExpr + let mods' ← mods.mapM synthStmtExprExpr return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) + let posts' ← posts.mapM (·.mapM synthStmtExprExpr) return (.Abstract posts', { val := .TVoid, source := none }) | .External => return (.External, { val := .TVoid, source := none }) @@ -752,8 +805,8 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) - let dec' ← proc.decreases.mapM resolveStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) + let dec' ← proc.decreases.mapM synthStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -767,7 +820,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -793,8 +846,8 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) - let dec' ← proc.decreases.mapM resolveStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) + let dec' ← proc.decreases.mapM synthStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -807,7 +860,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, @@ -849,8 +902,8 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do -- in scope when resolving the constraint and witness expressions. let (valueName', constraint', witness') ← withScope do let valueName' ← defineNameCheckDup ct.valueName (.quantifierVar ct.valueName base') - let (constraint', _) ← resolveStmtExpr ct.constraint - let (witness', _) ← resolveStmtExpr ct.witness + let (constraint', _) ← synthStmtExpr ct.constraint + let (witness', _) ← synthStmtExpr ct.witness return (valueName', constraint', witness') return .Constrained { name := ctName', base := base', valueName := valueName', constraint := constraint', witness := witness' } @@ -876,11 +929,7 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM fun e => do - let (e', eTy) ← resolveStmtExpr e - if eTy.val != HighType.TVoid then - checkAssignable e'.source ty' eTy - pure e' + let init' ← c.initializer.mapM (checkStmtExpr · ty') let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } From be1d8decb67fa2eb53333b52c595def904efd2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 11:57:13 -0400 Subject: [PATCH 064/128] add resolution-only function discards the type synthesized --- Strata/Languages/Laurel/Resolution.lean | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index b423e09304..bd49bd8376 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -737,7 +737,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := /-- Resolve a statement expression, discarding the synthesized type. Use when only the resolved expression is needed (invariants, decreases, etc.). -/ -private def synthStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do +private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do let (e', _) ← synthStmtExpr e; pure e' /-- Check-mode resolution: resolve `e` and verify its type is a consistent @@ -790,12 +790,12 @@ def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do let (b', ty) ← synthStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM synthStmtExprExpr) - let impl' ← impl.mapM synthStmtExprExpr - let mods' ← mods.mapM synthStmtExprExpr + let posts' ← posts.mapM (·.mapM resolveStmtExpr) + let impl' ← impl.mapM resolveStmtExpr + let mods' ← mods.mapM resolveStmtExpr return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM synthStmtExprExpr) + let posts' ← posts.mapM (·.mapM resolveStmtExpr) return (.Abstract posts', { val := .TVoid, source := none }) | .External => return (.External, { val := .TVoid, source := none }) @@ -805,8 +805,8 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) - let dec' ← proc.decreases.mapM synthStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) + let dec' ← proc.decreases.mapM resolveStmtExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -820,7 +820,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -846,8 +846,8 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM synthStmtExprExpr) - let dec' ← proc.decreases.mapM synthStmtExprExpr + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) + let dec' ← proc.decreases.mapM resolveStmtExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -860,7 +860,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM synthStmtExprExpr + let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, From 83e449f1fc6f4a72bb3eecf7da4b66d0882fa80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 12:10:32 -0400 Subject: [PATCH 065/128] document type system --- docs/verso/LaurelDoc.lean | 402 +++++++++++++++++++------------------- 1 file changed, 200 insertions(+), 202 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index ef6014580d..9f89926f4a 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -148,218 +148,216 @@ A Laurel program consists of procedures, global variables, type definitions, and # Type checking -Type checking runs as part of the resolution pass, in `resolveStmtExpr`. Resolution -synthesizes a {name Strata.Laurel.HighType}`HighType` for every {name Strata.Laurel.StmtExpr}`StmtExpr` -bottom-up and emits diagnostics when the synthesized type clashes with what its context -requires. - -## Type system at a glance - -The checker is *synthesis-only* (no inference, no subtyping) over a flat type lattice, with -three _wildcard_ types that disable checking: - -- {name Strata.Laurel.HighType.Unknown}`Unknown` — synthesized when a name fails to resolve, - when a {name Strata.Laurel.HighType.UserDefined}`UserDefined` reference resolves to the - wrong kind, or for constructs whose result type isn't tracked - ({name Strata.Laurel.StmtExpr.This}`This`, - {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, - {name Strata.Laurel.StmtExpr.All}`All`, - {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf`, untyped - {name Strata.Laurel.StmtExpr.Hole}`Hole`). It is compatible with everything in both - directions (acts like _any_). -- {name Strata.Laurel.HighType.UserDefined}`UserDefined _` — also treated bivariantly. - Subtype/inheritance relationships aren't tracked here, and a - {name Strata.Laurel.HighType.UserDefined}`UserDefined` may be a constrained type wrapping a - primitive, so it's accepted wherever a primitive is expected. -- {name Strata.Laurel.HighType.TCore}`TCore _` — pass-through types from the Core language; - never checked. - -Everything else ({name Strata.Laurel.HighType.TInt}`TInt`, -{name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`, -{name Strata.Laurel.HighType.TBool}`TBool`, -{name Strata.Laurel.HighType.TString}`TString`, -{name Strata.Laurel.HighType.TVoid}`TVoid`, -{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [..]`) is compared by -*structural equality* via {name Strata.Laurel.highEq}`highEq`. There is no implicit numeric -promotion: {name Strata.Laurel.HighType.TInt}`TInt`, -{name Strata.Laurel.HighType.TReal}`TReal`, and -{name Strata.Laurel.HighType.TFloat64}`TFloat64` are siblings, not a chain. - -{name Strata.Laurel.HighType.TVoid}`TVoid` marks expressions that produce no value -({name Strata.Laurel.StmtExpr.Return}`Return`, +Type checking is woven into the resolution pass: every +{name Strata.Laurel.StmtExpr}`StmtExpr` gets a {name Strata.Laurel.HighType}`HighType`, and +mismatches against the surrounding context become diagnostics. The design is +*bidirectional*: each construct is resolved either in *synthesis* mode — return a type +inferred from the expression — or in *checking* mode — verify that the expression has a +given expected type. The two are different functions on +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. + +This page describes the design choices behind the checker. The implementation is in +`Resolution.lean`. + +## The two judgments + +There are two operations on expressions, written here in standard bidirectional notation: + +``` +Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) +Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) +``` + +Each construct picks a mode based on whether its type is determined locally (synth) or by +context (check). Mode assignment is part of the design — see _Mode assignment per construct_ +below. + +The two judgments are connected by a single change-of-direction rule, *subsumption*: + +``` +Γ ⊢ e ⇒ A A <: B +───────────────────── (sub) + Γ ⊢ e ⇐ B +``` + +Subsumption is the *only* place the checker switches from check to synth mode. It fires as a +default fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct +without a bespoke check rule: synthesize the expression's type, then verify the result is a +subtype of the expected type. Bespoke check rules push the expected type *into* +subexpressions instead of bouncing through synthesis, which keeps error messages localized +and lets the expected type propagate through nested control flow. + +## Subtyping and gradual consistency + +The relation `<:` is implemented by two Lean functions — both currently stubs, both +intended to be sharpened: + +- `isSubtype` — pure subtyping. The stub is structural + equality via {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the + `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds + {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps + {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. +- `isConsistentSubtype` — gradual consistency, in + the Siek–Taha sense. {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type + `?` and is consistent with everything in either direction; otherwise the relation + delegates to `isSubtype`. {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly + consistent for now, as a clearly-labelled migration escape hatch from the Core language — + this carve-out is intentionally temporary. + +Subsumption (and every bespoke check rule) uses +`isConsistentSubtype`, never raw `isSubtype`. That +single choice is what makes the system *gradual*: an expression of type +{name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) +flows freely into any typed slot, and any expression flows freely into a slot of type +{name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between +fully-known types only. + +## What changed from the synth-only design + +A previous iteration was synth-only with three *bivariantly-compatible* wildcards: +{name Strata.Laurel.HighType.Unknown}`Unknown`, +{name Strata.Laurel.HighType.UserDefined}`UserDefined`, and +{name Strata.Laurel.HighType.TCore}`TCore`. The +{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was particularly +load-bearing: it meant that *no* assignment, call argument, or comparison involving a user +type was ever rejected, because subtyping wasn't tracked at all and constrained types +weren't unwrapped — we couldn't tell what was safe. + +The bidirectional design replaces that with two cleanly-separated concerns: + +- {name Strata.Laurel.HighType.Unknown}`Unknown` keeps wildcard semantics, but now as a + *real* semantic claim (gradual typing) rather than a workaround. +- {name Strata.Laurel.HighType.UserDefined}`UserDefined` becomes a regular type. Once + `isSubtype` is implemented properly, `Cat ≤ Animal` will + pass, `Cat ≤ Dog` will fail, and constrained types will be unwrappable to their base. The + current stub is conservative (structural equality only); it can be tightened + incrementally without changing any callers. + +## Block and `TVoid` + +Statement-position constructs that produce no value synthesize +{name Strata.Laurel.HighType.TVoid}`TVoid`: +{name Strata.Laurel.StmtExpr.Return}`Return`, {name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, {name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, -{name Strata.Laurel.Variable.Declare}`Var Declare`, opaque/abstract/external bodies). -{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr tys` models the result of a -procedure call with multiple outputs. - -## Checking judgments - -Four helper checks fire from context positions: - -- `checkBool` — accepts {name Strata.Laurel.HighType.TBool}`TBool`, - {name Strata.Laurel.HighType.Unknown}`Unknown`, or any - {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by - {name Strata.Laurel.StmtExpr.IfThenElse}`if`/{name Strata.Laurel.StmtExpr.While}`while` - conditions, logical primitive ops, - {name Strata.Laurel.StmtExpr.Assert}`Assert`, and - {name Strata.Laurel.StmtExpr.Assume}`Assume`. -- `checkNumeric` — accepts {name Strata.Laurel.HighType.TInt}`TInt`, - {name Strata.Laurel.HighType.TReal}`TReal`, - {name Strata.Laurel.HighType.TFloat64}`TFloat64`, - {name Strata.Laurel.HighType.Unknown}`Unknown`, or any - {name Strata.Laurel.HighType.UserDefined}`UserDefined`. Used by arithmetic and ordering - primitive ops. -- `checkAssignable expected actual` — accepts equality under - {name Strata.Laurel.highEq}`highEq`, *or* either side being - {name Strata.Laurel.HighType.Unknown}`Unknown` / - {name Strata.Laurel.HighType.UserDefined}`UserDefined` / - {name Strata.Laurel.HighType.TCore}`TCore`. Used by assignment, call arguments, functional - body vs. declared output, and constant initializers. -- `checkComparable` — same wildcards as `checkAssignable`, but with a symmetric error message. - Used for the operands of {name Strata.Laurel.Operation.Eq}`==` and - {name Strata.Laurel.Operation.Neq}`!=`. - -The {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out in `checkBool` and -`checkNumeric` is conservative on purpose: a constrained type might wrap a -{name Strata.Laurel.HighType.TBool}`bool` or a numeric type. - -## Synthesis rules - -Literals synthesize their obvious primitive types: integers give -{name Strata.Laurel.HighType.TInt}`TInt`, booleans -{name Strata.Laurel.HighType.TBool}`TBool`, strings -{name Strata.Laurel.HighType.TString}`TString`, decimals -{name Strata.Laurel.HighType.TReal}`TReal`. Variable and field references take their type -from scope; a {name Strata.Laurel.Variable.Declare}`Var (.Declare p)` synthesizes -{name Strata.Laurel.HighType.TVoid}`TVoid` because it is a declaration statement. - -Control flow: -- {name Strata.Laurel.StmtExpr.IfThenElse}`if c then t else e_1; …; e_n` — `c` is checked - against bool; the result type is the _then_-branch type. Else-branch types are discarded. -- {name Strata.Laurel.StmtExpr.Block}`Block [s_1; …; s_n]` — the type is the last - statement's type, or {name Strata.Laurel.HighType.TVoid}`TVoid` if empty. This is what makes - a transparent functional body usable as a value. -- {name Strata.Laurel.StmtExpr.While}`While`, - {name Strata.Laurel.StmtExpr.Exit}`Exit`, - {name Strata.Laurel.StmtExpr.Return}`Return _`, - {name Strata.Laurel.StmtExpr.Assert}`Assert`, - {name Strata.Laurel.StmtExpr.Assume}`Assume` — all synthesize - {name Strata.Laurel.HighType.TVoid}`TVoid`. The condition positions of - {name Strata.Laurel.StmtExpr.While}`While`, - {name Strata.Laurel.StmtExpr.Assert}`Assert`, and - {name Strata.Laurel.StmtExpr.Assume}`Assume` enforce `checkBool`. - -Calls ({name Strata.Laurel.StmtExpr.StaticCall}`StaticCall`, -{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall`) synthesize each argument, then apply -`checkAssignable param arg` pairwise. -{name Strata.Laurel.StmtExpr.InstanceCall}`InstanceCall` drops the first parameter (the -implicit `self`). The return type is determined as follows: -- procedure with one output → that output's type -- procedure with `n ≠ 1` outputs → - {name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr [t_1, …, t_n]` -- datatype constructor whose name contains `..is` → - {name Strata.Laurel.HighType.TBool}`TBool` (testers) -- other datatype constructors → {name Strata.Laurel.HighType.UserDefined}`UserDefined T` -- parameters or constants in callee position → their declared type -- anything else → {name Strata.Laurel.HighType.Unknown}`Unknown` - -Primitive ops (see {name Strata.Laurel.Operation}`Operation`): -- {name Strata.Laurel.Operation.And}`And`, - {name Strata.Laurel.Operation.Or}`Or`, - {name Strata.Laurel.Operation.AndThen}`AndThen`, - {name Strata.Laurel.Operation.OrElse}`OrElse`, - {name Strata.Laurel.Operation.Not}`Not`, - {name Strata.Laurel.Operation.Implies}`Implies` — operands `checkBool`; result - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.Operation.Lt}`Lt`, - {name Strata.Laurel.Operation.Leq}`Leq`, - {name Strata.Laurel.Operation.Gt}`Gt`, - {name Strata.Laurel.Operation.Geq}`Geq` — operands `checkNumeric`; result - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.Operation.Eq}`Eq`, - {name Strata.Laurel.Operation.Neq}`Neq` — `checkComparable lhs rhs` (binary only); result - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.Operation.Neg}`Neg`, - {name Strata.Laurel.Operation.Add}`Add`, - {name Strata.Laurel.Operation.Sub}`Sub`, - {name Strata.Laurel.Operation.Mul}`Mul`, - {name Strata.Laurel.Operation.Div}`Div`, - {name Strata.Laurel.Operation.Mod}`Mod`, - {name Strata.Laurel.Operation.DivT}`DivT`, - {name Strata.Laurel.Operation.ModT}`ModT` — operands `checkNumeric`; result is the type of - the first argument. -- {name Strata.Laurel.Operation.StrConcat}`StrConcat` — no operand check; result - {name Strata.Laurel.HighType.TString}`TString`. - -The _result is the type of the first argument_ rule is how arithmetic handles -{name Strata.Laurel.HighType.TInt}`TInt` / {name Strata.Laurel.HighType.TReal}`TReal` / -{name Strata.Laurel.HighType.TFloat64}`TFloat64` without a unification step. A consequence: -`int + real` will not be flagged, since each operand individually passes `checkNumeric`. - -Other forms: -- {name Strata.Laurel.StmtExpr.New}`New T` synthesizes - {name Strata.Laurel.HighType.UserDefined}`UserDefined T`, falling back to - {name Strata.Laurel.HighType.Unknown}`Unknown` if `T` resolved to the wrong kind. -- {name Strata.Laurel.StmtExpr.AsType}`AsType e T` synthesizes `T`. - {name Strata.Laurel.StmtExpr.IsType}`IsType _ _` and - {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals` synthesize - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`, - {name Strata.Laurel.StmtExpr.Assigned}`Assigned`, - {name Strata.Laurel.StmtExpr.Fresh}`Fresh` synthesize - {name Strata.Laurel.HighType.TBool}`TBool`. -- {name Strata.Laurel.StmtExpr.Old}`Old e` and - {name Strata.Laurel.StmtExpr.ProveBy}`ProveBy val proof` propagate the type of their first - sub-expression. {name Strata.Laurel.StmtExpr.PureFieldUpdate}`PureFieldUpdate target …` - propagates the type of `target`. -- {name Strata.Laurel.StmtExpr.Hole}`Hole _ (some T)` synthesizes `T`. - {name Strata.Laurel.StmtExpr.Hole}`Hole _ none`, - {name Strata.Laurel.StmtExpr.This}`This`, - {name Strata.Laurel.StmtExpr.Abstract}`Abstract`, - {name Strata.Laurel.StmtExpr.All}`All`, and - {name Strata.Laurel.StmtExpr.ContractOf}`ContractOf` synthesize - {name Strata.Laurel.HighType.Unknown}`Unknown`. - -## Checking positions - -There is no separate checking mode — checking happens by synthesizing and then invoking one of -the four helpers above. The places that check: - -1. *Assignment.* Target count must equal RHS arity - ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr` length, else 1), suppressed - when RHS is {name Strata.Laurel.HighType.TVoid}`TVoid`. When single-target and arities - match, `checkAssignable target_ty value_ty` runs. -2. *Call arguments.* `checkAssignable param_ty arg_ty` for each pair (instance calls skip - `self`). -3. *Functional procedure body.* When a {name Strata.Laurel.Procedure}`Procedure` is - `isFunctional`, has a transparent body, exactly one output, and the body type is not - {name Strata.Laurel.HighType.TVoid}`TVoid`, `checkAssignable output_ty body_ty` runs. -4. *Constant initializer.* `checkAssignable declared_ty init_ty`, skipped when the - initializer is {name Strata.Laurel.HighType.TVoid}`TVoid`. - -## Summary - -In type-system terms, the checker is: - -- *monomorphic, structurally-equal, no-subtyping* over primitive types, -- with a *gradual / dynamic escape hatch* — {name Strata.Laurel.HighType.Unknown}`Unknown`, - {name Strata.Laurel.HighType.UserDefined}`UserDefined`, and - {name Strata.Laurel.HighType.TCore}`TCore` are bivariantly compatible with everything, so - unresolved names, user-defined types, and Core types never produce spurious mismatches, -- in *synthesis-only direction* (no contextual checking flowing into expressions), +{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies. +This makes blocks compose cleanly: control-flow statements don't pollute a block's +synthesized type. + +A {name Strata.Laurel.StmtExpr.Block}`Block` is statement chaining `{ s_1; …; s_n }`. The +checker treats it permissively in two ways: + +1. *Non-last statements are not required to be {name Strata.Laurel.HighType.TVoid}`TVoid`.* + In synth mode their types are computed and discarded; in check mode they are still + synthesized rather than checked against `void`. This matches Java/Python/JavaScript + expression-statement semantics: `f(x);` where `f` returns a value is normal idiomatic + code, and forcing an explicit discard would be hostile to the imperative style Laurel + targets. The cost is that `5;` (a literal in statement position) is silently accepted; if + we ever want to flag that, it should land as a lint, not a type error. + +2. *The last statement is the block's type.* Empty blocks have type + {name Strata.Laurel.HighType.TVoid}`TVoid`. This is what lets a transparent functional + procedure body be `{ … some statements …; expr }`. + +In check mode, the bespoke `Block` rule pushes the expected type into the *last* statement +rather than checking the block's synthesized type at the boundary. This buys two things: +errors fire at the actual offending sub-expression (e.g. inside a deeply nested +{name Strata.Laurel.StmtExpr.IfThenElse}`if`), and the expected type keeps propagating +through nested {name Strata.Laurel.StmtExpr.Block}`Block` / +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / +{name Strata.Laurel.StmtExpr.Hole}`Hole` / +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to subsumption of +{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. + +## Mode assignment per construct + +The intended mode for each construct (some are still being converted to bidirectional in +the implementation): + +| Construct | Mode | Notes | +|---|---|---| +| Literals, `Var .Local`, `Var .Field`, `New T`, `IsType`, `ReferenceEquals`, `Quantifier`, `Assigned`, `Fresh`, `Hole _ (some T)`, `StaticCall`, `InstanceCall` | synth | type is determined locally | +| `Var .Declare`, `Exit`, `Return`, `While`, `Assert`, `Assume`, `Assign` | synth ⇒ {name Strata.Laurel.HighType.TVoid}`TVoid` | side-effecting; condition operands checked inward | +| `IfThenElse cond t e_opt` | bespoke check | `cond ⇐ TBool`; `t ⇐ T`; `e ⇐ T` if present | +| `Block` | bespoke check | `s_1..s_{n-1}` synth, `s_n ⇐ T`; synth uses last's synthesized type | +| `Hole _ none` | bespoke check | check mode succeeds with `expected`; synth mode → `Unknown` | +| `AsType e T` | synth ⇒ `T` | the cast is the user's claim; no check on `e` | +| `Old`, `ProveBy v _`, `PureFieldUpdate t _ _` | propagate type of subexpr | unchanged | +| `This`, `Abstract`, `All`, `ContractOf` | synth ⇒ {name Strata.Laurel.HighType.Unknown}`Unknown` | type not tracked | + +{name Strata.Laurel.StmtExpr.PrimitiveOp}`PrimitiveOp` operands are checked inward against +the operator's expected operand type ({name Strata.Laurel.HighType.TBool}`TBool` for +logical, numeric for arithmetic and ordering, {name Strata.Laurel.HighType.TString}`TString` +for `StrConcat`). {name Strata.Laurel.Operation.Eq}`Eq`/{name Strata.Laurel.Operation.Neq}`Neq` +synthesize both operands and require consistency in either direction +(`isConsistentSubtype l r ∨ isConsistentSubtype r l`). + +Arithmetic ops `Neg`/`Add`/…/`ModT` synthesize *the type of the first argument*. This is how +the checker handles {name Strata.Laurel.HighType.TInt}`TInt` / +{name Strata.Laurel.HighType.TReal}`TReal` / {name Strata.Laurel.HighType.TFloat64}`TFloat64` +without a unification step. A consequence: `int + real` is not flagged today, since each +operand passes the numeric check individually. A real fix would be a numeric-promotion or +unification rule; for now this is a known relaxation. + +## Two helpers for resolution sites + +Some positions (procedure preconditions, decreases, invariants, postconditions, modifies +clauses, constrained-type witness, etc.) need resolution to run but the type of the +expression is either uninteresting or already known by another path. They use: + +- {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` — the full synth API, returning + `(StmtExprMd × HighTypeMd)`. +- {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` — the check API, returning the resolved + expression and verifying its type is a consistent subtype of the expected type. +- `resolveStmtExpr` — a thin wrapper that calls + `synthStmtExpr` and discards the synthesized type. Used at sites where typing is not + enforced (verification annotations, modifies/reads clauses). + +The right principle is: when the position has a known expected type +({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for `decreases`, the +declared output for a constant initializer or a functional body), use +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. When it doesn't, use +`resolveStmtExpr`. {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` +itself is mostly an internal interface used by other rules. + +## Returns and the expected return type + +`Return e` synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` (the construct itself +produces no value), but the *value being returned* should be checked against the enclosing +procedure's declared output type. The intended design: thread the expected return type +through {name Strata.Laurel.ResolveState}`ResolveState`, set it from `proc.outputs` in +{name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` before resolving the +body, and have the `Return` rule push the expected type into its value via +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. This closes a soundness gap in the +synth-only design where `return 0` in a `bool`-returning procedure was not caught (because +the body's overall synthesized type was {name Strata.Laurel.HighType.TVoid}`TVoid` and the +body-vs-output check was skipped on `TVoid`). + +## What this is, in type-system terms + +The checker is: + +- *bidirectional*, with a single subsumption rule at the synth↔check boundary, +- with a *gradual* relation (`isConsistentSubtype`) + rather than a strict one — {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic + type, justified by Laurel's targeting of dynamic source languages, +- over a *nominal-with-stubs* subtype relation + (`isSubtype`) — currently structural equality, intended to + walk inheritance chains and unwrap aliases / constrained types, - with *arity tracking via tuple types* ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`) for multi-output procedures, - and *side-effecting expressions modeled as* {name Strata.Laurel.HighType.TVoid}`TVoid` so blocks, returns, and loops compose cleanly. -The wildcard carve-outs are the dominant design choice: the checker's behavior on -user-defined and unresolved-kind code is essentially _anything goes_, and strict checking -applies only between the built-in primitive types. +The wildcard carve-out for {name Strata.Laurel.HighType.UserDefined}`UserDefined` from the +previous design is gone — user-defined types are no longer a backdoor through the checker. +The {name Strata.Laurel.HighType.TCore}`TCore` carve-out is preserved for now as a +migration aid and is expected to be removed. # Translation Pipeline From a7d90d63ad44660a69dd60d3e9a7a5d5a78c9dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:26:06 -0400 Subject: [PATCH 066/128] ifthenelse type checking --- Strata/Languages/Laurel/Resolution.lean | 43 ++++++++++++++++--------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index bd49bd8376..97f6556331 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -516,17 +516,25 @@ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List Hig | some (_, .constant c) => pure (c.type, []) | _ => pure ({ val := .Unknown, source := callee.source }, []) +mutual def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match _: exprMd with | AstNode.mk expr source => let (val', ty) ← match _: expr with | .IfThenElse cond thenBr elseBr => - let (cond', condTy) ← synthStmtExpr cond - checkBool cond'.source condTy + -- Condition is checked against TBool. The result type is TVoid when the + -- else branch is absent (statement form: the then-branch's value is + -- discarded), otherwise the then-branch's synthesized type. We don't + -- compare the two branches against each other since statement-position + -- ifs commonly mix a value branch with a TVoid branch (return/exit). + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } let (thenBr', thenTy) ← synthStmtExpr thenBr let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do let (e', _) ← synthStmtExpr a.val; pure e') - pure (.IfThenElse cond' thenBr' elseBr', thenTy) + let resultTy := match elseBr with + | none => { val := .TVoid, source := source } + | some _ => thenTy + pure (.IfThenElse cond' thenBr' elseBr', resultTy) | .Block stmts label => -- Synth-mode block: non-last statements have their synthesized type discarded -- (lax rule, matches Java/Python/JS expression-statement semantics). @@ -732,13 +740,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.Hole det ty', ty') | none => pure (.Hole det none, { val := .Unknown, source := source }) return ({ val := val', source := source }, ty) - termination_by exprMd - decreasing_by all_goals term_by_mem - -/-- Resolve a statement expression, discarding the synthesized type. - Use when only the resolved expression is needed (invariants, decreases, etc.). -/ -private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do - let (e', _) ← synthStmtExpr e; pure e' + termination_by (exprMd, 0) + decreasing_by all_goals first + | (apply Prod.Lex.left; term_by_mem) + | (apply Prod.Lex.right; decide) /-- Check-mode resolution: resolve `e` and verify its type is a consistent subtype of `expected`. Bidirectional rules for individual constructs push @@ -752,11 +757,9 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE -- Bespoke check rule: discard non-last statement types (lax), push -- `expected` into the last statement. Empty block reduces to subsumption -- of TVoid against `expected`. - -- The init traversal calls `synthStmtExpr`, a different function, so it - -- needs no termination proof; only the recursive `checkStmtExpr last` - -- call needs `last ∈ stmts`, supplied by `List.mem_of_getLast?`. withScope do - let init' ← stmts.dropLast.mapM (fun s => do + let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do + have : s ∈ stmts := List.dropLast_subset stmts hMem let (s', _) ← synthStmtExpr s; pure s') match _lastResult: stmts.getLast? with | none => @@ -774,8 +777,16 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE unless isConsistentSubtype actual expected do typeMismatch source (formatType expected) actual pure e' - termination_by exprMd - decreasing_by all_goals term_by_mem + termination_by (exprMd, 1) + decreasing_by all_goals first + | (apply Prod.Lex.left; term_by_mem) + | (try subst_eqs; apply Prod.Lex.right; decide) +end + +/-- Resolve a statement expression, discarding the synthesized type. + Use when only the resolved expression is needed (invariants, decreases, etc.). -/ +private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do + let (e', _) ← synthStmtExpr e; pure e' /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do From 0958f26991d097f0846cbb599419142a311d10cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:28:11 -0400 Subject: [PATCH 067/128] document ifthenelse type checking --- docs/verso/LaurelDoc.lean | 57 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 9f89926f4a..c3d9a314f2 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -272,6 +272,61 @@ through nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to subsumption of {name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. +## IfThenElse + +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse cond t e_opt` has been converted to +*partial* bidirectional form. Today the implementation has a synth rule but reaches check +mode only through the subsumption fallback; a bespoke check rule that pushes the expected +type into both branches is the planned next step. + +The synth rule: + +- *Condition.* `cond` is checked against {name Strata.Laurel.HighType.TBool}`TBool` via a + recursive `checkStmtExpr cond TBool` call. This replaces the previous synth-then-`checkBool` + pattern with the clean bidirectional one — the expected type is pushed inward, so a + literal `if 5 then …` flags the literal directly rather than the surrounding `if`. +- *Branches.* `thenBr` is synthesized; if present, `elseBr` is synthesized too. The two + branch types are *not* compared against each other. The reason is that in Laurel's + unified statement-expression model, statement-position `if`s commonly mix a value + branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch (early + {name Strata.Laurel.StmtExpr.Return}`return`, {name Strata.Laurel.StmtExpr.Exit}`exit`, an + {name Strata.Laurel.StmtExpr.Assert}`assert`, …), which a strict equality check on + branches would reject incorrectly. +- *Result type.* When `elseBr` is `none`, the result is + {name Strata.Laurel.HighType.TVoid}`TVoid` — the construct is in statement form and the + then-branch's value is discarded. When `elseBr` is `some _`, the result is the + then-branch's synthesized type. The arbitrary preference for the then-branch here is + harmless: the result is always consumed by an enclosing `checkAssignable` / + subsumption-fallback, which gives a one-sided check against the surrounding context's + expected type. + +The change to `none` → {name Strata.Laurel.HighType.TVoid}`TVoid` closes a soundness gap in +the previous design, where `if c then 5` synthesized {name Strata.Laurel.HighType.TInt}`TInt` +unconditionally — even though there is no value when `c` is false — so an assignment +`x: int := if c then 5` would have type-checked. With the new rule, the synthesized type is +{name Strata.Laurel.HighType.TVoid}`TVoid` and the assignment is correctly rejected. + +The planned bespoke check rule is straightforward: `cond ⇐ TBool`, `thenBr ⇐ expected`, and +`elseBr ⇐ expected` if present; if absent, fall back to subsumption of +{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. The benefit is the +same as for `Block`: errors fire at the offending sub-expression rather than the +surrounding `if`, and the expected type propagates through nested control flow. + +## Mutual recursion and termination + +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` are now mutually recursive: the synth rule +for {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for +the condition, and the check function falls back to synth via the subsumption rule. + +Termination uses a lexicographic measure `(exprMd, tag)` where the tag is `0` for +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and `1` for +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. Any descent into a strict subterm +decreases via `Prod.Lex.left` (first component shrinks); the subsumption rule +`check e → synth e` calls synth on the *same* expression, which decreases via +`Prod.Lex.right` (second component goes from 1 to 0). This is the standard well-founded +encoding for bidirectional systems where one direction calls the other on the same input. + ## Mode assignment per construct The intended mode for each construct (some are still being converted to bidirectional in @@ -281,7 +336,7 @@ the implementation): |---|---|---| | Literals, `Var .Local`, `Var .Field`, `New T`, `IsType`, `ReferenceEquals`, `Quantifier`, `Assigned`, `Fresh`, `Hole _ (some T)`, `StaticCall`, `InstanceCall` | synth | type is determined locally | | `Var .Declare`, `Exit`, `Return`, `While`, `Assert`, `Assume`, `Assign` | synth ⇒ {name Strata.Laurel.HighType.TVoid}`TVoid` | side-effecting; condition operands checked inward | -| `IfThenElse cond t e_opt` | bespoke check | `cond ⇐ TBool`; `t ⇐ T`; `e ⇐ T` if present | +| `IfThenElse cond t e_opt` | synth (`cond ⇐ TBool`); planned bespoke check | see below | | `Block` | bespoke check | `s_1..s_{n-1}` synth, `s_n ⇐ T`; synth uses last's synthesized type | | `Hole _ none` | bespoke check | check mode succeeds with `expected`; synth mode → `Unknown` | | `AsType e T` | synth ⇒ `T` | the cast is the user's claim; no check on `e` | From 2a513c7a580c03cfe336cee47887ff17d1ce4f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:33:24 -0400 Subject: [PATCH 068/128] typechecking description refactor general design rules (one section per rule) --- docs/verso/LaurelDoc.lean | 392 +++++++++++++++++++++++++++----------- 1 file changed, 277 insertions(+), 115 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index c3d9a314f2..64dd119a59 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -234,128 +234,290 @@ The bidirectional design replaces that with two cleanly-separated concerns: current stub is conservative (structural equality only); it can be tightened incrementally without changing any callers. -## Block and `TVoid` - -Statement-position constructs that produce no value synthesize -{name Strata.Laurel.HighType.TVoid}`TVoid`: -{name Strata.Laurel.StmtExpr.Return}`Return`, -{name Strata.Laurel.StmtExpr.Exit}`Exit`, -{name Strata.Laurel.StmtExpr.While}`While`, -{name Strata.Laurel.StmtExpr.Assert}`Assert`, -{name Strata.Laurel.StmtExpr.Assume}`Assume`, -{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies. -This makes blocks compose cleanly: control-flow statements don't pollute a block's -synthesized type. - -A {name Strata.Laurel.StmtExpr.Block}`Block` is statement chaining `{ s_1; …; s_n }`. The -checker treats it permissively in two ways: - -1. *Non-last statements are not required to be {name Strata.Laurel.HighType.TVoid}`TVoid`.* - In synth mode their types are computed and discarded; in check mode they are still - synthesized rather than checked against `void`. This matches Java/Python/JavaScript - expression-statement semantics: `f(x);` where `f` returns a value is normal idiomatic - code, and forcing an explicit discard would be hostile to the imperative style Laurel - targets. The cost is that `5;` (a literal in statement position) is silently accepted; if - we ever want to flag that, it should land as a lint, not a type error. - -2. *The last statement is the block's type.* Empty blocks have type - {name Strata.Laurel.HighType.TVoid}`TVoid`. This is what lets a transparent functional - procedure body be `{ … some statements …; expr }`. - -In check mode, the bespoke `Block` rule pushes the expected type into the *last* statement -rather than checking the block's synthesized type at the boundary. This buys two things: -errors fire at the actual offending sub-expression (e.g. inside a deeply nested -{name Strata.Laurel.StmtExpr.IfThenElse}`if`), and the expected type keeps propagating -through nested {name Strata.Laurel.StmtExpr.Block}`Block` / +## Notation + +Typing rules are written in the standard derivation-tree form: premises above the line, +conclusion below, rule name on the right. + +``` +premise_1 premise_2 … premise_n +───────────────────────────────────── (Rule-Name) + conclusion +``` + +We use: + +- `e ⇒ T` — _e_ synthesizes _T_ (synth mode, `synthStmtExpr`). +- `e ⇐ T` — _e_ checks against _T_ (check mode, `checkStmtExpr`). +- `T <: U` — gradual consistency-subtyping, i.e. `isConsistentSubtype T U`. +- `Γ` for the lexical scope is left implicit — every rule threads it identically. + +Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This +includes {name Strata.Laurel.StmtExpr.Return}`Return`, +{name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, +{name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, +{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies +— they're recorded in the rules below. + +## Subsumption (the synth↔check boundary) + +``` +e ⇒ A A <: B +───────────────── (Sub) + e ⇐ B +``` + +Subsumption is the *only* place check switches to synth. It fires as the default fallback +in `checkStmtExpr` for every construct without a bespoke check rule. Bespoke check rules +push the expected type *into* subexpressions, which keeps errors localized. + +## Typing rules + +Below, each construct is given as a derivation. Rules marked with ✓ in the implementation +column are implemented today; rules marked ✗ are planned. The current implementation has +bespoke check rules for {name Strata.Laurel.StmtExpr.Block}`Block` only; everything else +reaches check mode through Sub. Where a synth rule pushes an expected type into a +subexpression (e.g. `cond ⇐ TBool` in {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), +that's listed as a premise. + +### Literals and references + +``` + (Lit-Int) ✓ +───────────── ──────────────── ───────────────── + LiteralInt n ⇒ TInt LiteralBool b ⇒ TBool LiteralString s ⇒ TString + +──────────────────────── Γ(x) = T + LiteralDecimal d ⇒ TReal ───────────────── (Var-Local) ✓ + Var (.Local x) ⇒ T + + e ⇒ _ Γ(f) = T_f Γ(x) ↦ T fresh +───────────────────────── (Var-Field) ✓ ───────────────────────── (Var-Declare) ✓ + Var (.Field e f) ⇒ T_f Var (.Declare ⟨x, T⟩) ⇒ TVoid +``` + +`Var (.Field e f)` resolves `f` against the type of `e` (or the enclosing instance type for +`self.f`); the typing rule is independent of which path resolution took. + +### IfThenElse + +``` +cond ⇐ TBool thenBr ⇒ T +───────────────────────────── (If-NoElse) ✓ + IfThenElse cond thenBr none ⇒ TVoid + +cond ⇐ TBool thenBr ⇒ T_t elseBr ⇒ T_e +───────────────────────────────────────────────── (If-Synth) ✓ + IfThenElse cond thenBr (some elseBr) ⇒ T_t + +cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T +───────────────────────────────────────────── (If-Check) ✗ (planned) + IfThenElse cond thenBr (some elseBr) ⇐ T +``` + +If-Synth picks the then-branch type by convention; the result is always consumed by an +enclosing `checkAssignable` or by Sub, which provides a one-sided check against the +surrounding context. The two branches are deliberately not compared against each other: +statement-position `if`s commonly mix a value branch with a +{name Strata.Laurel.HighType.TVoid}`TVoid` branch (early `return`, `exit`, `assert`, …), +which a strict equality check would reject incorrectly. + +If-NoElse synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value +to give back when `cond` is false. This rejects `x : int := if c then 5` at the assignment. + +### Block + +``` + none of these statements has a typing premise + (their synthesized types are discarded — lax) + ─────────────────────────────────────────── + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T + ──────────────────────────────────────────────────────── (Block-Synth) ✓ + Block [s_1; …; s_n] label ⇒ T + +──────────────────── (Block-Synth-Empty) ✓ + Block [] label ⇒ TVoid + + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T +───────────────────────────────────────────── (Block-Check) ✓ + Block [s_1; …; s_n] label ⇐ T + + TVoid <: T +───────────────────── (Block-Check-Empty) ✓ + Block [] label ⇐ T +``` + +Block-Synth is lax: non-last statements are synthesized but their types are discarded. +This matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` +returns a value is normal idiomatic code. The cost is that `5;` (a literal in statement +position) is silently accepted; flagging it would belong to a lint, not the type checker. + +Block-Check pushes the expected type into the *last* statement rather than checking the +block's synthesized type at the boundary. Errors then fire at the offending subexpression +inside `s_n` rather than at the surrounding {name Strata.Laurel.StmtExpr.Block}`Block`, and +the expected type keeps propagating through nested +{name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to subsumption of -{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. - -## IfThenElse - -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse cond t e_opt` has been converted to -*partial* bidirectional form. Today the implementation has a synth rule but reaches check -mode only through the subsumption fallback; a bespoke check rule that pushes the expected -type into both branches is the planned next step. - -The synth rule: - -- *Condition.* `cond` is checked against {name Strata.Laurel.HighType.TBool}`TBool` via a - recursive `checkStmtExpr cond TBool` call. This replaces the previous synth-then-`checkBool` - pattern with the clean bidirectional one — the expected type is pushed inward, so a - literal `if 5 then …` flags the literal directly rather than the surrounding `if`. -- *Branches.* `thenBr` is synthesized; if present, `elseBr` is synthesized too. The two - branch types are *not* compared against each other. The reason is that in Laurel's - unified statement-expression model, statement-position `if`s commonly mix a value - branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch (early - {name Strata.Laurel.StmtExpr.Return}`return`, {name Strata.Laurel.StmtExpr.Exit}`exit`, an - {name Strata.Laurel.StmtExpr.Assert}`assert`, …), which a strict equality check on - branches would reject incorrectly. -- *Result type.* When `elseBr` is `none`, the result is - {name Strata.Laurel.HighType.TVoid}`TVoid` — the construct is in statement form and the - then-branch's value is discarded. When `elseBr` is `some _`, the result is the - then-branch's synthesized type. The arbitrary preference for the then-branch here is - harmless: the result is always consumed by an enclosing `checkAssignable` / - subsumption-fallback, which gives a one-sided check against the surrounding context's - expected type. - -The change to `none` → {name Strata.Laurel.HighType.TVoid}`TVoid` closes a soundness gap in -the previous design, where `if c then 5` synthesized {name Strata.Laurel.HighType.TInt}`TInt` -unconditionally — even though there is no value when `c` is false — so an assignment -`x: int := if c then 5` would have type-checked. With the new rule, the synthesized type is -{name Strata.Laurel.HighType.TVoid}`TVoid` and the assignment is correctly rejected. - -The planned bespoke check rule is straightforward: `cond ⇐ TBool`, `thenBr ⇐ expected`, and -`elseBr ⇐ expected` if present; if absent, fall back to subsumption of -{name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. The benefit is the -same as for `Block`: errors fire at the offending sub-expression rather than the -surrounding `if`, and the expected type propagates through nested control flow. +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to a subsumption +check of {name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. + +### Statements that synthesize TVoid + +``` +───────────────── (Exit) ✓ cond ⇐ TBool invs ⇐ TBool dec ⇐ ? body ⇒ _ + Exit target ⇒ TVoid ──────────────────────────────────────────────────────────────── (While) ✓-ish + While cond invs dec body ⇒ TVoid + + +───────────────────────── (Return-None) ✓ e ⇒ _ + Return none ⇒ TVoid ───────────────────── (Return-Some) ✓ + Return (some e) ⇒ TVoid + + +cond ⇐ TBool cond ⇐ TBool +────────────────── (Assert) ✓-ish ────────────── (Assume) ✓-ish + Assert cond ⇒ TVoid Assume cond ⇒ TVoid + + + Γ(x) = T_x e ⇒ T_e T_e <: T_x targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i +───────────────────────────────────────── (Assign-Single) ✓-ish ─────────────────────────────────────────────────────────────────── (Assign-Multi) ✓-ish + Assign [x] e ⇒ TVoid Assign targets e ⇒ TVoid +``` + +✓-ish marks rules that are implemented but still call the legacy `checkBool` / +`checkAssignable` helpers rather than `checkStmtExpr cond TBool`. Functionally equivalent +under the gradual relation `<:` (since `checkBool` accepts the same types as +`isConsistentSubtype _ TBool` modulo the temporary {name Strata.Laurel.HighType.TCore}`TCore` +carve-out); slated to be migrated to `checkStmtExpr`. + +The {name Strata.Laurel.StmtExpr.Return}`Return`-with-value rule today only resolves `e` +without checking it against the enclosing procedure's declared output type. The intended +rule is: + +``` + Γ_proc.outputs = [T] e ⇐ T +───────────────────────────────── (Return-Some-Checked) ✗ (planned) + Return (some e) ⇒ TVoid +``` + +This requires threading the expected return type through `ResolveState`. Without it, +`return 0` in a `bool`-returning procedure goes uncaught. + +### Calls and primitive operations + +``` + callee resolves to procedure with inputs Ts and outputs [T] + args ⇒ Us U_i <: T_i (pairwise) +────────────────────────────────────────────────────────────── (Static-Call) ✓-ish + StaticCall callee args ⇒ T + + callee resolves to procedure with inputs Ts and outputs [T_1; …; T_n] (n ≠ 1) + args ⇒ Us U_i <: T_i (pairwise) +───────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi) ✓-ish + StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] + + target ⇒ _ callee resolves with inputs [self; Ts] and outputs [T] + args ⇒ Us U_i <: T_i (pairwise; self is dropped) +───────────────────────────────────────────────────────────────────────── (Instance-Call) ✓-ish + InstanceCall target callee args ⇒ T + + + args ⇐ TBool (each) +────────────────────────────── (Op-Bool) ✓-ish op ∈ {And, Or, AndThen, OrElse, Not, Implies} + PrimitiveOp op args ⇒ TBool + + + args ⇐ Numeric (each) +───────────────────────────── (Op-Cmp) ✓-ish op ∈ {Lt, Leq, Gt, Geq} + PrimitiveOp op args ⇒ TBool + + + lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l +────────────────────────────────────────────────────────── (Op-Eq) ✓-ish op ∈ {Eq, Neq} + PrimitiveOp op [lhs; rhs] ⇒ TBool + + + args ⇐ Numeric (each) args.head ⇒ T +────────────────────────────────────────── (Op-Arith) ✓-ish op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} + PrimitiveOp op args ⇒ T + + + args ⇐ TString (each) — current implementation: no operand check +───────────────────────────── (Op-Concat) ✓-ish + PrimitiveOp op args ⇒ TString +``` + +`Numeric` abbreviates "consistent with one of +{name Strata.Laurel.HighType.TInt}`TInt`, {name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` +rather than a `checkStmtExpr` chain; equivalent under the gradual relation. + +Op-Arith's "result is the type of the first argument" rule handles `int + int → int`, +`real + real → real`, etc. without a unification step. A consequence: `int + real` is *not* +flagged because each operand individually passes the numeric check. A real fix would be a +numeric-promotion or unification rule; for now this is a known relaxation. + +Op-Concat currently performs no operand check; the rule above describes the intended +behavior. + +### Object-related and verification forms + +``` + ref resolves to a composite or datatype T +───────────────────────────────────────────── (New-Ok) ✓ otherwise New ref ⇒ Unknown + New ref ⇒ UserDefined T + + +───────────────── (This) ✓ ──────────────────────────── (Abstract / All / ContractOf) ✓ + This ⇒ Unknown Abstract / All / ContractOf … ⇒ Unknown + + + lhs ⇒ _ rhs ⇒ _ +───────────────────────── (RefEq) ✓ target ⇒ _ + ReferenceEquals lhs rhs ⇒ TBool ────────────────── (AsType) ✓ + AsType target T ⇒ T + + + target ⇒ _ body ⇒ _ +───────────────── (IsType) ✓ ────────────────────────── (Quantifier) ✓ + IsType target T ⇒ TBool Quantifier mode ⟨x, T⟩ trig body ⇒ TBool + + + name ⇒ _ v ⇒ T v ⇒ _ +───────────────── (Assigned) ✓ ──────────── (Old) ✓ ────────────── (Fresh) ✓ + Assigned name ⇒ TBool Old v ⇒ T Fresh v ⇒ TBool + + + v ⇒ T proof ⇒ _ target ⇒ T_t newVal ⇒ _ +────────────────────── (ProveBy) ✓ ───────────────────────────────── (PureFieldUpdate) ✓ + ProveBy v proof ⇒ T PureFieldUpdate target f newVal ⇒ T_t +``` + +### Holes + +``` + Unknown <: T +───────────────────── (Hole-Some) ✓ ───────────────────── (Hole-None-Synth) ✓ ───────────────────── (Hole-None-Check) ✗ (planned) + Hole d (some T) ⇒ T Hole d none ⇒ Unknown Hole d none ⇐ T +``` + +In check mode, `Hole d none ⇐ T` reduces to subsumption today (`Unknown <: T`, which always +holds). The planned bespoke rule would record the inferred `T` on the hole node so +downstream passes can see it, instead of leaving `none` until the hole-inference pass. ## Mutual recursion and termination -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` are now mutually recursive: the synth rule -for {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for -the condition, and the check function falls back to synth via the subsumption rule. +`synthStmtExpr` and `checkStmtExpr` are mutually recursive: the synth rule for +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for the +condition, and the check function falls back to synth via Sub. Termination uses a lexicographic measure `(exprMd, tag)` where the tag is `0` for -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and `1` for -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. Any descent into a strict subterm -decreases via `Prod.Lex.left` (first component shrinks); the subsumption rule -`check e → synth e` calls synth on the *same* expression, which decreases via -`Prod.Lex.right` (second component goes from 1 to 0). This is the standard well-founded -encoding for bidirectional systems where one direction calls the other on the same input. - -## Mode assignment per construct - -The intended mode for each construct (some are still being converted to bidirectional in -the implementation): - -| Construct | Mode | Notes | -|---|---|---| -| Literals, `Var .Local`, `Var .Field`, `New T`, `IsType`, `ReferenceEquals`, `Quantifier`, `Assigned`, `Fresh`, `Hole _ (some T)`, `StaticCall`, `InstanceCall` | synth | type is determined locally | -| `Var .Declare`, `Exit`, `Return`, `While`, `Assert`, `Assume`, `Assign` | synth ⇒ {name Strata.Laurel.HighType.TVoid}`TVoid` | side-effecting; condition operands checked inward | -| `IfThenElse cond t e_opt` | synth (`cond ⇐ TBool`); planned bespoke check | see below | -| `Block` | bespoke check | `s_1..s_{n-1}` synth, `s_n ⇐ T`; synth uses last's synthesized type | -| `Hole _ none` | bespoke check | check mode succeeds with `expected`; synth mode → `Unknown` | -| `AsType e T` | synth ⇒ `T` | the cast is the user's claim; no check on `e` | -| `Old`, `ProveBy v _`, `PureFieldUpdate t _ _` | propagate type of subexpr | unchanged | -| `This`, `Abstract`, `All`, `ContractOf` | synth ⇒ {name Strata.Laurel.HighType.Unknown}`Unknown` | type not tracked | - -{name Strata.Laurel.StmtExpr.PrimitiveOp}`PrimitiveOp` operands are checked inward against -the operator's expected operand type ({name Strata.Laurel.HighType.TBool}`TBool` for -logical, numeric for arithmetic and ordering, {name Strata.Laurel.HighType.TString}`TString` -for `StrConcat`). {name Strata.Laurel.Operation.Eq}`Eq`/{name Strata.Laurel.Operation.Neq}`Neq` -synthesize both operands and require consistency in either direction -(`isConsistentSubtype l r ∨ isConsistentSubtype r l`). - -Arithmetic ops `Neg`/`Add`/…/`ModT` synthesize *the type of the first argument*. This is how -the checker handles {name Strata.Laurel.HighType.TInt}`TInt` / -{name Strata.Laurel.HighType.TReal}`TReal` / {name Strata.Laurel.HighType.TFloat64}`TFloat64` -without a unification step. A consequence: `int + real` is not flagged today, since each -operand passes the numeric check individually. A real fix would be a numeric-promotion or -unification rule; for now this is a known relaxation. +`synthStmtExpr` and `1` for `checkStmtExpr`. Any descent into a strict subterm decreases +via `Prod.Lex.left` (first component shrinks); Sub calls synth on the *same* expression, +which decreases via `Prod.Lex.right` (second component goes from 1 to 0). This is the +standard well-founded encoding for bidirectional systems where one direction calls the +other on the same input. ## Two helpers for resolution sites From fcbe1fc9bdd0711753063faaa1eba357737d3469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:44:13 -0400 Subject: [PATCH 069/128] reformat typechecking section --- docs/verso/LaurelDoc.lean | 632 +++++++++++++++++++++----------------- 1 file changed, 351 insertions(+), 281 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 64dd119a59..1577232261 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -150,432 +150,502 @@ A Laurel program consists of procedures, global variables, type definitions, and Type checking is woven into the resolution pass: every {name Strata.Laurel.StmtExpr}`StmtExpr` gets a {name Strata.Laurel.HighType}`HighType`, and -mismatches against the surrounding context become diagnostics. The design is -*bidirectional*: each construct is resolved either in *synthesis* mode — return a type -inferred from the expression — or in *checking* mode — verify that the expression has a -given expected type. The two are different functions on -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. - -This page describes the design choices behind the checker. The implementation is in +mismatches against the surrounding context become diagnostics. The implementation is in `Resolution.lean`. -## The two judgments +## Design + +### Bidirectional type checking There are two operations on expressions, written here in standard bidirectional notation: ``` -Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) -Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) +e ⇒ T -- "e synthesizes T" (synthStmtExpr) +e ⇐ T -- "e checks against T" (checkStmtExpr) ``` -Each construct picks a mode based on whether its type is determined locally (synth) or by -context (check). Mode assignment is part of the design — see _Mode assignment per construct_ -below. - -The two judgments are connected by a single change-of-direction rule, *subsumption*: +Synthesis returns a type inferred from the expression itself; checking verifies that the +expression has a given expected type. Each construct picks a mode based on whether its type +is determined locally (synth) or by context (check). The two judgments are connected by a +single change-of-direction rule, *subsumption*: ``` -Γ ⊢ e ⇒ A A <: B -───────────────────── (sub) - Γ ⊢ e ⇐ B +e ⇒ A A <: B +───────────────── (Sub) + e ⇐ B ``` -Subsumption is the *only* place the checker switches from check to synth mode. It fires as a -default fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct -without a bespoke check rule: synthesize the expression's type, then verify the result is a -subtype of the expected type. Bespoke check rules push the expected type *into* -subexpressions instead of bouncing through synthesis, which keeps error messages localized -and lets the expected type propagate through nested control flow. - -## Subtyping and gradual consistency +Subsumption is the *only* place the checker switches from check to synth mode. It fires as +the default fallback in +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct without a bespoke +check rule: synthesize the expression's type, then verify the result is a subtype of the +expected type. Bespoke check rules push the expected type *into* subexpressions instead of +bouncing through synthesis, which keeps error messages localized and lets the expected type +propagate through nested control flow. + +`synthStmtExpr` and `checkStmtExpr` are mutually recursive: synth rules invoke check on +subexpressions whose expected type is known (e.g. `cond ⇐ TBool` in +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), and `checkStmtExpr` falls back to +`synthStmtExpr` via Sub. Termination uses a lexicographic measure `(exprMd, tag)` where the +tag is `0` for synth and `1` for check; any descent into a strict subterm decreases via +`Prod.Lex.left`, while Sub calls synth on the *same* expression and decreases via +`Prod.Lex.right`. This is the standard well-founded encoding for bidirectional systems. + +There is also a thin `resolveStmtExpr` wrapper that calls `synthStmtExpr` and discards the +synthesized type. It's used at sites where typing is not enforced (verification annotations, +modifies/reads clauses). The right principle for new call sites is: when the position has a +known expected type ({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for +`decreases`, the declared output for a constant initializer or a functional body), use +`checkStmtExpr`. When it doesn't, use `resolveStmtExpr`. `synthStmtExpr` itself is mostly an +internal interface used by other rules. + +### Gradual typing The relation `<:` is implemented by two Lean functions — both currently stubs, both intended to be sharpened: -- `isSubtype` — pure subtyping. The stub is structural - equality via {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the - `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds +- `isSubtype` — pure subtyping. The stub is structural equality via + {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the `extending` + chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. -- `isConsistentSubtype` — gradual consistency, in - the Siek–Taha sense. {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type - `?` and is consistent with everything in either direction; otherwise the relation - delegates to `isSubtype`. {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly - consistent for now, as a clearly-labelled migration escape hatch from the Core language — - this carve-out is intentionally temporary. - -Subsumption (and every bespoke check rule) uses -`isConsistentSubtype`, never raw `isSubtype`. That -single choice is what makes the system *gradual*: an expression of type +- `isConsistentSubtype` — gradual consistency, in the Siek–Taha sense. + {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type `?` and is consistent + with everything in either direction; otherwise the relation delegates to `isSubtype`. + {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now, as a + clearly-labelled migration escape hatch from the Core language — this carve-out is + intentionally temporary. + +Subsumption (and every bespoke check rule) uses `isConsistentSubtype`, never raw +`isSubtype`. That single choice is what makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely into any typed slot, and any expression flows freely into a slot of type {name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between fully-known types only. -## What changed from the synth-only design - A previous iteration was synth-only with three *bivariantly-compatible* wildcards: {name Strata.Laurel.HighType.Unknown}`Unknown`, {name Strata.Laurel.HighType.UserDefined}`UserDefined`, and {name Strata.Laurel.HighType.TCore}`TCore`. The -{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was particularly -load-bearing: it meant that *no* assignment, call argument, or comparison involving a user -type was ever rejected, because subtyping wasn't tracked at all and constrained types -weren't unwrapped — we couldn't tell what was safe. +{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no +assignment, call argument, or comparison involving a user type was ever rejected. The +bidirectional design retires that carve-out — user-defined types are now a regular +participant in `<:`, and tightening `isSubtype` (to walk inheritance and unwrap +constrained types) gradually buys real checking on user-defined code without changing +callers. -The bidirectional design replaces that with two cleanly-separated concerns: +Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This +includes {name Strata.Laurel.StmtExpr.Return}`Return`, +{name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, +{name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, +{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies +— recorded in the rules below. -- {name Strata.Laurel.HighType.Unknown}`Unknown` keeps wildcard semantics, but now as a - *real* semantic claim (gradual typing) rather than a workaround. -- {name Strata.Laurel.HighType.UserDefined}`UserDefined` becomes a regular type. Once - `isSubtype` is implemented properly, `Cat ≤ Animal` will - pass, `Cat ≤ Dog` will fail, and constrained types will be unwrappable to their base. The - current stub is conservative (structural equality only); it can be tightened - incrementally without changing any callers. +## Typing rules -## Notation +Each construct is given as a derivation. Premises sit above the line, conclusion below. +Rules tagged `(impl)` are implemented; rules tagged `(planned)` describe the intended +behavior but aren't yet wired in. `Γ` (the lexical scope) is left implicit; every rule +threads it identically. -Typing rules are written in the standard derivation-tree form: premises above the line, -conclusion below, rule name on the right. +### Sub (subsumption) ``` -premise_1 premise_2 … premise_n -───────────────────────────────────── (Rule-Name) - conclusion +e ⇒ A A <: B +───────────────── (Sub, impl) + e ⇐ B ``` -We use: +The default fallback in `checkStmtExpr`. Used by every construct that doesn't have a +bespoke check rule. -- `e ⇒ T` — _e_ synthesizes _T_ (synth mode, `synthStmtExpr`). -- `e ⇐ T` — _e_ checks against _T_ (check mode, `checkStmtExpr`). -- `T <: U` — gradual consistency-subtyping, i.e. `isConsistentSubtype T U`. -- `Γ` for the lexical scope is left implicit — every rule threads it identically. +### LiteralInt -Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This -includes {name Strata.Laurel.StmtExpr.Return}`Return`, -{name Strata.Laurel.StmtExpr.Exit}`Exit`, {name Strata.Laurel.StmtExpr.While}`While`, -{name Strata.Laurel.StmtExpr.Assert}`Assert`, {name Strata.Laurel.StmtExpr.Assume}`Assume`, -{name Strata.Laurel.Variable.Declare}`Var Declare`, and the opaque/abstract/external bodies -— they're recorded in the rules below. +``` +───────────────────── (Lit-Int, impl) + LiteralInt n ⇒ TInt +``` -## Subsumption (the synth↔check boundary) +### LiteralBool ``` -e ⇒ A A <: B -───────────────── (Sub) - e ⇐ B +────────────────────── (Lit-Bool, impl) + LiteralBool b ⇒ TBool ``` -Subsumption is the *only* place check switches to synth. It fires as the default fallback -in `checkStmtExpr` for every construct without a bespoke check rule. Bespoke check rules -push the expected type *into* subexpressions, which keeps errors localized. +### LiteralString -## Typing rules +``` +──────────────────────────── (Lit-String, impl) + LiteralString s ⇒ TString +``` -Below, each construct is given as a derivation. Rules marked with ✓ in the implementation -column are implemented today; rules marked ✗ are planned. The current implementation has -bespoke check rules for {name Strata.Laurel.StmtExpr.Block}`Block` only; everything else -reaches check mode through Sub. Where a synth rule pushes an expected type into a -subexpression (e.g. `cond ⇐ TBool` in {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), -that's listed as a premise. +### LiteralDecimal -### Literals and references +``` +───────────────────────────── (Lit-Decimal, impl) + LiteralDecimal d ⇒ TReal +``` +### Var (.Local) + +``` + Γ(x) = T +────────────────────── (Var-Local, impl) + Var (.Local x) ⇒ T ``` - (Lit-Int) ✓ -───────────── ──────────────── ───────────────── - LiteralInt n ⇒ TInt LiteralBool b ⇒ TBool LiteralString s ⇒ TString -──────────────────────── Γ(x) = T - LiteralDecimal d ⇒ TReal ───────────────── (Var-Local) ✓ - Var (.Local x) ⇒ T +### Var (.Field) - e ⇒ _ Γ(f) = T_f Γ(x) ↦ T fresh -───────────────────────── (Var-Field) ✓ ───────────────────────── (Var-Declare) ✓ - Var (.Field e f) ⇒ T_f Var (.Declare ⟨x, T⟩) ⇒ TVoid ``` + e ⇒ _ Γ(f) = T_f +───────────────────────── (Var-Field, impl) + Var (.Field e f) ⇒ T_f +``` + +`f` is resolved against the type of `e` (or the enclosing instance type for `self.f`); the +typing rule is independent of which path resolution took. -`Var (.Field e f)` resolves `f` against the type of `e` (or the enclosing instance type for -`self.f`); the typing rule is independent of which path resolution took. +### Var (.Declare) + +``` + Γ(x) ↦ T fresh +────────────────────────────────── (Var-Declare, impl) + Var (.Declare ⟨x, T⟩) ⇒ TVoid +``` ### IfThenElse ``` cond ⇐ TBool thenBr ⇒ T -───────────────────────────── (If-NoElse) ✓ - IfThenElse cond thenBr none ⇒ TVoid +───────────────────────────────────────── (If-NoElse, impl) + IfThenElse cond thenBr none ⇒ TVoid + cond ⇐ TBool thenBr ⇒ T_t elseBr ⇒ T_e -───────────────────────────────────────────────── (If-Synth) ✓ - IfThenElse cond thenBr (some elseBr) ⇒ T_t +───────────────────────────────────────────────── (If-Synth, impl) + IfThenElse cond thenBr (some elseBr) ⇒ T_t + cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T -───────────────────────────────────────────── (If-Check) ✗ (planned) - IfThenElse cond thenBr (some elseBr) ⇐ T +───────────────────────────────────────────── (If-Check, planned) + IfThenElse cond thenBr (some elseBr) ⇐ T ``` -If-Synth picks the then-branch type by convention; the result is always consumed by an -enclosing `checkAssignable` or by Sub, which provides a one-sided check against the -surrounding context. The two branches are deliberately not compared against each other: -statement-position `if`s commonly mix a value branch with a -{name Strata.Laurel.HighType.TVoid}`TVoid` branch (early `return`, `exit`, `assert`, …), -which a strict equality check would reject incorrectly. - If-NoElse synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value -to give back when `cond` is false. This rejects `x : int := if c then 5` at the assignment. +to give back when `cond` is false. Without this rule, `x : int := if c then 5` would +type-check spuriously. + +If-Synth picks the then-branch type; the result is always consumed by an enclosing +`checkAssignable` or by Sub, which provides a one-sided check against the surrounding +context. The two branches are deliberately not compared against each other: statement-position +`if`s commonly mix a value branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch +(early `return`, `exit`, `assert`, …), which a strict equality check would reject incorrectly. ### Block ``` - none of these statements has a typing premise - (their synthesized types are discarded — lax) - ─────────────────────────────────────────── - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T - ──────────────────────────────────────────────────────── (Block-Synth) ✓ - Block [s_1; …; s_n] label ⇒ T + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T +─────────────────────────────────────────────────── (Block-Synth, impl) + Block [s_1; …; s_n] label ⇒ T + -──────────────────── (Block-Synth-Empty) ✓ +──────────────────────── (Block-Synth-Empty, impl) Block [] label ⇒ TVoid - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T -───────────────────────────────────────────── (Block-Check) ✓ - Block [s_1; …; s_n] label ⇐ T + + s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T +─────────────────────────────────────────────────── (Block-Check, impl) + Block [s_1; …; s_n] label ⇐ T + TVoid <: T -───────────────────── (Block-Check-Empty) ✓ +────────────────────── (Block-Check-Empty, impl) Block [] label ⇐ T ``` -Block-Synth is lax: non-last statements are synthesized but their types are discarded. -This matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` +The non-last statements are synthesized but their types are discarded — this is the lax +rule. It matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` returns a value is normal idiomatic code. The cost is that `5;` (a literal in statement position) is silently accepted; flagging it would belong to a lint, not the type checker. -Block-Check pushes the expected type into the *last* statement rather than checking the -block's synthesized type at the boundary. Errors then fire at the offending subexpression -inside `s_n` rather than at the surrounding {name Strata.Laurel.StmtExpr.Block}`Block`, and -the expected type keeps propagating through nested +In check mode, the expected type is pushed into the *last* statement rather than checked at +the boundary. Errors then fire at the offending subexpression inside `s_n`, and the +expected type keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. Empty blocks reduce to a subsumption -check of {name Strata.Laurel.HighType.TVoid}`TVoid` against the expected type. +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. -### Statements that synthesize TVoid +### Exit ``` -───────────────── (Exit) ✓ cond ⇐ TBool invs ⇐ TBool dec ⇐ ? body ⇒ _ - Exit target ⇒ TVoid ──────────────────────────────────────────────────────────────── (While) ✓-ish - While cond invs dec body ⇒ TVoid +───────────────────── (Exit, impl) + Exit target ⇒ TVoid +``` +### Return -───────────────────────── (Return-None) ✓ e ⇒ _ - Return none ⇒ TVoid ───────────────────── (Return-Some) ✓ - Return (some e) ⇒ TVoid +``` +───────────────────────── (Return-None, impl) + Return none ⇒ TVoid -cond ⇐ TBool cond ⇐ TBool -────────────────── (Assert) ✓-ish ────────────── (Assume) ✓-ish - Assert cond ⇒ TVoid Assume cond ⇒ TVoid + e ⇒ _ +────────────────────────── (Return-Some, impl) + Return (some e) ⇒ TVoid - Γ(x) = T_x e ⇒ T_e T_e <: T_x targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────── (Assign-Single) ✓-ish ─────────────────────────────────────────────────────────────────── (Assign-Multi) ✓-ish - Assign [x] e ⇒ TVoid Assign targets e ⇒ TVoid + Γ_proc.outputs = [T] e ⇐ T +───────────────────────────────── (Return-Some-Checked, planned) + Return (some e) ⇒ TVoid ``` -✓-ish marks rules that are implemented but still call the legacy `checkBool` / -`checkAssignable` helpers rather than `checkStmtExpr cond TBool`. Functionally equivalent -under the gradual relation `<:` (since `checkBool` accepts the same types as -`isConsistentSubtype _ TBool` modulo the temporary {name Strata.Laurel.HighType.TCore}`TCore` -carve-out); slated to be migrated to `checkStmtExpr`. +The current `Return-Some` rule discards the value's synthesized type. The planned rule +threads the expected return type through {name Strata.Laurel.ResolveState}`ResolveState` +(set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`), so `return 0` in +a `bool`-returning procedure can be caught at the `Return` site. -The {name Strata.Laurel.StmtExpr.Return}`Return`-with-value rule today only resolves `e` -without checking it against the enclosing procedure's declared output type. The intended -rule is: +### While ``` - Γ_proc.outputs = [T] e ⇐ T -───────────────────────────────── (Return-Some-Checked) ✗ (planned) - Return (some e) ⇒ TVoid + cond ⇐ TBool invs_i ⇐ TBool dec ⇐ ? body ⇒ _ +───────────────────────────────────────────────────────────── (While, impl-ish) + While cond invs dec body ⇒ TVoid ``` -This requires threading the expected return type through `ResolveState`. Without it, -`return 0` in a `bool`-returning procedure goes uncaught. +`impl-ish` here means the rule is implemented but `cond` and `invs_i` go through the legacy +`checkBool` helper rather than `checkStmtExpr cond TBool`. Functionally equivalent under +`<:`; slated for migration. -### Calls and primitive operations +### Assert ``` - callee resolves to procedure with inputs Ts and outputs [T] + cond ⇐ TBool +────────────────────────── (Assert, impl-ish) + Assert cond ⇒ TVoid +``` + +### Assume + +``` + cond ⇐ TBool +───────────────────── (Assume, impl-ish) + Assume cond ⇒ TVoid +``` + +### Assign + +``` + Γ(x) = T_x e ⇒ T_e T_e <: T_x +───────────────────────────────────────── (Assign-Single, impl-ish) + Assign [x] e ⇒ TVoid + + + targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i +───────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) + Assign targets e ⇒ TVoid +``` + +### StaticCall + +``` + callee = static-procedure with inputs Ts and outputs [T] args ⇒ Us U_i <: T_i (pairwise) -────────────────────────────────────────────────────────────── (Static-Call) ✓-ish - StaticCall callee args ⇒ T +──────────────────────────────────────────────────────────── (Static-Call, impl-ish) + StaticCall callee args ⇒ T + - callee resolves to procedure with inputs Ts and outputs [T_1; …; T_n] (n ≠ 1) + callee = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi) ✓-ish - StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] +───────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) + StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] +``` + +### InstanceCall - target ⇒ _ callee resolves with inputs [self; Ts] and outputs [T] +``` + target ⇒ _ callee = instance-procedure with inputs [self; Ts] and outputs [T] args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────── (Instance-Call) ✓-ish - InstanceCall target callee args ⇒ T +───────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) + InstanceCall target callee args ⇒ T +``` +### PrimitiveOp (logical) - args ⇐ TBool (each) -────────────────────────────── (Op-Bool) ✓-ish op ∈ {And, Or, AndThen, OrElse, Not, Implies} +``` + args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} +───────────────────────────── (Op-Bool, impl-ish) PrimitiveOp op args ⇒ TBool +``` +### PrimitiveOp (comparison) - args ⇐ Numeric (each) -───────────────────────────── (Op-Cmp) ✓-ish op ∈ {Lt, Leq, Gt, Geq} +``` + args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} +───────────────────────────── (Op-Cmp, impl-ish) PrimitiveOp op args ⇒ TBool +``` +`Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` +rather than a `checkStmtExpr` chain; equivalent under `<:`. + +### PrimitiveOp (equality) - lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l -────────────────────────────────────────────────────────── (Op-Eq) ✓-ish op ∈ {Eq, Neq} - PrimitiveOp op [lhs; rhs] ⇒ TBool +``` + lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} +────────────────────────────────────────────────────── (Op-Eq, impl-ish) + PrimitiveOp op [lhs; rhs] ⇒ TBool +``` +### PrimitiveOp (arithmetic) - args ⇐ Numeric (each) args.head ⇒ T -────────────────────────────────────────── (Op-Arith) ✓-ish op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} +``` + args_i ⇐ Numeric args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} +────────────────────────────────────────── (Op-Arith, impl-ish) PrimitiveOp op args ⇒ T +``` + +The "result is the type of the first argument" rule handles `int + int → int`, +`real + real → real` etc. without unification. A consequence: `int + real` is *not* +flagged today — each operand individually passes `Numeric`. A real fix would be a +numeric-promotion or unification rule; for now this is a known relaxation. +### PrimitiveOp (string concatenation) - args ⇐ TString (each) — current implementation: no operand check -───────────────────────────── (Op-Concat) ✓-ish +``` + args_i ⇐ TString op = StrConcat +───────────────────────────── (Op-Concat, planned) PrimitiveOp op args ⇒ TString ``` -`Numeric` abbreviates "consistent with one of -{name Strata.Laurel.HighType.TInt}`TInt`, {name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` -rather than a `checkStmtExpr` chain; equivalent under the gradual relation. - -Op-Arith's "result is the type of the first argument" rule handles `int + int → int`, -`real + real → real`, etc. without a unification step. A consequence: `int + real` is *not* -flagged because each operand individually passes the numeric check. A real fix would be a -numeric-promotion or unification rule; for now this is a known relaxation. - -Op-Concat currently performs no operand check; the rule above describes the intended -behavior. +The current implementation performs no operand check on `StrConcat`; the planned rule +above describes the intended behavior. -### Object-related and verification forms +### New ``` ref resolves to a composite or datatype T -───────────────────────────────────────────── (New-Ok) ✓ otherwise New ref ⇒ Unknown +───────────────────────────────────────────── (New-Ok, impl) New ref ⇒ UserDefined T -───────────────── (This) ✓ ──────────────────────────── (Abstract / All / ContractOf) ✓ - This ⇒ Unknown Abstract / All / ContractOf … ⇒ Unknown + ref does not resolve to a composite or datatype +───────────────────────────────────────────────── (New-Fallback, impl) + New ref ⇒ Unknown +``` + +### AsType +``` + target ⇒ _ +───────────────────── (AsType, impl) + AsType target T ⇒ T +``` - lhs ⇒ _ rhs ⇒ _ -───────────────────────── (RefEq) ✓ target ⇒ _ - ReferenceEquals lhs rhs ⇒ TBool ────────────────── (AsType) ✓ - AsType target T ⇒ T +`AsType` does not check `target` against `T` — the cast is the user's claim. +### IsType - target ⇒ _ body ⇒ _ -───────────────── (IsType) ✓ ────────────────────────── (Quantifier) ✓ - IsType target T ⇒ TBool Quantifier mode ⟨x, T⟩ trig body ⇒ TBool +``` + target ⇒ _ +────────────────────────── (IsType, impl) + IsType target T ⇒ TBool +``` +### ReferenceEquals - name ⇒ _ v ⇒ T v ⇒ _ -───────────────── (Assigned) ✓ ──────────── (Old) ✓ ────────────── (Fresh) ✓ - Assigned name ⇒ TBool Old v ⇒ T Fresh v ⇒ TBool +``` + lhs ⇒ _ rhs ⇒ _ +─────────────────────────────── (RefEq, impl) + ReferenceEquals lhs rhs ⇒ TBool +``` +### Quantifier - v ⇒ T proof ⇒ _ target ⇒ T_t newVal ⇒ _ -────────────────────── (ProveBy) ✓ ───────────────────────────────── (PureFieldUpdate) ✓ - ProveBy v proof ⇒ T PureFieldUpdate target f newVal ⇒ T_t +``` + body ⇒ _ +───────────────────────────────────────────── (Quantifier, impl) + Quantifier mode ⟨x, T⟩ trig body ⇒ TBool ``` -### Holes +### Assigned ``` - Unknown <: T -───────────────────── (Hole-Some) ✓ ───────────────────── (Hole-None-Synth) ✓ ───────────────────── (Hole-None-Check) ✗ (planned) - Hole d (some T) ⇒ T Hole d none ⇒ Unknown Hole d none ⇐ T + name ⇒ _ +───────────────────────── (Assigned, impl) + Assigned name ⇒ TBool ``` -In check mode, `Hole d none ⇐ T` reduces to subsumption today (`Unknown <: T`, which always +### Old + +``` + v ⇒ T +───────────── (Old, impl) + Old v ⇒ T +``` + +### Fresh + +``` + v ⇒ _ +────────────────── (Fresh, impl) + Fresh v ⇒ TBool +``` + +### ProveBy + +``` + v ⇒ T proof ⇒ _ +────────────────────────── (ProveBy, impl) + ProveBy v proof ⇒ T +``` + +### PureFieldUpdate + +``` + target ⇒ T_t newVal ⇒ _ +───────────────────────────────────── (PureFieldUpdate, impl) + PureFieldUpdate target f newVal ⇒ T_t +``` + +### This + +``` +───────────────────── (This, impl) + This ⇒ Unknown +``` + +### Abstract / All / ContractOf + +``` +──────────────────────────────────────── (Abstract / All / ContractOf, impl) + Abstract / All / ContractOf … ⇒ Unknown +``` + +### Hole + +``` +─────────────────────── (Hole-Some, impl) + Hole d (some T) ⇒ T + + +───────────────────────── (Hole-None-Synth, impl) + Hole d none ⇒ Unknown + + + Unknown <: T +────────────────────── (Hole-None-Check, planned) + Hole d none ⇐ T +``` + +In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always holds). The planned bespoke rule would record the inferred `T` on the hole node so downstream passes can see it, instead of leaving `none` until the hole-inference pass. -## Mutual recursion and termination - -`synthStmtExpr` and `checkStmtExpr` are mutually recursive: the synth rule for -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` invokes check-mode resolution for the -condition, and the check function falls back to synth via Sub. - -Termination uses a lexicographic measure `(exprMd, tag)` where the tag is `0` for -`synthStmtExpr` and `1` for `checkStmtExpr`. Any descent into a strict subterm decreases -via `Prod.Lex.left` (first component shrinks); Sub calls synth on the *same* expression, -which decreases via `Prod.Lex.right` (second component goes from 1 to 0). This is the -standard well-founded encoding for bidirectional systems where one direction calls the -other on the same input. - -## Two helpers for resolution sites - -Some positions (procedure preconditions, decreases, invariants, postconditions, modifies -clauses, constrained-type witness, etc.) need resolution to run but the type of the -expression is either uninteresting or already known by another path. They use: - -- {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` — the full synth API, returning - `(StmtExprMd × HighTypeMd)`. -- {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` — the check API, returning the resolved - expression and verifying its type is a consistent subtype of the expected type. -- `resolveStmtExpr` — a thin wrapper that calls - `synthStmtExpr` and discards the synthesized type. Used at sites where typing is not - enforced (verification annotations, modifies/reads clauses). - -The right principle is: when the position has a known expected type -({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for `decreases`, the -declared output for a constant initializer or a functional body), use -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. When it doesn't, use -`resolveStmtExpr`. {name Strata.Laurel.synthStmtExpr}`synthStmtExpr` -itself is mostly an internal interface used by other rules. - -## Returns and the expected return type - -`Return e` synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` (the construct itself -produces no value), but the *value being returned* should be checked against the enclosing -procedure's declared output type. The intended design: thread the expected return type -through {name Strata.Laurel.ResolveState}`ResolveState`, set it from `proc.outputs` in -{name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` before resolving the -body, and have the `Return` rule push the expected type into its value via -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`. This closes a soundness gap in the -synth-only design where `return 0` in a `bool`-returning procedure was not caught (because -the body's overall synthesized type was {name Strata.Laurel.HighType.TVoid}`TVoid` and the -body-vs-output check was skipped on `TVoid`). - -## What this is, in type-system terms - -The checker is: - -- *bidirectional*, with a single subsumption rule at the synth↔check boundary, -- with a *gradual* relation (`isConsistentSubtype`) - rather than a strict one — {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic - type, justified by Laurel's targeting of dynamic source languages, -- over a *nominal-with-stubs* subtype relation - (`isSubtype`) — currently structural equality, intended to - walk inheritance chains and unwrap aliases / constrained types, -- with *arity tracking via tuple types* - ({name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`) for multi-output - procedures, -- and *side-effecting expressions modeled as* - {name Strata.Laurel.HighType.TVoid}`TVoid` so blocks, returns, and loops compose cleanly. - -The wildcard carve-out for {name Strata.Laurel.HighType.UserDefined}`UserDefined` from the -previous design is gone — user-defined types are no longer a backdoor through the checker. -The {name Strata.Laurel.HighType.TCore}`TCore` carve-out is preserved for now as a -migration aid and is expected to be removed. - # Translation Pipeline Laurel programs are verified by translating them to Strata Core and then invoking the Core From 084fa2d21be3b02dbe49754e8eb0ccd8029276fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:48:06 -0400 Subject: [PATCH 070/128] concise explanations --- docs/verso/LaurelDoc.lean | 72 +++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 1577232261..d79bf17900 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -243,10 +243,10 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, ## Typing rules -Each construct is given as a derivation. Premises sit above the line, conclusion below. -Rules tagged `(impl)` are implemented; rules tagged `(planned)` describe the intended -behavior but aren't yet wired in. `Γ` (the lexical scope) is left implicit; every rule -threads it identically. +Each construct is given as a derivation. `(impl)` = implemented; `(planned)` = intended, +not yet wired in. `(impl-ish)` = implemented but still calls a legacy helper (`checkBool` / +`checkNumeric`/`checkAssignable`) instead of going through `checkStmtExpr`; functionally +equivalent under `<:`. ### Sub (subsumption) @@ -256,8 +256,7 @@ e ⇒ A A <: B e ⇐ B ``` -The default fallback in `checkStmtExpr`. Used by every construct that doesn't have a -bespoke check rule. +Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### LiteralInt @@ -303,8 +302,8 @@ bespoke check rule. Var (.Field e f) ⇒ T_f ``` -`f` is resolved against the type of `e` (or the enclosing instance type for `self.f`); the -typing rule is independent of which path resolution took. +Resolution looks `f` up against the type of `e` (or the enclosing instance type for +`self.f`); the typing rule itself is path-agnostic. ### Var (.Declare) @@ -332,15 +331,12 @@ cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T IfThenElse cond thenBr (some elseBr) ⇐ T ``` -If-NoElse synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value -to give back when `cond` is false. Without this rule, `x : int := if c then 5` would -type-check spuriously. +If-NoElse uses {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when +`cond` is false; without it, `x : int := if c then 5` would type-check spuriously. -If-Synth picks the then-branch type; the result is always consumed by an enclosing -`checkAssignable` or by Sub, which provides a one-sided check against the surrounding -context. The two branches are deliberately not compared against each other: statement-position -`if`s commonly mix a value branch with a {name Strata.Laurel.HighType.TVoid}`TVoid` branch -(early `return`, `exit`, `assert`, …), which a strict equality check would reject incorrectly. +If-Synth picks the then-branch arbitrarily and does *not* compare branches: a statement- +position `if` often pairs a value branch with a `return`/`exit`/`assert`. The surrounding +context's `checkAssignable` or Sub provides the actual check downstream. ### Block @@ -364,15 +360,13 @@ context. The two branches are deliberately not compared against each other: stat Block [] label ⇐ T ``` -The non-last statements are synthesized but their types are discarded — this is the lax -rule. It matches Java/Python/JavaScript expression-statement semantics: `f(x);` where `f` -returns a value is normal idiomatic code. The cost is that `5;` (a literal in statement -position) is silently accepted; flagging it would belong to a lint, not the type checker. +Non-last statements are synthesized but their types discarded (the lax rule). This matches +Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` +is silently accepted; flagging it belongs to a lint. -In check mode, the expected type is pushed into the *last* statement rather than checked at -the boundary. Errors then fire at the offending subexpression inside `s_n`, and the -expected type keeps propagating through nested -{name Strata.Laurel.StmtExpr.Block}`Block` / +Check mode pushes `T` into the *last* statement instead of comparing the block's +synthesized type at the boundary. Errors then fire at the offending subexpression, and `T` +keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. @@ -401,11 +395,11 @@ expected type keeps propagating through nested Return (some e) ⇒ TVoid ``` -The current `Return-Some` rule discards the value's synthesized type. The planned rule -threads the expected return type through {name Strata.Laurel.ResolveState}`ResolveState` -(set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`), so `return 0` in -a `bool`-returning procedure can be caught at the `Return` site. +`Return-Some` currently throws away the value's type, so `return 0` in a `bool`-returning +procedure isn't caught. The planned rule threads the expected return type through +{name Strata.Laurel.ResolveState}`ResolveState` (set from `proc.outputs` in +{name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`). ### While @@ -415,9 +409,8 @@ a `bool`-returning procedure can be caught at the `Return` site. While cond invs dec body ⇒ TVoid ``` -`impl-ish` here means the rule is implemented but `cond` and `invs_i` go through the legacy -`checkBool` helper rather than `checkStmtExpr cond TBool`. Functionally equivalent under -`<:`; slated for migration. +`dec` (the optional decreases clause) is currently resolved without a type check; the +intended target is a numeric type, not yet enforced. ### Assert @@ -490,8 +483,7 @@ a `bool`-returning procedure can be caught at the `Return` site. `Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, {name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`". Today this is enforced by `checkNumeric` -rather than a `checkStmtExpr` chain; equivalent under `<:`. +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". ### PrimitiveOp (equality) @@ -509,10 +501,9 @@ rather than a `checkStmtExpr` chain; equivalent under `<:`. PrimitiveOp op args ⇒ T ``` -The "result is the type of the first argument" rule handles `int + int → int`, -`real + real → real` etc. without unification. A consequence: `int + real` is *not* -flagged today — each operand individually passes `Numeric`. A real fix would be a -numeric-promotion or unification rule; for now this is a known relaxation. +"Result is the type of the first argument" handles `int + int → int`, `real + real → real`, +etc. without unification. Known relaxation: `int + real` passes (each operand individually +passes `Numeric`); a proper fix needs numeric promotion or unification. ### PrimitiveOp (string concatenation) @@ -522,8 +513,7 @@ numeric-promotion or unification rule; for now this is a known relaxation. PrimitiveOp op args ⇒ TString ``` -The current implementation performs no operand check on `StrConcat`; the planned rule -above describes the intended behavior. +Operand check not yet implemented — `StrConcat` accepts any operands today. ### New @@ -546,7 +536,7 @@ above describes the intended behavior. AsType target T ⇒ T ``` -`AsType` does not check `target` against `T` — the cast is the user's claim. +`target` is resolved but not checked against `T` — the cast is the user's claim. ### IsType From 294be7bdc6d3655b51355756a5f0dc38229cfe46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:49:38 -0400 Subject: [PATCH 071/128] restore contexts in rules --- docs/verso/LaurelDoc.lean | 290 ++++++++++++++++++++------------------ 1 file changed, 151 insertions(+), 139 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index d79bf17900..7eb90d4f87 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -243,17 +243,19 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, ## Typing rules -Each construct is given as a derivation. `(impl)` = implemented; `(planned)` = intended, -not yet wired in. `(impl-ish)` = implemented but still calls a legacy helper (`checkBool` / -`checkNumeric`/`checkAssignable`) instead of going through `checkStmtExpr`; functionally -equivalent under `<:`. +Each construct is given as a derivation. `Γ` is the current lexical scope (see +{name Strata.Laurel.ResolveState}`ResolveState`'s `scope`); it threads identically through +every premise and conclusion unless a rule explicitly extends it (written `Γ, x : T`). +`(impl)` = implemented; `(planned)` = intended, not yet wired in. `(impl-ish)` = implemented +but still calls a legacy helper (`checkBool` / `checkNumeric` / `checkAssignable`) instead of +going through `checkStmtExpr`; functionally equivalent under `<:`. ### Sub (subsumption) ``` -e ⇒ A A <: B -───────────────── (Sub, impl) - e ⇐ B +Γ ⊢ e ⇒ A A <: B +───────────────────── (Sub, impl) + Γ ⊢ e ⇐ B ``` Fallback in `checkStmtExpr` whenever no bespoke check rule applies. @@ -261,45 +263,45 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### LiteralInt ``` -───────────────────── (Lit-Int, impl) - LiteralInt n ⇒ TInt +────────────────────────── (Lit-Int, impl) + Γ ⊢ LiteralInt n ⇒ TInt ``` ### LiteralBool ``` -────────────────────── (Lit-Bool, impl) - LiteralBool b ⇒ TBool +─────────────────────────── (Lit-Bool, impl) + Γ ⊢ LiteralBool b ⇒ TBool ``` ### LiteralString ``` -──────────────────────────── (Lit-String, impl) - LiteralString s ⇒ TString +───────────────────────────────── (Lit-String, impl) + Γ ⊢ LiteralString s ⇒ TString ``` ### LiteralDecimal ``` -───────────────────────────── (Lit-Decimal, impl) - LiteralDecimal d ⇒ TReal +────────────────────────────────── (Lit-Decimal, impl) + Γ ⊢ LiteralDecimal d ⇒ TReal ``` ### Var (.Local) ``` - Γ(x) = T -────────────────────── (Var-Local, impl) - Var (.Local x) ⇒ T + Γ(x) = T +─────────────────────────── (Var-Local, impl) + Γ ⊢ Var (.Local x) ⇒ T ``` ### Var (.Field) ``` - e ⇒ _ Γ(f) = T_f -───────────────────────── (Var-Field, impl) - Var (.Field e f) ⇒ T_f + Γ ⊢ e ⇒ _ Γ(f) = T_f +────────────────────────────── (Var-Field, impl) + Γ ⊢ Var (.Field e f) ⇒ T_f ``` Resolution looks `f` up against the type of `e` (or the enclosing instance type for @@ -308,27 +310,30 @@ Resolution looks `f` up against the type of `e` (or the enclosing instance type ### Var (.Declare) ``` - Γ(x) ↦ T fresh -────────────────────────────────── (Var-Declare, impl) - Var (.Declare ⟨x, T⟩) ⇒ TVoid + x ∉ dom(Γ) +───────────────────────────────────────── (Var-Declare, impl) + Γ ⊢ Var (.Declare ⟨x, T⟩) ⇒ TVoid ⊣ Γ, x : T ``` +`⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the +remainder of the enclosing scope. + ### IfThenElse ``` -cond ⇐ TBool thenBr ⇒ T -───────────────────────────────────────── (If-NoElse, impl) - IfThenElse cond thenBr none ⇒ TVoid +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T +───────────────────────────────────────────── (If-NoElse, impl) + Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid -cond ⇐ TBool thenBr ⇒ T_t elseBr ⇒ T_e -───────────────────────────────────────────────── (If-Synth, impl) - IfThenElse cond thenBr (some elseBr) ⇒ T_t +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e +────────────────────────────────────────────────────────────── (If-Synth, impl) + Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t -cond ⇐ TBool thenBr ⇐ T elseBr ⇐ T -───────────────────────────────────────────── (If-Check, planned) - IfThenElse cond thenBr (some elseBr) ⇐ T +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T +────────────────────────────────────────────────────────── (If-Check, planned) + Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T ``` If-NoElse uses {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when @@ -341,25 +346,30 @@ context's `checkAssignable` or Sub provides the actual check downstream. ### Block ``` - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇒ T -─────────────────────────────────────────────────── (Block-Synth, impl) - Block [s_1; …; s_n] label ⇒ T +Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T +─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) + Γ ⊢ Block [s_1; …; s_n] label ⇒ T -──────────────────────── (Block-Synth-Empty, impl) - Block [] label ⇒ TVoid +───────────────────────────── (Block-Synth-Empty, impl) + Γ ⊢ Block [] label ⇒ TVoid - s_1 ⇒ _ … s_{n-1} ⇒ _ s_n ⇐ T -─────────────────────────────────────────────────── (Block-Check, impl) - Block [s_1; …; s_n] label ⇐ T +Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T +─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) + Γ ⊢ Block [s_1; …; s_n] label ⇐ T TVoid <: T -────────────────────── (Block-Check-Empty, impl) - Block [] label ⇐ T +───────────────────────── (Block-Check-Empty, impl) + Γ ⊢ Block [] label ⇐ T ``` +The notation `Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced +by its predecessor and may itself extend the scope (`Var (.Declare …)` does); the +`Γ_{n-1}` that types `s_n` is the scope after all earlier declarations. Bindings introduced +inside the block don't escape — `Γ` is what surrounds the block. + Non-last statements are synthesized but their types discarded (the lax rule). This matches Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` is silently accepted; flagging it belongs to a lint. @@ -374,25 +384,25 @@ keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / ### Exit ``` -───────────────────── (Exit, impl) - Exit target ⇒ TVoid +──────────────────────── (Exit, impl) + Γ ⊢ Exit target ⇒ TVoid ``` ### Return ``` -───────────────────────── (Return-None, impl) - Return none ⇒ TVoid +───────────────────────────── (Return-None, impl) + Γ ⊢ Return none ⇒ TVoid - e ⇒ _ -────────────────────────── (Return-Some, impl) - Return (some e) ⇒ TVoid + Γ ⊢ e ⇒ _ +────────────────────────────── (Return-Some, impl) + Γ ⊢ Return (some e) ⇒ TVoid - Γ_proc.outputs = [T] e ⇐ T -───────────────────────────────── (Return-Some-Checked, planned) - Return (some e) ⇒ TVoid + Γ_proc.outputs = [T] Γ ⊢ e ⇐ T +────────────────────────────────────── (Return-Some-Checked, planned) + Γ ⊢ Return (some e) ⇒ TVoid ``` `Return-Some` currently throws away the value's type, so `return 0` in a `bool`-returning @@ -404,9 +414,9 @@ procedure isn't caught. The planned rule threads the expected return type throug ### While ``` - cond ⇐ TBool invs_i ⇐ TBool dec ⇐ ? body ⇒ _ -───────────────────────────────────────────────────────────── (While, impl-ish) - While cond invs dec body ⇒ TVoid + Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ +─────────────────────────────────────────────────────────────────────────────── (While, impl-ish) + Γ ⊢ While cond invs dec body ⇒ TVoid ``` `dec` (the optional decreases clause) is currently resolved without a type check; the @@ -415,70 +425,70 @@ intended target is a numeric type, not yet enforced. ### Assert ``` - cond ⇐ TBool -────────────────────────── (Assert, impl-ish) - Assert cond ⇒ TVoid + Γ ⊢ cond ⇐ TBool +────────────────────────────── (Assert, impl-ish) + Γ ⊢ Assert cond ⇒ TVoid ``` ### Assume ``` - cond ⇐ TBool -───────────────────── (Assume, impl-ish) - Assume cond ⇒ TVoid + Γ ⊢ cond ⇐ TBool +───────────────────────────── (Assume, impl-ish) + Γ ⊢ Assume cond ⇒ TVoid ``` ### Assign ``` - Γ(x) = T_x e ⇒ T_e T_e <: T_x -───────────────────────────────────────── (Assign-Single, impl-ish) - Assign [x] e ⇒ TVoid + Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x +─────────────────────────────────────────────── (Assign-Single, impl-ish) + Γ ⊢ Assign [x] e ⇒ TVoid - targets ⇒ Ts e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) - Assign targets e ⇒ TVoid + Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i +───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) + Γ ⊢ Assign targets e ⇒ TVoid ``` ### StaticCall ``` - callee = static-procedure with inputs Ts and outputs [T] - args ⇒ Us U_i <: T_i (pairwise) -──────────────────────────────────────────────────────────── (Static-Call, impl-ish) - StaticCall callee args ⇒ T + Γ(callee) = static-procedure with inputs Ts and outputs [T] + Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) +───────────────────────────────────────────────────────────── (Static-Call, impl-ish) + Γ ⊢ StaticCall callee args ⇒ T - callee = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 - args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) - StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] + Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 + Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) +────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) + Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` ### InstanceCall ``` - target ⇒ _ callee = instance-procedure with inputs [self; Ts] and outputs [T] - args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) - InstanceCall target callee args ⇒ T + Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] + Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) +───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) + Γ ⊢ InstanceCall target callee args ⇒ T ``` ### PrimitiveOp (logical) ``` - args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} -───────────────────────────── (Op-Bool, impl-ish) - PrimitiveOp op args ⇒ TBool + Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} +────────────────────────────────── (Op-Bool, impl-ish) + Γ ⊢ PrimitiveOp op args ⇒ TBool ``` ### PrimitiveOp (comparison) ``` - args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} -───────────────────────────── (Op-Cmp, impl-ish) - PrimitiveOp op args ⇒ TBool + Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} +───────────────────────────────── (Op-Cmp, impl-ish) + Γ ⊢ PrimitiveOp op args ⇒ TBool ``` `Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, @@ -488,17 +498,17 @@ intended target is a numeric type, not yet enforced. ### PrimitiveOp (equality) ``` - lhs ⇒ T_l rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} -────────────────────────────────────────────────────── (Op-Eq, impl-ish) - PrimitiveOp op [lhs; rhs] ⇒ TBool + Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} +───────────────────────────────────────────────────────────────── (Op-Eq, impl-ish) + Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` ### PrimitiveOp (arithmetic) ``` - args_i ⇐ Numeric args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} -────────────────────────────────────────── (Op-Arith, impl-ish) - PrimitiveOp op args ⇒ T + Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} +────────────────────────────────────────────────── (Op-Arith, impl-ish) + Γ ⊢ PrimitiveOp op args ⇒ T ``` "Result is the type of the first argument" handles `int + int → int`, `real + real → real`, @@ -508,9 +518,9 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ### PrimitiveOp (string concatenation) ``` - args_i ⇐ TString op = StrConcat -───────────────────────────── (Op-Concat, planned) - PrimitiveOp op args ⇒ TString + Γ ⊢ args_i ⇐ TString op = StrConcat +───────────────────────────────────── (Op-Concat, planned) + Γ ⊢ PrimitiveOp op args ⇒ TString ``` Operand check not yet implemented — `StrConcat` accepts any operands today. @@ -518,22 +528,22 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. ### New ``` - ref resolves to a composite or datatype T -───────────────────────────────────────────── (New-Ok, impl) - New ref ⇒ UserDefined T + Γ(ref) is a composite or datatype T +────────────────────────────────────────── (New-Ok, impl) + Γ ⊢ New ref ⇒ UserDefined T - ref does not resolve to a composite or datatype -───────────────────────────────────────────────── (New-Fallback, impl) - New ref ⇒ Unknown + Γ(ref) is not a composite or datatype +───────────────────────────────────────── (New-Fallback, impl) + Γ ⊢ New ref ⇒ Unknown ``` ### AsType ``` - target ⇒ _ -───────────────────── (AsType, impl) - AsType target T ⇒ T + Γ ⊢ target ⇒ _ +───────────────────────────── (AsType, impl) + Γ ⊢ AsType target T ⇒ T ``` `target` is resolved but not checked against `T` — the cast is the user's claim. @@ -541,95 +551,97 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. ### IsType ``` - target ⇒ _ -────────────────────────── (IsType, impl) - IsType target T ⇒ TBool + Γ ⊢ target ⇒ _ +───────────────────────────────── (IsType, impl) + Γ ⊢ IsType target T ⇒ TBool ``` ### ReferenceEquals ``` - lhs ⇒ _ rhs ⇒ _ -─────────────────────────────── (RefEq, impl) - ReferenceEquals lhs rhs ⇒ TBool + Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ +─────────────────────────────────────── (RefEq, impl) + Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` ### Quantifier ``` - body ⇒ _ -───────────────────────────────────────────── (Quantifier, impl) - Quantifier mode ⟨x, T⟩ trig body ⇒ TBool + Γ, x : T ⊢ body ⇒ _ +───────────────────────────────────────────────── (Quantifier, impl) + Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool ``` +The bound variable `x : T` is introduced in scope only for the body (and trigger). + ### Assigned ``` - name ⇒ _ -───────────────────────── (Assigned, impl) - Assigned name ⇒ TBool + Γ ⊢ name ⇒ _ +───────────────────────────── (Assigned, impl) + Γ ⊢ Assigned name ⇒ TBool ``` ### Old ``` - v ⇒ T -───────────── (Old, impl) - Old v ⇒ T + Γ ⊢ v ⇒ T +───────────────── (Old, impl) + Γ ⊢ Old v ⇒ T ``` ### Fresh ``` - v ⇒ _ -────────────────── (Fresh, impl) - Fresh v ⇒ TBool + Γ ⊢ v ⇒ _ +───────────────────── (Fresh, impl) + Γ ⊢ Fresh v ⇒ TBool ``` ### ProveBy ``` - v ⇒ T proof ⇒ _ -────────────────────────── (ProveBy, impl) - ProveBy v proof ⇒ T + Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ +─────────────────────────────────── (ProveBy, impl) + Γ ⊢ ProveBy v proof ⇒ T ``` ### PureFieldUpdate ``` - target ⇒ T_t newVal ⇒ _ -───────────────────────────────────── (PureFieldUpdate, impl) - PureFieldUpdate target f newVal ⇒ T_t + Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ +─────────────────────────────────────────────── (PureFieldUpdate, impl) + Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t ``` ### This ``` -───────────────────── (This, impl) - This ⇒ Unknown +────────────────────────── (This, impl) + Γ ⊢ This ⇒ Unknown ``` ### Abstract / All / ContractOf ``` -──────────────────────────────────────── (Abstract / All / ContractOf, impl) - Abstract / All / ContractOf … ⇒ Unknown +───────────────────────────────────────────── (Abstract / All / ContractOf, impl) + Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown ``` ### Hole ``` -─────────────────────── (Hole-Some, impl) - Hole d (some T) ⇒ T +──────────────────────────── (Hole-Some, impl) + Γ ⊢ Hole d (some T) ⇒ T -───────────────────────── (Hole-None-Synth, impl) - Hole d none ⇒ Unknown +───────────────────────────────── (Hole-None-Synth, impl) + Γ ⊢ Hole d none ⇒ Unknown - Unknown <: T -────────────────────── (Hole-None-Check, planned) - Hole d none ⇐ T + Unknown <: T +───────────────────────── (Hole-None-Check, planned) + Γ ⊢ Hole d none ⇐ T ``` In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always From 06ce7b98b9a1c424702bec8f666bec78faadf19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 13:53:23 -0400 Subject: [PATCH 072/128] simplify presentation --- docs/verso/LaurelDoc.lean | 235 +++++++++++++++++++++++++------------- 1 file changed, 157 insertions(+), 78 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 7eb90d4f87..ff0d09183d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -250,7 +250,26 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x but still calls a legacy helper (`checkBool` / `checkNumeric` / `checkAssignable`) instead of going through `checkStmtExpr`; functionally equivalent under `<:`. -### Sub (subsumption) +### Index + +- *Subsumption* — Sub +- *Literals* — Lit-Int, Lit-Bool, Lit-String, Lit-Decimal +- *Variables* — Var-Local, Var-Field, Var-Declare +- *Control flow* — If-NoElse, If-Synth, If-Check (planned); Block-Synth, Block-Synth-Empty, + Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, Return-Some-Checked + (planned); While +- *Verification statements* — Assert, Assume +- *Assignment* — Assign-Single, Assign-Multi +- *Calls* — Static-Call, Static-Call-Multi, Instance-Call +- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat (planned) +- *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate +- *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy +- *Untyped forms* — This; Abstract / All / ContractOf +- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) + +### Subsumption + +#### Sub ``` Γ ⊢ e ⇒ A A <: B @@ -260,35 +279,39 @@ going through `checkStmtExpr`; functionally equivalent under `<:`. Fallback in `checkStmtExpr` whenever no bespoke check rule applies. -### LiteralInt +### Literals + +#### Lit-Int ``` ────────────────────────── (Lit-Int, impl) Γ ⊢ LiteralInt n ⇒ TInt ``` -### LiteralBool +#### Lit-Bool ``` ─────────────────────────── (Lit-Bool, impl) Γ ⊢ LiteralBool b ⇒ TBool ``` -### LiteralString +#### Lit-String ``` ───────────────────────────────── (Lit-String, impl) Γ ⊢ LiteralString s ⇒ TString ``` -### LiteralDecimal +#### Lit-Decimal ``` ────────────────────────────────── (Lit-Decimal, impl) Γ ⊢ LiteralDecimal d ⇒ TReal ``` -### Var (.Local) +### Variables + +#### Var-Local ``` Γ(x) = T @@ -296,7 +319,7 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. Γ ⊢ Var (.Local x) ⇒ T ``` -### Var (.Field) +#### Var-Field ``` Γ ⊢ e ⇒ _ Γ(f) = T_f @@ -307,7 +330,7 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. Resolution looks `f` up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -### Var (.Declare) +#### Var-Declare ``` x ∉ dom(Γ) @@ -318,100 +341,125 @@ Resolution looks `f` up against the type of `e` (or the enclosing instance type `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. -### IfThenElse +### Control flow + +#### If-NoElse ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T ───────────────────────────────────────────── (If-NoElse, impl) Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid +``` + +The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no +value when `cond` is false; without this, `x : int := if c then 5` would type-check +spuriously. +#### If-Synth +``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e ────────────────────────────────────────────────────────────── (If-Synth, impl) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t +``` + +Picks the then-branch type arbitrarily; the two branches are *not* compared, since a +statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The +enclosing `checkAssignable` or Sub provides the actual check downstream. +#### If-Check +``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T ────────────────────────────────────────────────────────── (If-Check, planned) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T ``` -If-NoElse uses {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when -`cond` is false; without it, `x : int := if c then 5` would type-check spuriously. - -If-Synth picks the then-branch arbitrarily and does *not* compare branches: a statement- -position `if` often pairs a value branch with a `return`/`exit`/`assert`. The surrounding -context's `checkAssignable` or Sub provides the actual check downstream. - -### Block +#### Block-Synth ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T ─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) Γ ⊢ Block [s_1; …; s_n] label ⇒ T +``` + +`Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced by its +predecessor and may itself extend it (`Var (.Declare …)` does); `s_n` is typed in +`Γ_{n-1}`. Bindings introduced inside the block don't escape — `Γ` is what surrounds the +block. +Non-last statements are synthesized but their types discarded (the lax rule). This matches +Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` +is silently accepted; flagging it belongs to a lint. +#### Block-Synth-Empty + +``` ───────────────────────────── (Block-Synth-Empty, impl) Γ ⊢ Block [] label ⇒ TVoid +``` +#### Block-Check +``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T ─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) Γ ⊢ Block [s_1; …; s_n] label ⇐ T +``` + +Pushes `T` into the *last* statement rather than comparing the block's synthesized type at +the boundary. Errors fire at the offending subexpression, and `T` keeps propagating through +nested {name Strata.Laurel.StmtExpr.Block}`Block` / +{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / +{name Strata.Laurel.StmtExpr.Hole}`Hole` / +{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. +#### Block-Check-Empty +``` TVoid <: T ───────────────────────── (Block-Check-Empty, impl) Γ ⊢ Block [] label ⇐ T ``` -The notation `Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced -by its predecessor and may itself extend the scope (`Var (.Declare …)` does); the -`Γ_{n-1}` that types `s_n` is the scope after all earlier declarations. Bindings introduced -inside the block don't escape — `Γ` is what surrounds the block. - -Non-last statements are synthesized but their types discarded (the lax rule). This matches -Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` -is silently accepted; flagging it belongs to a lint. - -Check mode pushes `T` into the *last* statement instead of comparing the block's -synthesized type at the boundary. Errors then fire at the offending subexpression, and `T` -keeps propagating through nested {name Strata.Laurel.StmtExpr.Block}`Block` / -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / -{name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. - -### Exit +#### Exit ``` ──────────────────────── (Exit, impl) Γ ⊢ Exit target ⇒ TVoid ``` -### Return +#### Return-None ``` ───────────────────────────── (Return-None, impl) Γ ⊢ Return none ⇒ TVoid +``` +#### Return-Some +``` Γ ⊢ e ⇒ _ ────────────────────────────── (Return-Some, impl) Γ ⊢ Return (some e) ⇒ TVoid +``` +The value's synthesized type is currently discarded, so `return 0` in a `bool`-returning +procedure isn't caught. Replaced by Return-Some-Checked once the expected return type is +threaded through {name Strata.Laurel.ResolveState}`ResolveState`. +#### Return-Some-Checked + +``` Γ_proc.outputs = [T] Γ ⊢ e ⇐ T ────────────────────────────────────── (Return-Some-Checked, planned) Γ ⊢ Return (some e) ⇒ TVoid ``` -`Return-Some` currently throws away the value's type, so `return 0` in a `bool`-returning -procedure isn't caught. The planned rule threads the expected return type through -{name Strata.Laurel.ResolveState}`ResolveState` (set from `proc.outputs` in -{name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`). +Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`. -### While +#### While ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ @@ -419,10 +467,12 @@ procedure isn't caught. The planned rule threads the expected return type throug Γ ⊢ While cond invs dec body ⇒ TVoid ``` -`dec` (the optional decreases clause) is currently resolved without a type check; the -intended target is a numeric type, not yet enforced. +`dec` (the optional decreases clause) is resolved without a type check today; the intended +target is a numeric type. + +### Verification statements -### Assert +#### Assert ``` Γ ⊢ cond ⇐ TBool @@ -430,7 +480,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ Assert cond ⇒ TVoid ``` -### Assume +#### Assume ``` Γ ⊢ cond ⇐ TBool @@ -438,35 +488,45 @@ intended target is a numeric type, not yet enforced. Γ ⊢ Assume cond ⇒ TVoid ``` -### Assign +### Assignment + +#### Assign-Single ``` Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x ─────────────────────────────────────────────── (Assign-Single, impl-ish) Γ ⊢ Assign [x] e ⇒ TVoid +``` +#### Assign-Multi +``` Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i ───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) Γ ⊢ Assign targets e ⇒ TVoid ``` -### StaticCall +### Calls + +#### Static-Call ``` Γ(callee) = static-procedure with inputs Ts and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) ───────────────────────────────────────────────────────────── (Static-Call, impl-ish) Γ ⊢ StaticCall callee args ⇒ T +``` +#### Static-Call-Multi +``` Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) ────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` -### InstanceCall +#### Instance-Call ``` Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] @@ -475,7 +535,13 @@ intended target is a numeric type, not yet enforced. Γ ⊢ InstanceCall target callee args ⇒ T ``` -### PrimitiveOp (logical) +### Primitive operations + +`Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, +{name Strata.Laurel.HighType.TReal}`TReal`, +{name Strata.Laurel.HighType.TFloat64}`TFloat64`". + +#### Op-Bool ``` Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} @@ -483,7 +549,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -### PrimitiveOp (comparison) +#### Op-Cmp ``` Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} @@ -491,11 +557,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -`Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, -{name Strata.Laurel.HighType.TReal}`TReal`, -{name Strata.Laurel.HighType.TFloat64}`TFloat64`". - -### PrimitiveOp (equality) +#### Op-Eq ``` Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} @@ -503,7 +565,7 @@ intended target is a numeric type, not yet enforced. Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` -### PrimitiveOp (arithmetic) +#### Op-Arith ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} @@ -515,7 +577,7 @@ intended target is a numeric type, not yet enforced. etc. without unification. Known relaxation: `int + real` passes (each operand individually passes `Numeric`); a proper fix needs numeric promotion or unification. -### PrimitiveOp (string concatenation) +#### Op-Concat ``` Γ ⊢ args_i ⇐ TString op = StrConcat @@ -525,20 +587,25 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. Operand check not yet implemented — `StrConcat` accepts any operands today. -### New +### Object forms + +#### New-Ok ``` Γ(ref) is a composite or datatype T ────────────────────────────────────────── (New-Ok, impl) Γ ⊢ New ref ⇒ UserDefined T +``` +#### New-Fallback +``` Γ(ref) is not a composite or datatype ───────────────────────────────────────── (New-Fallback, impl) Γ ⊢ New ref ⇒ Unknown ``` -### AsType +#### AsType ``` Γ ⊢ target ⇒ _ @@ -548,7 +615,7 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. `target` is resolved but not checked against `T` — the cast is the user's claim. -### IsType +#### IsType ``` Γ ⊢ target ⇒ _ @@ -556,7 +623,7 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. Γ ⊢ IsType target T ⇒ TBool ``` -### ReferenceEquals +#### RefEq ``` Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ @@ -564,7 +631,17 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` -### Quantifier +#### PureFieldUpdate + +``` + Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ +─────────────────────────────────────────────── (PureFieldUpdate, impl) + Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t +``` + +### Verification expressions + +#### Quantifier ``` Γ, x : T ⊢ body ⇒ _ @@ -574,7 +651,7 @@ Operand check not yet implemented — `StrConcat` accepts any operands today. The bound variable `x : T` is introduced in scope only for the body (and trigger). -### Assigned +#### Assigned ``` Γ ⊢ name ⇒ _ @@ -582,7 +659,7 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ Assigned name ⇒ TBool ``` -### Old +#### Old ``` Γ ⊢ v ⇒ T @@ -590,7 +667,7 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ Old v ⇒ T ``` -### Fresh +#### Fresh ``` Γ ⊢ v ⇒ _ @@ -598,7 +675,7 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ Fresh v ⇒ TBool ``` -### ProveBy +#### ProveBy ``` Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ @@ -606,47 +683,49 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger Γ ⊢ ProveBy v proof ⇒ T ``` -### PureFieldUpdate - -``` - Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ -─────────────────────────────────────────────── (PureFieldUpdate, impl) - Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t -``` +### Untyped forms -### This +#### This ``` ────────────────────────── (This, impl) Γ ⊢ This ⇒ Unknown ``` -### Abstract / All / ContractOf +#### Abstract / All / ContractOf ``` ───────────────────────────────────────────── (Abstract / All / ContractOf, impl) Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown ``` -### Hole +### Holes + +#### Hole-Some ``` ──────────────────────────── (Hole-Some, impl) Γ ⊢ Hole d (some T) ⇒ T +``` +#### Hole-None-Synth +``` ───────────────────────────────── (Hole-None-Synth, impl) Γ ⊢ Hole d none ⇒ Unknown +``` +#### Hole-None-Check +``` Unknown <: T ───────────────────────── (Hole-None-Check, planned) Γ ⊢ Hole d none ⇐ T ``` In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always -holds). The planned bespoke rule would record the inferred `T` on the hole node so -downstream passes can see it, instead of leaving `none` until the hole-inference pass. +holds). The planned rule would record the inferred `T` on the hole node so downstream +passes can see it, instead of leaving `none` until the hole-inference pass. # Translation Pipeline From 835d8b5ade3872d856eef4927520c1b5e0ccff84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:14:16 -0400 Subject: [PATCH 073/128] add back in contexts --- docs/verso/LaurelDoc.lean | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index ff0d09183d..4b5f314c2d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -160,8 +160,8 @@ mismatches against the surrounding context become diagnostics. The implementatio There are two operations on expressions, written here in standard bidirectional notation: ``` -e ⇒ T -- "e synthesizes T" (synthStmtExpr) -e ⇐ T -- "e checks against T" (checkStmtExpr) +Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) +Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) ``` Synthesis returns a type inferred from the expression itself; checking verifies that the @@ -170,9 +170,9 @@ is determined locally (synth) or by context (check). The two judgments are conne single change-of-direction rule, *subsumption*: ``` -e ⇒ A A <: B -───────────────── (Sub) - e ⇐ B +Γ ⊢ e ⇒ A A <: B +───────────────────── (Sub) + Γ ⊢ e ⇐ B ``` Subsumption is the *only* place the checker switches from check to synth mode. It fires as From a7ae7bb996eb5f856b8a5cee7db85d8121c5caa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:50:07 -0400 Subject: [PATCH 074/128] =?UTF-8?q?describe=20literals=20and=20easy=20rule?= =?UTF-8?q?s=20(call,=20assert/assume=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/verso/LaurelDoc.lean | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4b5f314c2d..4abf2eff2c 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -246,9 +246,7 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, Each construct is given as a derivation. `Γ` is the current lexical scope (see {name Strata.Laurel.ResolveState}`ResolveState`'s `scope`); it threads identically through every premise and conclusion unless a rule explicitly extends it (written `Γ, x : T`). -`(impl)` = implemented; `(planned)` = intended, not yet wired in. `(impl-ish)` = implemented -but still calls a legacy helper (`checkBool` / `checkNumeric` / `checkAssignable`) instead of -going through `checkStmtExpr`; functionally equivalent under `<:`. +`(impl)` = implemented; `(planned)` = intended, not yet wired in. ### Index @@ -261,7 +259,7 @@ going through `checkStmtExpr`; functionally equivalent under `<:`. - *Verification statements* — Assert, Assume - *Assignment* — Assign-Single, Assign-Multi - *Calls* — Static-Call, Static-Call-Multi, Instance-Call -- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat (planned) +- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate - *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy - *Untyped forms* — This; Abstract / All / ContractOf @@ -365,7 +363,8 @@ spuriously. Picks the then-branch type arbitrarily; the two branches are *not* compared, since a statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The -enclosing `checkAssignable` or Sub provides the actual check downstream. +enclosing context's check (Sub, or a containing `checkSubtype` like an assignment) provides +the actual check downstream. #### If-Check @@ -463,7 +462,7 @@ Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedur ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ -─────────────────────────────────────────────────────────────────────────────── (While, impl-ish) +─────────────────────────────────────────────────────────────────────────────── (While, impl) Γ ⊢ While cond invs dec body ⇒ TVoid ``` @@ -476,7 +475,7 @@ target is a numeric type. ``` Γ ⊢ cond ⇐ TBool -────────────────────────────── (Assert, impl-ish) +────────────────────────────── (Assert, impl) Γ ⊢ Assert cond ⇒ TVoid ``` @@ -484,7 +483,7 @@ target is a numeric type. ``` Γ ⊢ cond ⇐ TBool -───────────────────────────── (Assume, impl-ish) +───────────────────────────── (Assume, impl) Γ ⊢ Assume cond ⇒ TVoid ``` @@ -494,7 +493,7 @@ target is a numeric type. ``` Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x -─────────────────────────────────────────────── (Assign-Single, impl-ish) +─────────────────────────────────────────────── (Assign-Single, impl) Γ ⊢ Assign [x] e ⇒ TVoid ``` @@ -502,7 +501,7 @@ target is a numeric type. ``` Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl-ish) +───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl) Γ ⊢ Assign targets e ⇒ TVoid ``` @@ -513,7 +512,7 @@ target is a numeric type. ``` Γ(callee) = static-procedure with inputs Ts and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────── (Static-Call, impl-ish) +───────────────────────────────────────────────────────────── (Static-Call, impl) Γ ⊢ StaticCall callee args ⇒ T ``` @@ -522,7 +521,7 @@ target is a numeric type. ``` Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl-ish) +────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl) Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` @@ -531,7 +530,7 @@ target is a numeric type. ``` Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl-ish) +───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl) Γ ⊢ InstanceCall target callee args ⇒ T ``` @@ -545,7 +544,7 @@ target is a numeric type. ``` Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} -────────────────────────────────── (Op-Bool, impl-ish) +────────────────────────────────── (Op-Bool, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` @@ -553,7 +552,7 @@ target is a numeric type. ``` Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} -───────────────────────────────── (Op-Cmp, impl-ish) +───────────────────────────────── (Op-Cmp, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` @@ -561,7 +560,7 @@ target is a numeric type. ``` Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} -───────────────────────────────────────────────────────────────── (Op-Eq, impl-ish) +───────────────────────────────────────────────────────────────── (Op-Eq, impl) Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` @@ -569,7 +568,7 @@ target is a numeric type. ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} -────────────────────────────────────────────────── (Op-Arith, impl-ish) +────────────────────────────────────────────────── (Op-Arith, impl) Γ ⊢ PrimitiveOp op args ⇒ T ``` @@ -581,12 +580,10 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ``` Γ ⊢ args_i ⇐ TString op = StrConcat -───────────────────────────────────── (Op-Concat, planned) +───────────────────────────────────── (Op-Concat, impl) Γ ⊢ PrimitiveOp op args ⇒ TString ``` -Operand check not yet implemented — `StrConcat` accepts any operands today. - ### Object forms #### New-Ok From f3d657ee45e8520500f77f9456993c50b3ceeedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:50:34 -0400 Subject: [PATCH 075/128] remove old helpers + mis-subtyping diagnostics --- Strata/Languages/Laurel/Resolution.lean | 112 +++++++++--------------- 1 file changed, 41 insertions(+), 71 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 97f6556331..1259185178 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -37,10 +37,10 @@ Walks the AST under `ResolveM`, a state monad over `ResolveState`. Phase 1: declared type to build a qualified lookup key), - opens fresh nested scopes via `withScope` for blocks, quantifiers, procedure bodies, and constrained-type constraint/witness expressions, -- synthesizes a `HighType` for every `StmtExpr` and runs the type-checking - helpers (`checkBool`, `checkNumeric`, `checkAssignable`, `checkComparable`) - on assignments, call arguments, condition positions, functional bodies, and - constant initializers. +- synthesizes a `HighType` for every `StmtExpr` and checks it (via + `checkStmtExpr` for fresh subexpressions, or `checkSubtype` when a type is + already in hand) on assignments, call arguments, condition positions, + functional bodies, and constant initializers. Before any bodies are walked, `preRegisterTopLevel` registers every top-level name (types and their constructors / testers / destructors / instance @@ -436,54 +436,21 @@ private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := | .TCore _, _ | _, .TCore _ => true | _, _ => isSubtype sub sup -/-- Check that a type is boolean, emitting a diagnostic if not. -/ -private def checkBool (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do +/-- Type-level subtype check: emits the standard "expected/got" diagnostic when + `actual` is not a consistent subtype of `expected`. Used at sites where the + actual type is already in hand (assignment, call args, body vs declared + output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ +private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do + unless isConsistentSubtype actual expected do + typeMismatch source (s!"'{formatType expected}'") actual + +/-- Test whether a type is in the set of numeric primitives, modulo gradual + consistency. Used by Op-Cmp / Op-Arith. -/ +private def isConsistentNumeric (ty : HighTypeMd) : Bool := match ty.val with - | .TBool | .Unknown => pure () - | .UserDefined _ => pure () -- constrained types may wrap bool - | _ => typeMismatch source "bool" ty - -/-- Check that a type is numeric (int, real, or float64), emitting a diagnostic if not. -/ -private def checkNumeric (source : Option FileRange) (ty : HighTypeMd) : ResolveM Unit := do - match ty.val with - | .TInt | .TReal | .TFloat64 | .Unknown => pure () - | .UserDefined _ => pure () -- constrained types may wrap numeric types - | _ => typeMismatch source "a numeric type" ty - -/-- Check that two types are compatible, emitting a diagnostic if not. - UserDefined types are always considered compatible with each other since - subtype relationships (inheritance) are not tracked during resolution. - TCore types are not checked since they are pass-through types from the Core language. -/ -private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do - match expected.val, actual.val with - | .Unknown, _ => pure () - | _, .Unknown => pure () - | .UserDefined _, _ => pure () -- subtype relationships not tracked here - | _, .UserDefined _ => pure () -- subtype relationships not tracked here - | .TCore _, _ => pure () -- pass-through Core types not checked during resolution - | _, .TCore _ => pure () -- pass-through Core types not checked during resolution - | _, _ => - if !highEq expected actual then - let expectedStr := formatType expected - let actualStr := formatType actual - let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" - modify fun s => { s with errors := s.errors.push diag } - -/-- Check that two types are comparable (for == and !=), emitting a symmetric diagnostic if not. -/ -private def checkComparable (source : Option FileRange) (lhsTy : HighTypeMd) (rhsTy : HighTypeMd) : ResolveM Unit := do - match lhsTy.val, rhsTy.val with - | .Unknown, _ => pure () - | _, .Unknown => pure () - | .UserDefined _, _ => pure () - | _, .UserDefined _ => pure () - | .TCore _, _ => pure () - | _, .TCore _ => pure () - | _, _ => - if !highEq lhsTy rhsTy then - let lhsStr := formatType lhsTy - let rhsStr := formatType rhsTy - let diag := diagnosticFromSource source s!"Operands of '==' have incompatible types '{lhsStr}' and '{rhsStr}'" - modify fun s => { s with errors := s.errors.push diag } + | .TInt | .TReal | .TFloat64 | .Unknown => true + | .TCore _ => true + | _ => false /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do @@ -547,10 +514,9 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | none => { val := .TVoid, source := source } pure (.Block stmts' label, lastTy) | .While cond invs dec body => - let (cond', condTy) ← synthStmtExpr cond - checkBool cond'.source condTy + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } let invs' ← invs.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') + checkStmtExpr a.val { val := .TBool, source := a.val.source }) let dec' ← dec.attach.mapM (fun a => have := a.property; do let (e', _) ← synthStmtExpr a.val; pure e') let (body', _) ← synthStmtExpr body @@ -614,7 +580,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | some (_, node) => pure node.getType | none => pure { val := HighType.Unknown, source := fieldName.source : HighTypeMd } let tTy ← targetTy - checkAssignable source tTy valueTy + checkSubtype source tTy valueTy pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => let (target', _) ← synthStmtExpr target @@ -633,9 +599,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee - -- Check argument types match parameter types for (argTy, paramTy) in argTypes.zip paramTypes do - checkAssignable source paramTy argTy + checkSubtype source paramTy argTy pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => let results ← args.mapM synthStmtExpr @@ -649,18 +614,25 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | some headTy => headTy.val | none => HighType.TInt | .StrConcat => HighType.TString - -- Type check operands match op with | .And | .Or | .AndThen | .OrElse | .Not | .Implies => - for aTy in argTypes do checkBool source aTy + for aTy in argTypes do + checkSubtype source { val := .TBool, source := aTy.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => - for aTy in argTypes do checkNumeric source aTy + for aTy in argTypes do + unless isConsistentNumeric aTy do typeMismatch aTy.source "a numeric type" aTy | .Eq | .Neq => - -- Check that operands are compatible with each other (symmetric check) + -- Symmetric: pass if either direction is consistent. match argTypes with - | [lhsTy, rhsTy] => checkComparable source lhsTy rhsTy + | [lhsTy, rhsTy] => + unless isConsistentSubtype lhsTy rhsTy || isConsistentSubtype rhsTy lhsTy do + let diag := diagnosticFromSource source + s!"Operands of '==' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + modify fun s => { s with errors := s.errors.push diag } | _ => pure () - | .StrConcat => pure () + | .StrConcat => + for aTy in argTypes do + checkSubtype source { val := .TString, source := aTy.source } aTy pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source @@ -695,10 +667,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee - -- Check argument types match parameter types (skip first param which is 'self') + -- Skip first param (self) when matching args. let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] for (argTy, paramTy) in argTypes.zip callParamTypes do - checkAssignable source paramTy argTy + checkSubtype source paramTy argTy pure (.InstanceCall target' callee' args', retTy) | .Quantifier mode param trigger body => withScope do @@ -718,12 +690,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (val', _) ← synthStmtExpr val pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => - let (cond', condTy) ← synthStmtExpr condExpr - checkBool cond'.source condTy + let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) | .Assume cond => - let (cond', condTy) ← synthStmtExpr cond - checkBool cond'.source condTy + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } pure (.Assume cond', { val := .TVoid, source := source }) | .ProveBy val proof => let (val', valTy) ← synthStmtExpr val @@ -829,7 +799,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do | [singleOutput] => -- Only check when body produces a value (not void from return/while/assign) if bodyTy.val != HighType.TVoid then - checkAssignable proc.name.source singleOutput.type bodyTy + checkSubtype proc.name.source singleOutput.type bodyTy | _ => pure () let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', @@ -869,7 +839,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv match proc.outputs with | [singleOutput] => if bodyTy.val != HighType.TVoid then - checkAssignable proc.name.source singleOutput.type bodyTy + checkSubtype proc.name.source singleOutput.type bodyTy | _ => pure () let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr modify fun s => { s with instanceTypeName := savedInstType } From 054a8ba39bbe67b2a390e9c1e91ed368988303f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 14:56:38 -0400 Subject: [PATCH 076/128] quantifier check for bool body --- Strata/Languages/Laurel/Resolution.lean | 2 +- docs/verso/LaurelDoc.lean | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 1259185178..01676402aa 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -678,7 +678,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do let (e', _) ← synthStmtExpr pv.val; pure e') - let (body', _) ← synthStmtExpr body + let body' ← checkStmtExpr body { val := .TBool, source := body.source } pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) | .Assigned name => let (name', _) ← synthStmtExpr name diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4abf2eff2c..34217be4ab 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -641,12 +641,14 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. #### Quantifier ``` - Γ, x : T ⊢ body ⇒ _ + Γ, x : T ⊢ body ⇐ TBool ───────────────────────────────────────────────── (Quantifier, impl) Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool ``` -The bound variable `x : T` is introduced in scope only for the body (and trigger). +The bound variable `x : T` is introduced in scope only for the body (and trigger). The body +is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a +proposition; without this, `forall x: int :: x + 1` would be silently accepted. #### Assigned From 597c79e687987c2e7b11e52103e44bb9f00aa42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 15:03:02 -0400 Subject: [PATCH 077/128] remove redundant headings --- docs/verso/LaurelDoc.lean | 96 --------------------------------------- 1 file changed, 96 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 34217be4ab..6bb9271cbf 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -267,8 +267,6 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x ### Subsumption -#### Sub - ``` Γ ⊢ e ⇒ A A <: B ───────────────────── (Sub, impl) @@ -279,29 +277,21 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### Literals -#### Lit-Int - ``` ────────────────────────── (Lit-Int, impl) Γ ⊢ LiteralInt n ⇒ TInt ``` -#### Lit-Bool - ``` ─────────────────────────── (Lit-Bool, impl) Γ ⊢ LiteralBool b ⇒ TBool ``` -#### Lit-String - ``` ───────────────────────────────── (Lit-String, impl) Γ ⊢ LiteralString s ⇒ TString ``` -#### Lit-Decimal - ``` ────────────────────────────────── (Lit-Decimal, impl) Γ ⊢ LiteralDecimal d ⇒ TReal @@ -309,16 +299,12 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### Variables -#### Var-Local - ``` Γ(x) = T ─────────────────────────── (Var-Local, impl) Γ ⊢ Var (.Local x) ⇒ T ``` -#### Var-Field - ``` Γ ⊢ e ⇒ _ Γ(f) = T_f ────────────────────────────── (Var-Field, impl) @@ -328,8 +314,6 @@ Fallback in `checkStmtExpr` whenever no bespoke check rule applies. Resolution looks `f` up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -#### Var-Declare - ``` x ∉ dom(Γ) ───────────────────────────────────────── (Var-Declare, impl) @@ -341,8 +325,6 @@ remainder of the enclosing scope. ### Control flow -#### If-NoElse - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T ───────────────────────────────────────────── (If-NoElse, impl) @@ -353,8 +335,6 @@ The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because the value when `cond` is false; without this, `x : int := if c then 5` would type-check spuriously. -#### If-Synth - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e ────────────────────────────────────────────────────────────── (If-Synth, impl) @@ -366,16 +346,12 @@ statement-position `if` often pairs a value branch with a `return`/`exit`/`asser enclosing context's check (Sub, or a containing `checkSubtype` like an assignment) provides the actual check downstream. -#### If-Check - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T ────────────────────────────────────────────────────────── (If-Check, planned) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T ``` -#### Block-Synth - ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T ─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) @@ -391,15 +367,11 @@ Non-last statements are synthesized but their types discarded (the lax rule). Th Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` is silently accepted; flagging it belongs to a lint. -#### Block-Synth-Empty - ``` ───────────────────────────── (Block-Synth-Empty, impl) Γ ⊢ Block [] label ⇒ TVoid ``` -#### Block-Check - ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T ─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) @@ -413,30 +385,22 @@ nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. -#### Block-Check-Empty - ``` TVoid <: T ───────────────────────── (Block-Check-Empty, impl) Γ ⊢ Block [] label ⇐ T ``` -#### Exit - ``` ──────────────────────── (Exit, impl) Γ ⊢ Exit target ⇒ TVoid ``` -#### Return-None - ``` ───────────────────────────── (Return-None, impl) Γ ⊢ Return none ⇒ TVoid ``` -#### Return-Some - ``` Γ ⊢ e ⇒ _ ────────────────────────────── (Return-Some, impl) @@ -447,8 +411,6 @@ The value's synthesized type is currently discarded, so `return 0` in a `bool`-r procedure isn't caught. Replaced by Return-Some-Checked once the expected return type is threaded through {name Strata.Laurel.ResolveState}`ResolveState`. -#### Return-Some-Checked - ``` Γ_proc.outputs = [T] Γ ⊢ e ⇐ T ────────────────────────────────────── (Return-Some-Checked, planned) @@ -458,8 +420,6 @@ threaded through {name Strata.Laurel.ResolveState}`ResolveState`. Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / {name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`. -#### While - ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ ─────────────────────────────────────────────────────────────────────────────── (While, impl) @@ -471,16 +431,12 @@ target is a numeric type. ### Verification statements -#### Assert - ``` Γ ⊢ cond ⇐ TBool ────────────────────────────── (Assert, impl) Γ ⊢ Assert cond ⇒ TVoid ``` -#### Assume - ``` Γ ⊢ cond ⇐ TBool ───────────────────────────── (Assume, impl) @@ -489,16 +445,12 @@ target is a numeric type. ### Assignment -#### Assign-Single - ``` Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x ─────────────────────────────────────────────── (Assign-Single, impl) Γ ⊢ Assign [x] e ⇒ TVoid ``` -#### Assign-Multi - ``` Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i ───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl) @@ -507,8 +459,6 @@ target is a numeric type. ### Calls -#### Static-Call - ``` Γ(callee) = static-procedure with inputs Ts and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) @@ -516,8 +466,6 @@ target is a numeric type. Γ ⊢ StaticCall callee args ⇒ T ``` -#### Static-Call-Multi - ``` Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) @@ -525,8 +473,6 @@ target is a numeric type. Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] ``` -#### Instance-Call - ``` Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) @@ -540,32 +486,24 @@ target is a numeric type. {name Strata.Laurel.HighType.TReal}`TReal`, {name Strata.Laurel.HighType.TFloat64}`TFloat64`". -#### Op-Bool - ``` Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} ────────────────────────────────── (Op-Bool, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -#### Op-Cmp - ``` Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} ───────────────────────────────── (Op-Cmp, impl) Γ ⊢ PrimitiveOp op args ⇒ TBool ``` -#### Op-Eq - ``` Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} ───────────────────────────────────────────────────────────────── (Op-Eq, impl) Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` -#### Op-Arith - ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} ────────────────────────────────────────────────── (Op-Arith, impl) @@ -576,8 +514,6 @@ target is a numeric type. etc. without unification. Known relaxation: `int + real` passes (each operand individually passes `Numeric`); a proper fix needs numeric promotion or unification. -#### Op-Concat - ``` Γ ⊢ args_i ⇐ TString op = StrConcat ───────────────────────────────────── (Op-Concat, impl) @@ -586,24 +522,18 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ### Object forms -#### New-Ok - ``` Γ(ref) is a composite or datatype T ────────────────────────────────────────── (New-Ok, impl) Γ ⊢ New ref ⇒ UserDefined T ``` -#### New-Fallback - ``` Γ(ref) is not a composite or datatype ───────────────────────────────────────── (New-Fallback, impl) Γ ⊢ New ref ⇒ Unknown ``` -#### AsType - ``` Γ ⊢ target ⇒ _ ───────────────────────────── (AsType, impl) @@ -612,24 +542,18 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. `target` is resolved but not checked against `T` — the cast is the user's claim. -#### IsType - ``` Γ ⊢ target ⇒ _ ───────────────────────────────── (IsType, impl) Γ ⊢ IsType target T ⇒ TBool ``` -#### RefEq - ``` Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ ─────────────────────────────────────── (RefEq, impl) Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` -#### PureFieldUpdate - ``` Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ ─────────────────────────────────────────────── (PureFieldUpdate, impl) @@ -638,8 +562,6 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ### Verification expressions -#### Quantifier - ``` Γ, x : T ⊢ body ⇐ TBool ───────────────────────────────────────────────── (Quantifier, impl) @@ -650,32 +572,24 @@ The bound variable `x : T` is introduced in scope only for the body (and trigger is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a proposition; without this, `forall x: int :: x + 1` would be silently accepted. -#### Assigned - ``` Γ ⊢ name ⇒ _ ───────────────────────────── (Assigned, impl) Γ ⊢ Assigned name ⇒ TBool ``` -#### Old - ``` Γ ⊢ v ⇒ T ───────────────── (Old, impl) Γ ⊢ Old v ⇒ T ``` -#### Fresh - ``` Γ ⊢ v ⇒ _ ───────────────────── (Fresh, impl) Γ ⊢ Fresh v ⇒ TBool ``` -#### ProveBy - ``` Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ ─────────────────────────────────── (ProveBy, impl) @@ -684,15 +598,11 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. ### Untyped forms -#### This - ``` ────────────────────────── (This, impl) Γ ⊢ This ⇒ Unknown ``` -#### Abstract / All / ContractOf - ``` ───────────────────────────────────────────── (Abstract / All / ContractOf, impl) Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown @@ -700,22 +610,16 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. ### Holes -#### Hole-Some - ``` ──────────────────────────── (Hole-Some, impl) Γ ⊢ Hole d (some T) ⇒ T ``` -#### Hole-None-Synth - ``` ───────────────────────────────── (Hole-None-Synth, impl) Γ ⊢ Hole d none ⇒ Unknown ``` -#### Hole-None-Check - ``` Unknown <: T ───────────────────────── (Hole-None-Check, planned) From 3ea24314eb3cdb1358cbbb95caed4e68692a6bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 15:09:05 -0400 Subject: [PATCH 078/128] class-rules : updates on same-typed fields + this in class context only --- Strata/Languages/Laurel/Resolution.lean | 17 +++++++++++-- docs/verso/LaurelDoc.lean | 32 +++++++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 01676402aa..bd139a67d1 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -590,7 +590,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .PureFieldUpdate target fieldName newVal => let (target', targetTy) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let (newVal', _) ← synthStmtExpr newVal + let fieldTy ← getVarType fieldName' + let newVal' ← checkStmtExpr newVal fieldTy pure (.PureFieldUpdate target' fieldName' newVal', targetTy) | .StaticCall callee args => let callee' ← resolveRef callee source @@ -646,7 +647,19 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let ty := if kindOk then { val := HighType.UserDefined ref', source := source } else { val := HighType.Unknown, source := source } pure (.New ref', ty) - | .This => pure (.This, { val := .Unknown, source := source }) + | .This => + let s ← get + match s.instanceTypeName with + | some typeName => + let typeId : Identifier := + match s.scope.get? typeName with + | some (uid, _) => { text := typeName, uniqueId := some uid, source := source } + | none => { text := typeName, source := source } + pure (.This, { val := .UserDefined typeId, source := source }) + | none => + let diag := diagnosticFromSource source "'this' is not allowed outside instance methods" + modify fun s => { s with errors := s.errors.push diag } + pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => let (lhs', _) ← synthStmtExpr lhs let (rhs', _) ← synthStmtExpr rhs diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 6bb9271cbf..8506285865 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -262,7 +262,8 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate - *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy -- *Untyped forms* — This; Abstract / All / ContractOf +- *Self reference* — This-Inside, This-Outside +- *Untyped forms* — Abstract / All / ContractOf - *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) ### Subsumption @@ -555,11 +556,14 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ``` ``` - Γ ⊢ target ⇒ T_t Γ ⊢ newVal ⇒ _ -─────────────────────────────────────────────── (PureFieldUpdate, impl) - Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t + Γ ⊢ target ⇒ T_t Γ(f) = T_f Γ ⊢ newVal ⇐ T_f +───────────────────────────────────────────────────────────── (PureFieldUpdate, impl) + Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t ``` +`f` is resolved against `T_t` (or the enclosing instance type) and `newVal` is checked +against the field's declared type. + ### Verification expressions ``` @@ -596,13 +600,27 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. Γ ⊢ ProveBy v proof ⇒ T ``` -### Untyped forms +### Self reference ``` -────────────────────────── (This, impl) - Γ ⊢ This ⇒ Unknown + Γ.instanceTypeName = some T +────────────────────────────────── (This-Inside, impl) + Γ ⊢ This ⇒ UserDefined T + + + Γ.instanceTypeName = none +────────────────────────────── (This-Outside, impl) + Γ ⊢ This ⇒ Unknown [emits "'this' is not allowed outside instance methods"] ``` +`Γ.instanceTypeName` is the +{name Strata.Laurel.ResolveState}`ResolveState` field set by +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of +an instance method body. With it, `this.field` and instance-method dispatch synthesize real +types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}`Unknown`. + +### Untyped forms + ``` ───────────────────────────────────────────── (Abstract / All / ContractOf, impl) Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown From 787282667faf36164c86ffed3fc832bbe75a3a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 15:37:40 -0400 Subject: [PATCH 079/128] references checks --- Strata/Languages/Laurel/Resolution.lean | 21 ++++++++++++++++++--- docs/verso/LaurelDoc.lean | 23 +++++++++++++++++------ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index bd139a67d1..3df24ce05e 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -452,6 +452,15 @@ private def isConsistentNumeric (ty : HighTypeMd) : Bool := | .TCore _ => true | _ => false +/-- Test whether a type is a user-defined reference type, modulo gradual + consistency. Used by Fresh and ReferenceEquals, which only make sense on + composite/datatype references. -/ +private def isConsistentReference (ty : HighTypeMd) : Bool := + match ty.val with + | .UserDefined _ | .Unknown => true + | .TCore _ => true + | _ => false + /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -661,8 +670,12 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := modify fun s => { s with errors := s.errors.push diag } pure (.This, { val := .Unknown, source := source }) | .ReferenceEquals lhs rhs => - let (lhs', _) ← synthStmtExpr lhs - let (rhs', _) ← synthStmtExpr rhs + let (lhs', lhsTy) ← synthStmtExpr lhs + let (rhs', rhsTy) ← synthStmtExpr rhs + unless isConsistentReference lhsTy do + typeMismatch lhsTy.source "a reference type" lhsTy + unless isConsistentReference rhsTy do + typeMismatch rhsTy.source "a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => let (target', _) ← synthStmtExpr target @@ -700,7 +713,9 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (val', valTy) ← synthStmtExpr val pure (.Old val', valTy) | .Fresh val => - let (val', _) ← synthStmtExpr val + let (val', valTy) ← synthStmtExpr val + unless isConsistentReference valTy do + typeMismatch valTy.source "a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 8506285865..2c7f9542d2 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -550,11 +550,18 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. ``` ``` - Γ ⊢ lhs ⇒ _ Γ ⊢ rhs ⇒ _ -─────────────────────────────────────── (RefEq, impl) - Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool + Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r isReference T_l isReference T_r +───────────────────────────────────────────────────────────────────────────── (RefEq, impl) + Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` +`isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined`, +{name Strata.Laurel.HighType.Unknown}`Unknown`, or {name Strata.Laurel.HighType.TCore}`TCore` +type. Reference equality is meaningless on primitives. Compatibility between `T_l` and +`T_r` (e.g. rejecting `Cat === Dog` for unrelated user-defined types) is delegated to +future tightening of `<:` — today, two distinct user-defined names already mismatch +structurally, so the check would only fire under stronger subtyping. + ``` Γ ⊢ target ⇒ T_t Γ(f) = T_f Γ ⊢ newVal ⇐ T_f ───────────────────────────────────────────────────────────── (PureFieldUpdate, impl) @@ -589,11 +596,15 @@ proposition; without this, `forall x: int :: x + 1` would be silently accepted. ``` ``` - Γ ⊢ v ⇒ _ -───────────────────── (Fresh, impl) - Γ ⊢ Fresh v ⇒ TBool + Γ ⊢ v ⇒ T isReference T +───────────────────────────────── (Fresh, impl) + Γ ⊢ Fresh v ⇒ TBool ``` +`isReference T` is the same predicate as in {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals`. +{name Strata.Laurel.StmtExpr.Fresh}`Fresh` only makes sense on heap-allocated references; +`fresh(5)` is rejected. + ``` Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ ─────────────────────────────────── (ProveBy, impl) From b212eb5404b2aad4f0d5388a38c2a4d46f71dd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 16:21:06 -0400 Subject: [PATCH 080/128] pretty printers --- Strata/Languages/Laurel/Laurel.lean | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index 86ae83d022..a7dc4377d2 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -100,6 +100,20 @@ inductive Operation : Type where | StrConcat deriving Repr +instance : ToString Operation where + toString + | .Eq => "==" | .Neq => "!=" + | .And => "&&" | .Or => "||" + | .Not => "!" | .Implies => "==>" + | .AndThen => "&&!" | .OrElse => "||!" + | .Neg => "-" | .Add => "+" + | .Sub => "-" | .Mul => "*" + | .Div => "/" | .Mod => "%" + | .DivT => "/t" | .ModT => "%t" + | .Lt => "<" | .Leq => "<=" + | .Gt => ">" | .Geq => ">=" + | .StrConcat => "++" + /-- A wrapper that pairs a value with source-level metadata such as source locations and annotations. All Laurel AST nodes are wrapped in @@ -334,6 +348,40 @@ inductive ContractType where | Reads | Modifies | Precondition | PostCondition end +/-- A short user-facing name for the construct, used in diagnostic messages. -/ +def StmtExpr.constrName : StmtExpr → String + | .IfThenElse .. => "if" + | .Block .. => "block" + | .While .. => "while" + | .Exit .. => "exit" + | .Return .. => "return" + | .LiteralInt .. => "integer literal" + | .LiteralBool .. => "boolean literal" + | .LiteralString .. => "string literal" + | .LiteralDecimal .. => "decimal literal" + | .Var .. => "variable" + | .Assign .. => ":=" + | .PureFieldUpdate .. => "field update" + | .StaticCall .. => "call" + | .PrimitiveOp op _ => toString op + | .New .. => "new" + | .This => "this" + | .ReferenceEquals .. => "reference equality" + | .AsType .. => "as" + | .IsType .. => "is" + | .InstanceCall .. => "method call" + | .Quantifier .. => "quantifier" + | .Assigned .. => "assigned" + | .Old .. => "old" + | .Fresh .. => "fresh" + | .Assert .. => "assert" + | .Assume .. => "assume" + | .ProveBy .. => "by" + | .ContractOf .. => "contractOf" + | .Abstract => "abstract" + | .All => "all" + | .Hole .. => "hole" + @[expose] abbrev HighTypeMd := AstNode HighType @[expose] abbrev StmtExprMd := AstNode StmtExpr @[expose] abbrev VariableMd := AstNode Variable From c51fc96efb94cf742a3b24af9dcd6c0b92117acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 16:38:07 -0400 Subject: [PATCH 081/128] better type mismatch diagnostics --- Strata/Languages/Laurel/Resolution.lean | 74 ++++++++++++++----------- docs/verso/LaurelDoc.lean | 36 +++++++----- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 3df24ce05e..26783e2de9 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -417,24 +417,35 @@ private def formatType (ty : HighTypeMd) : String := "(" ++ ", ".intercalate parts ++ ")" | other => toString (formatHighTypeVal other) -/-- Emit a type mismatch diagnostic. -/ -private def typeMismatch (source : Option FileRange) (expected : String) (actual : HighTypeMd) : ResolveM Unit := do - let actualStr := formatType actual - let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" +/-- Emit a type mismatch diagnostic. With a `construct`, the message is + "'' , got ''"; without, + ", got ''". -/ +private def typeMismatch (source : Option FileRange) (construct : Option StmtExpr) + (problem : String) (actual : HighTypeMd) : ResolveM Unit := do + let constructor := match construct with + | some c => s!"'{c.constrName}' " + | none => "" + let diag := diagnosticFromSource source s!"{constructor}{problem}, got '{formatType actual}'" modify fun s => { s with errors := s.errors.push diag } /-- Subtyping. Stub: structural equality via `highEq`. TODO: To be replaced with a real check that walks `extending` chains for composites, unfolds aliases, and unwraps constrained types to their base. -/ private def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup -/-- Gradual consistency-subtyping (Siek–Taha style): `Unknown` is the dynamic - type and is consistent with everything in either direction. `TCore` is a - migration escape hatch and is bivariantly compatible for now. -/ -private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := - match sub.val, sup.val with +/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the + dynamic type and is consistent with everything; otherwise the relation + delegates to structural equality. `TCore` is a temporary migration + escape hatch. -/ +private def isConsistent (a b : HighTypeMd) : Bool := + match a.val, b.val with | .Unknown, _ | _, .Unknown => true | .TCore _, _ | _, .TCore _ => true - | _, _ => isSubtype sub sup + | _, _ => highEq a b + +/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For the flat type + lattice this collapses to `sub ~ sup ∨ sub <: sup`. -/ +private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := + isConsistent sub sup || isSubtype sub sup /-- Type-level subtype check: emits the standard "expected/got" diagnostic when `actual` is not a consistent subtype of `expected`. Used at sites where the @@ -442,20 +453,20 @@ private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do unless isConsistentSubtype actual expected do - typeMismatch source (s!"'{formatType expected}'") actual + typeMismatch source none s!"expected '{formatType expected}'" actual -/-- Test whether a type is in the set of numeric primitives, modulo gradual - consistency. Used by Op-Cmp / Op-Arith. -/ -private def isConsistentNumeric (ty : HighTypeMd) : Bool := +/-- Test whether a type is in the set of numeric primitives. `Unknown` and + `TCore` are accepted as gradual escape hatches. Used by Op-Cmp / Op-Arith. -/ +private def isNumeric (ty : HighTypeMd) : Bool := match ty.val with | .TInt | .TReal | .TFloat64 | .Unknown => true | .TCore _ => true | _ => false -/-- Test whether a type is a user-defined reference type, modulo gradual - consistency. Used by Fresh and ReferenceEquals, which only make sense on - composite/datatype references. -/ -private def isConsistentReference (ty : HighTypeMd) : Bool := +/-- Test whether a type is a user-defined reference type. `Unknown` and `TCore` + are accepted as gradual escape hatches. Used by Fresh and ReferenceEquals, + which only make sense on composite/datatype references. -/ +private def isReference (ty : HighTypeMd) : Bool := match ty.val with | .UserDefined _ | .Unknown => true | .TCore _ => true @@ -630,14 +641,14 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := checkSubtype source { val := .TBool, source := aTy.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => for aTy in argTypes do - unless isConsistentNumeric aTy do typeMismatch aTy.source "a numeric type" aTy + unless isNumeric aTy do + typeMismatch aTy.source (some expr) "expected a numeric type" aTy | .Eq | .Neq => - -- Symmetric: pass if either direction is consistent. match argTypes with | [lhsTy, rhsTy] => - unless isConsistentSubtype lhsTy rhsTy || isConsistentSubtype rhsTy lhsTy do + unless isConsistent lhsTy rhsTy do let diag := diagnosticFromSource source - s!"Operands of '==' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" modify fun s => { s with errors := s.errors.push diag } | _ => pure () | .StrConcat => @@ -672,10 +683,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .ReferenceEquals lhs rhs => let (lhs', lhsTy) ← synthStmtExpr lhs let (rhs', rhsTy) ← synthStmtExpr rhs - unless isConsistentReference lhsTy do - typeMismatch lhsTy.source "a reference type" lhsTy - unless isConsistentReference rhsTy do - typeMismatch rhsTy.source "a reference type" rhsTy + unless isReference lhsTy do + typeMismatch lhsTy.source (some expr) "expected a reference type" lhsTy + unless isReference rhsTy do + typeMismatch rhsTy.source (some expr) "expected a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => let (target', _) ← synthStmtExpr target @@ -714,8 +725,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.Old val', valTy) | .Fresh val => let (val', valTy) ← synthStmtExpr val - unless isConsistentReference valTy do - typeMismatch valTy.source "a reference type" valTy + unless isReference valTy do + typeMismatch valTy.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } @@ -761,9 +772,7 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE let (s', _) ← synthStmtExpr s; pure s') match _lastResult: stmts.getLast? with | none => - let tvoid : HighTypeMd := { val := .TVoid, source := source } - unless isConsistentSubtype tvoid expected do - typeMismatch source (formatType expected) tvoid + checkSubtype source expected { val := .TVoid, source := source } pure { val := .Block init' label, source := source } | some last => have := List.mem_of_getLast? _lastResult @@ -772,8 +781,7 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd - unless isConsistentSubtype actual expected do - typeMismatch source (formatType expected) actual + checkSubtype source expected actual pure e' termination_by (exprMd, 1) decreasing_by all_goals first diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 2c7f9542d2..4ff9cd7f0f 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -201,27 +201,30 @@ internal interface used by other rules. ### Gradual typing -The relation `<:` is implemented by two Lean functions — both currently stubs, both -intended to be sharpened: +The relation `<:` (used in Sub) is built from three Lean functions: - `isSubtype` — pure subtyping. The stub is structural equality via {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. -- `isConsistentSubtype` — gradual consistency, in the Siek–Taha sense. - {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type `?` and is consistent - with everything in either direction; otherwise the relation delegates to `isSubtype`. - {name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now, as a - clearly-labelled migration escape hatch from the Core language — this carve-out is - intentionally temporary. - -Subsumption (and every bespoke check rule) uses `isConsistentSubtype`, never raw -`isSubtype`. That single choice is what makes the system *gradual*: an expression of type +- `isConsistent` — the symmetric gradual relation `~` (Siek–Taha): + {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type and is consistent with + everything; otherwise structural equality. +- `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this + is the standard collapse of `∃R. T ~ R ∧ R <: U`. + +{name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now as a temporary +migration escape hatch from the Core language; the carve-out lives in `isConsistent` and is +intentionally temporary. + +Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what +makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely into any typed slot, and any expression flows freely into a slot of type {name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between -fully-known types only. +fully-known types only. The symmetric `isConsistent` is used directly by Op-Eq, where the +operand types must be mutually consistent (no subtype direction is privileged). A previous iteration was synth-only with three *bivariantly-compatible* wildcards: {name Strata.Laurel.HighType.Unknown}`Unknown`, @@ -500,11 +503,14 @@ target is a numeric type. ``` ``` - Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l <: T_r ∨ T_r <: T_l op ∈ {Eq, Neq} -───────────────────────────────────────────────────────────────── (Op-Eq, impl) - Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool + Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l ~ T_r op ∈ {Eq, Neq} +───────────────────────────────────────────────────────── (Op-Eq, impl) + Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool ``` +`~` is the consistency relation `isConsistent` — symmetric, with the +{name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. + ``` Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} ────────────────────────────────────────────────── (Op-Arith, impl) From a70d171f34b888ef84ffa6502965d68f74771a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 16:41:21 -0400 Subject: [PATCH 082/128] check ifthenelse --- Strata/Languages/Laurel/Resolution.lean | 10 ++++++++++ docs/verso/LaurelDoc.lean | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 26783e2de9..03fc98e54e 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -778,6 +778,16 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE have := List.mem_of_getLast? _lastResult let last' ← checkStmtExpr last expected pure { val := .Block (init' ++ [last']) label, source := source } + | .IfThenElse cond thenBr elseBr => + -- Push `expected` into both branches (rather than going through the synth + -- rule + Sub at the boundary). Without an else branch, fall back to + -- subsumption of TVoid against `expected`. + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let thenBr' ← checkStmtExpr thenBr expected + let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) + if elseBr.isNone then + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .IfThenElse cond' thenBr' elseBr', source := source } | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 4ff9cd7f0f..20550333f8 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -256,9 +256,9 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Subsumption* — Sub - *Literals* — Lit-Int, Lit-Bool, Lit-String, Lit-Decimal - *Variables* — Var-Local, Var-Field, Var-Declare -- *Control flow* — If-NoElse, If-Synth, If-Check (planned); Block-Synth, Block-Synth-Empty, - Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, Return-Some-Checked - (planned); While +- *Control flow* — If-NoElse, If-Synth, If-Check, If-Check-NoElse; Block-Synth, + Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, + Return-Some-Checked (planned); While - *Verification statements* — Assert, Assume - *Assignment* — Assign-Single, Assign-Multi - *Calls* — Static-Call, Static-Call-Multi, Instance-Call @@ -352,10 +352,21 @@ the actual check downstream. ``` Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T -────────────────────────────────────────────────────────── (If-Check, planned) +────────────────────────────────────────────────────────── (If-Check, impl) Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T + + +Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T TVoid <: T +───────────────────────────────────────────────────── (If-Check-NoElse, impl) + Γ ⊢ IfThenElse cond thenBr none ⇐ T ``` +Check mode pushes `T` into both branches (rather than going through If-Synth + Sub at the +boundary). Errors fire at the offending branch instead of the surrounding `if`. Without an +else branch, the construct can only succeed when `T` admits +{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `Block-Check-Empty` +performs for an empty block. + ``` Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T ─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) From 453d293f94e778e0c9e62d573401b5418f1e24ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:01:12 -0400 Subject: [PATCH 083/128] type check returns multiple return arity : 0 = Void 1 = return expr // return; with returns (res:T) signature n = return; allowed only --- Strata/Languages/Laurel/Resolution.lean | 33 ++++++++++++++++++- docs/verso/LaurelDoc.lean | 43 ++++++++++++++++++------- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 03fc98e54e..e781c9b65d 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -262,6 +262,10 @@ structure ResolveState where /-- When resolving inside an instance procedure, the owning composite type name. Used by `resolveFieldRef` to resolve `self.field` when `self` has type `Any`. -/ instanceTypeName : Option String := none + /-- When resolving inside a procedure body, the declared output types (in + declaration order). `none` means no enclosing procedure. Used by `Return` + to type-check the optional return value and to flag arity/shape mismatches. -/ + expectedReturnTypes : Option (List HighTypeMd) := none @[expose] abbrev ResolveM := StateM ResolveState @@ -543,8 +547,29 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) | .Return val => do + -- Match the optional return value against the enclosing procedure's + -- declared outputs. `expectedReturnTypes = none` means we're not inside a + -- procedure body (e.g. resolving a constant initializer); skip the check. + let expected := (← get).expectedReturnTypes let val' ← val.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') + match expected with + | some [singleOutput] => checkStmtExpr a.val singleOutput + | _ => let (e', _) ← synthStmtExpr a.val; pure e') + -- Arity/shape diagnostics independent of the value's own type. + match val, expected with + | none, some [] => pure () + | none, some [_] => pure () -- Dafny-style early exit + | none, some _ => pure () -- multi-output: bare return is fine + | some _, some [] => + let diag := diagnosticFromSource source + "void procedure cannot return a value" + modify fun s => { s with errors := s.errors.push diag } + | some _, some [_] => pure () -- value already checked above + | some _, some _ => + let diag := diagnosticFromSource source + "multi-output procedure cannot use 'return e'; assign to named outputs instead" + modify fun s => { s with errors := s.errors.push diag } + | _, none => pure () -- no enclosing procedure pure (.Return val', { val := .TVoid, source := source }) | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) @@ -834,7 +859,10 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do let outputs' ← proc.outputs.mapM resolveParameter let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) let dec' ← proc.decreases.mapM resolveStmtExpr + let savedReturns := (← get).expectedReturnTypes + modify fun s => { s with expectedReturnTypes := some (outputs'.map (·.type)) } let (body', bodyTy) ← resolveBody proc.body + modify fun s => { s with expectedReturnTypes := savedReturns } if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" @@ -875,7 +903,10 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv let outputs' ← proc.outputs.mapM resolveParameter let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExpr) let dec' ← proc.decreases.mapM resolveStmtExpr + let savedReturns := (← get).expectedReturnTypes + modify fun s => { s with expectedReturnTypes := some (outputs'.map (·.type)) } let (body', bodyTy) ← resolveBody proc.body + modify fun s => { s with expectedReturnTypes := savedReturns } if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 20550333f8..b7b74c5af2 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -258,7 +258,7 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Variables* — Var-Local, Var-Field, Var-Declare - *Control flow* — If-NoElse, If-Synth, If-Check, If-Check-NoElse; Block-Synth, Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, - Return-Some-Checked (planned); While + Return-Void-Error, Return-Multi-Error; While - *Verification statements* — Assert, Assume - *Assignment* — Assign-Single, Assign-Multi - *Calls* — Static-Call, Static-Call-Multi, Instance-Call @@ -411,29 +411,48 @@ nested {name Strata.Laurel.StmtExpr.Block}`Block` / Γ ⊢ Exit target ⇒ TVoid ``` +`Return` matches the optional return value against the enclosing procedure's declared +outputs. The expected output types are threaded through +{name Strata.Laurel.ResolveState}`ResolveState`'s `expectedReturnTypes`, set from +`proc.outputs` by {name Strata.Laurel.resolveProcedure}`resolveProcedure` / +{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of +the body. `none` means "no enclosing procedure" — e.g. resolving a constant initializer — +and skips all `Return` checks. + ``` ───────────────────────────── (Return-None, impl) Γ ⊢ Return none ⇒ TVoid ``` +A bare `return;` is allowed in any context. In a single-output procedure it acts as a +Dafny-style early exit — the output parameter retains whatever was last assigned to it. + ``` - Γ ⊢ e ⇒ _ -────────────────────────────── (Return-Some, impl) - Γ ⊢ Return (some e) ⇒ TVoid + Γ_proc.outputs = [T] Γ ⊢ e ⇐ T +────────────────────────────────────── (Return-Some, impl) + Γ ⊢ Return (some e) ⇒ TVoid ``` -The value's synthesized type is currently discarded, so `return 0` in a `bool`-returning -procedure isn't caught. Replaced by Return-Some-Checked once the expected return type is -threaded through {name Strata.Laurel.ResolveState}`ResolveState`. +In a single-output procedure, the value is checked against the declared output type. This +closes the prior soundness gap where `return 0` in a `bool`-returning procedure went +uncaught. ``` - Γ_proc.outputs = [T] Γ ⊢ e ⇐ T -────────────────────────────────────── (Return-Some-Checked, planned) - Γ ⊢ Return (some e) ⇒ TVoid + Γ_proc.outputs = [] +───────────────────────────────── (Return-Void-Error, impl) + Γ ⊢ Return (some e) — error: "void procedure cannot return a value" + + + Γ_proc.outputs = [T_1; …; T_n] (n ≥ 2) +────────────────────────────────────────────────────────── (Return-Multi-Error, impl) + Γ ⊢ Return (some e) — error: "multi-output procedure cannot + use 'return e'; assign to named outputs instead" ``` -Set from `proc.outputs` in {name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure`. +Multi-output procedures use named-output assignment (`r := …` on the declared output +parameters). `return e` syntactically takes a single +{name Strata.Laurel.StmtExpr.Return}`Option StmtExpr`, so it cannot carry multiple values; +flagging it points users at the named-output convention. ``` Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ From 7bc098dd20af45249423e2cf06e0f0e25583dbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:15:26 -0400 Subject: [PATCH 084/128] type check procedures contracts --- Strata/Languages/Laurel/Resolution.lean | 25 ++++++++++++- docs/verso/LaurelDoc.lean | 47 +++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index e781c9b65d..a0e441092c 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -764,8 +764,31 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (proof', _) ← synthStmtExpr proof pure (.ProveBy val' proof', valTy) | .ContractOf ty fn => + -- `fn` must be a direct identifier reference resolving to a procedure. + -- Anything else (arbitrary expressions, references to non-procedures) is + -- ill-formed: a contract belongs to a *named* procedure. let (fn', _) ← synthStmtExpr fn - pure (.ContractOf ty fn', { val := .Unknown, source := source }) + let s ← get + let fnIsProcRef : Bool := match fn'.val with + | .Var (.Local ref) => + match s.scope.get? ref.text with + | some (_, node) => + node.kind == .staticProcedure || + node.kind == .instanceProcedure || + node.kind == .unresolved + | none => true -- unresolved name already reported + | _ => false + unless fnIsProcRef do + let diag := diagnosticFromSource fn.source + "'contractOf' expected a procedure reference" + modify fun s => { s with errors := s.errors.push diag } + -- Result type: Bool for pre/postconditions, set of heap references for + -- reads/modifies. The element type of the set is left as Unknown for now + -- since the rule doesn't recover it from `fn`. + let resultTy : HighType := match ty with + | .Precondition | .PostCondition => .TBool + | .Reads | .Modifies => .TSet { val := .Unknown, source := none } + pure (.ContractOf ty fn', { val := resultTy, source := source }) | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) | .All => pure (.All, { val := .Unknown, source := source }) | .Hole det type => match type with diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index b7b74c5af2..b98a1bda98 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -266,7 +266,8 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate - *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy - *Self reference* — This-Inside, This-Outside -- *Untyped forms* — Abstract / All / ContractOf +- *Untyped forms* — Abstract / All +- *ContractOf* — ContractOf-Bool, ContractOf-Set, ContractOf-Error - *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) ### Subsumption @@ -669,10 +670,50 @@ types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}` ### Untyped forms ``` -───────────────────────────────────────────── (Abstract / All / ContractOf, impl) - Γ ⊢ Abstract / All / ContractOf … ⇒ Unknown +───────────────────────────────── (Abstract / All, impl) + Γ ⊢ Abstract / All … ⇒ Unknown ``` +### ContractOf + +`ContractOf ty fn` extracts a procedure's contract clause as a value: its preconditions +(`Precondition`), postconditions (`PostCondition`), reads set (`Reads`), or modifies set +(`Modifies`). `fn` must be a direct identifier reference to a procedure — a contract belongs +to a *named* procedure, not an arbitrary expression. + +``` + fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} +───────────────────────────────────────────────────────────────────────── (ContractOf-Bool, impl) + Γ ⊢ ContractOf Precondition fn ⇒ TBool + Γ ⊢ ContractOf PostCondition fn ⇒ TBool + + + fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} +───────────────────────────────────────────────────────────────────────── (ContractOf-Set, impl) + Γ ⊢ ContractOf Reads fn ⇒ TSet Unknown + Γ ⊢ ContractOf Modifies fn ⇒ TSet Unknown +``` + +`Precondition` and `PostCondition` are propositions, hence +{name Strata.Laurel.HighType.TBool}`TBool`. `Reads` and `Modifies` are sets of heap-allocated +locations — composite/datatype references and fields. The element type is left as +{name Strata.Laurel.HighType.Unknown}`Unknown` for now since the rule doesn't yet recover it +from `fn`'s declared modifies/reads clauses. + +``` + fn is not a procedure reference +───────────────────────────────────────────── (ContractOf-Error, impl) + Γ ⊢ ContractOf … fn — error: "'contractOf' expected a procedure reference" +``` + +When `fn` doesn't resolve to a procedure (e.g. it's an arbitrary expression, or resolves to +a constant/variable), the diagnostic fires and the construct synthesizes +{name Strata.Laurel.HighType.Unknown}`Unknown` to suppress cascading errors. + +The constructor is reserved for future use — Laurel's grammar has no `contractOf` +production today, and the translator emits "not yet implemented" for it. The typing rule +exists so resolution remains exhaustive over `StmtExpr`. + ### Holes ``` From c69210a638e66a382cdd0c4889ffedbb5edb6792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:28:15 -0400 Subject: [PATCH 085/128] check untyped holes --- Strata/Languages/Laurel/Resolution.lean | 5 +++++ docs/verso/LaurelDoc.lean | 17 ++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index a0e441092c..5022293b73 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -836,6 +836,11 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE if elseBr.isNone then checkSubtype source expected { val := .TVoid, source := source } pure { val := .IfThenElse cond' thenBr' elseBr', source := source } + | .Hole det none => + -- Untyped hole in check mode: record the expected type on the node so + -- downstream passes don't have to infer it again. Subsumption is trivial + -- (Unknown <: T always holds). + pure { val := .Hole det (some expected), source := source } | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index b98a1bda98..68c1fe0f62 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -268,7 +268,7 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x - *Self reference* — This-Inside, This-Outside - *Untyped forms* — Abstract / All - *ContractOf* — ContractOf-Bool, ContractOf-Set, ContractOf-Error -- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check (planned) +- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check ### Subsumption @@ -727,14 +727,17 @@ exists so resolution remains exhaustive over `StmtExpr`. ``` ``` - Unknown <: T -───────────────────────── (Hole-None-Check, planned) - Γ ⊢ Hole d none ⇐ T +───────────────────────────────────── (Hole-None-Check, impl) + Γ ⊢ Hole d none ⇐ T ↦ Hole d (some T) ``` -In check mode today, `Hole d none ⇐ T` reduces to subsumption (`Unknown <: T`, which always -holds). The planned rule would record the inferred `T` on the hole node so downstream -passes can see it, instead of leaving `none` until the hole-inference pass. +In check mode, an untyped hole records the expected type `T` on the node directly. The +subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it +just preserves the type information that's available at the check-mode boundary instead of +discarding it. A separate +{name Strata.Laurel.InferHoleTypes}`InferHoleTypes` pass still runs after resolution to +annotate holes that ended up in synth-only positions; over time, as more constructs gain +bespoke check rules, fewer holes will need that pass. # Translation Pipeline From e823cef7284b43feee2ae98b286bfbde98efb4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:28:49 -0400 Subject: [PATCH 086/128] remove dangling reference --- docs/verso/LaurelDoc.lean | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 68c1fe0f62..bf812f7eb5 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -734,10 +734,9 @@ exists so resolution remains exhaustive over `StmtExpr`. In check mode, an untyped hole records the expected type `T` on the node directly. The subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it just preserves the type information that's available at the check-mode boundary instead of -discarding it. A separate -{name Strata.Laurel.InferHoleTypes}`InferHoleTypes` pass still runs after resolution to -annotate holes that ended up in synth-only positions; over time, as more constructs gain -bespoke check rules, fewer holes will need that pass. +discarding it. A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended +up in synth-only positions; over time, as more constructs gain bespoke check rules, fewer +holes will need that pass. # Translation Pipeline From 47ad5db6d71389b8111ed680f4f87dbf849f0684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:44:38 -0400 Subject: [PATCH 087/128] move subtyping/consistency rules in type definition --- Strata/Languages/Laurel/Laurel.lean | 19 +++++++++++++++++++ Strata/Languages/Laurel/Resolution.lean | 19 ------------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index a7dc4377d2..a8c665d64c 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -489,6 +489,25 @@ instance : BEq HighTypeMd where deriving instance BEq for HighType +/-- Subtyping. Stub: structural equality via `highEq`. + TODO: walk `extending` chains for composites, unfold aliases, unwrap + constrained types to their base. -/ +def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup + +/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the + dynamic type and is consistent with everything; otherwise structural + equality. `TCore` is a temporary migration escape hatch. -/ +def isConsistent (a b : HighTypeMd) : Bool := + match a.val, b.val with + | .Unknown, _ | _, .Unknown => true + | .TCore _, _ | _, .TCore _ => true + | _, _ => highEq a b + +/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice + this collapses to `sub ~ sup ∨ sub <: sup`. -/ +def isConsistentSubtype (sub sup : HighTypeMd) : Bool := + isConsistent sub sup || isSubtype sub sup + def HighType.isBool : HighType → Bool | TBool => true | _ => false diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 5022293b73..8c88d5189b 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -432,25 +432,6 @@ private def typeMismatch (source : Option FileRange) (construct : Option StmtExp let diag := diagnosticFromSource source s!"{constructor}{problem}, got '{formatType actual}'" modify fun s => { s with errors := s.errors.push diag } -/-- Subtyping. Stub: structural equality via `highEq`. - TODO: To be replaced with a real check that walks `extending` chains for composites, unfolds aliases, and unwraps constrained types to their base. -/ -private def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup - -/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the - dynamic type and is consistent with everything; otherwise the relation - delegates to structural equality. `TCore` is a temporary migration - escape hatch. -/ -private def isConsistent (a b : HighTypeMd) : Bool := - match a.val, b.val with - | .Unknown, _ | _, .Unknown => true - | .TCore _, _ | _, .TCore _ => true - | _, _ => highEq a b - -/-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For the flat type - lattice this collapses to `sub ~ sup ∨ sub <: sup`. -/ -private def isConsistentSubtype (sub sup : HighTypeMd) : Bool := - isConsistent sub sup || isSubtype sub sup - /-- Type-level subtype check: emits the standard "expected/got" diagnostic when `actual` is not a consistent subtype of `expected`. Used at sites where the actual type is already in hand (assignment, call args, body vs declared From 40e1572215ccd4cb0a4fba28ba81dd2a75e430d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:45:09 -0400 Subject: [PATCH 088/128] inferholetypes flag already filled hole types when inconsistent --- Strata/Languages/Laurel/InferHoleTypes.lean | 13 ++++++++++++- docs/verso/LaurelDoc.lean | 11 ++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Strata/Languages/Laurel/InferHoleTypes.lean b/Strata/Languages/Laurel/InferHoleTypes.lean index d56ad86881..026a82e5b9 100644 --- a/Strata/Languages/Laurel/InferHoleTypes.lean +++ b/Strata/Languages/Laurel/InferHoleTypes.lean @@ -87,7 +87,7 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol match expr with | AstNode.mk val source => match val with - | .Hole det _ => + | .Hole det existingTy => if expectedType.val == .Unknown then modify fun s => { s with statistics := s.statistics.increment s!"{InferHoleTypesStats.holesLeftUnknown}" @@ -95,6 +95,17 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol } return expr else + -- If the hole already carried a type (from resolution's Hole-None-Check + -- rule, or from a user-written `?: T`), flag a conflict when the two + -- types disagree under consistency (gradual ~). + match existingTy with + | some prior => + unless isConsistent prior expectedType do + modify fun s => { s with + diagnostics := s.diagnostics ++ [diagnosticFromSource source + s!"hole annotated with '{formatHighTypeVal prior.val}' but context expects '{formatHighTypeVal expectedType.val}'"] + } + | none => pure () modify fun s => { s with statistics := s.statistics.increment s!"{InferHoleTypesStats.holesAnnotated}" } return ⟨.Hole det (some expectedType), source⟩ | .PrimitiveOp op args => diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index bf812f7eb5..300c7393c7 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -734,9 +734,14 @@ exists so resolution remains exhaustive over `StmtExpr`. In check mode, an untyped hole records the expected type `T` on the node directly. The subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it just preserves the type information that's available at the check-mode boundary instead of -discarding it. A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended -up in synth-only positions; over time, as more constructs gain bespoke check rules, fewer -holes will need that pass. +discarding it. + +A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended +up in synth-only positions. When that pass encounters a hole whose type was already set +(by Hole-None-Check or by a user-written `?: T`), it checks the resolution-time and +inference-time types for consistency under `~`; a disagreement fires the diagnostic +*"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what +would otherwise be a silent overwrite. # Translation Pipeline From 3897097f8407645d7db5a91e7dfda400eb78509d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 17:50:14 -0400 Subject: [PATCH 089/128] future roadmap --- Strata/Languages/Laurel/Resolution.lean | 19 ++++++++++++ docs/verso/LaurelDoc.lean | 41 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 8c88d5189b..152b2bf529 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -104,6 +104,25 @@ Each of these nodes carries a `uniqueId : Option Nat` field (defaulting to `none`). Phase 1 fills in unique values; Phase 2 then builds a map from reference IDs to `ResolvedNode` values describing the definition each reference resolves to. + +## Future structural changes + +A few open structural questions worth recording — see the *Type checking* section of +`LaurelDoc.lean` for context. + +- *Rename to `NameTypeResolution`.* This pass resolves names and type-checks expressions in + one walk. The current name only mentions half of what it does. `NameTypeResolution.lean` + (or similar) would advertise both responsibilities. +- *Eliminate `LaurelTypes.computeExprType` by caching types.* Five later passes + (`LaurelToCoreTranslator`, `ModifiesClauses`, `LiftImperativeExpressions`, + `HeapParameterization`, `TypeHierarchy`) re-derive `StmtExpr` types after resolution. + Resolution already synthesizes those types and discards them. Caching per-node types on + `SemanticModel` (or directly on the AST) would let the later passes look them up instead + of recomputing. +- *Shrink or remove `InferHoleTypes`.* `Hole-None-Check` already records expected types + during resolution for holes in check-mode positions. Holes in synth-only positions still + need the post-pass, but as more constructs gain bespoke check rules, fewer holes need + it; eventually the pass can go away. -/ namespace Strata.Laurel diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 300c7393c7..0b39902de0 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -743,6 +743,47 @@ inference-time types for consistency under `~`; a disagreement fires the diagnos *"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what would otherwise be a silent overwrite. +## Future structural changes + +The current pipeline has resolution and several downstream passes that recompute or +re-derive type information that resolution already synthesized. A few cleanups worth +considering: + +### Rename `Resolution.lean` → `NameTypeResolution.lean` + +The pass resolves names *and* type-checks expressions in one walk; the file name only +advertises the first half. A rename (e.g. `NameTypeResolution.lean` or +`ResolutionAndTyping.lean`) would describe what the pass actually does. The +`SemanticModel` and `ResolvedNode` types could keep their names — they're about resolved +references, not typing. + +### Eliminate `LaurelTypes.computeExprType` by caching types + +`LaurelTypes.lean` exports `computeExprType : SemanticModel → StmtExprMd → HighTypeMd`, +which five later passes call (`LaurelToCoreTranslator`, `ModifiesClauses`, +`LiftImperativeExpressions`, `HeapParameterization`, `TypeHierarchy`) to ask "what's the +type of this expression?" after resolution. Resolution already synthesizes the same types +during its walk, then discards them. Two ways to remove the duplication: + +- *Cache types on the AST.* Add a `HighTypeMd` field to `StmtExpr` (or a parallel + `Std.HashMap Nat HighTypeMd` keyed by node-id, attached to `SemanticModel`), populate it + during resolution, and have later passes read it. `computeExprType` becomes a lookup, + not a re-traversal. +- *Make the cache opt-in.* Same idea, but only enable the type-cache for passes that need + it. Less invasive but partially defeats the point. + +The duplication isn't a correctness issue today (both paths produce consistent results), +just wasted work and a maintenance hazard. + +### Shrink or remove `InferHoleTypes` + +`InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that +Hole-None-Check writes the expected type during resolution for holes in check-mode +positions, the post-pass only needs to handle holes in synth-only positions (e.g. call +arguments resolved through `synthStmtExpr` instead of `checkStmtExpr`). As more constructs +gain bespoke check rules, fewer holes will reach `InferHoleTypes`; eventually the pass +can be deleted entirely. + # Translation Pipeline Laurel programs are verified by translating them to Strata Core and then invoking the Core From 86b6d2735eaf2894c412a99507f78f344ee288f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:15:30 -0400 Subject: [PATCH 090/128] fix multi value return destructuring --- Strata/Languages/Laurel/Resolution.lean | 54 ++++++++++++------------- docs/verso/LaurelDoc.lean | 25 +++++++----- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 152b2bf529..622b35c302 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -599,33 +599,33 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) let (value', valueTy) ← synthStmtExpr value - -- Check that LHS target count matches the RHS arity (derived from the value type). - let expectedOutputCount := match valueTy.val with - | .MultiValuedExpr tys => tys.length - | _ => 1 - if valueTy.val != HighType.TVoid && targets'.length != expectedOutputCount then - let diag := diagnosticFromSource source - s!"Assignment target count mismatch: {targets'.length} targets but right-hand side produces {expectedOutputCount} values" - modify fun s => { s with errors := s.errors.push diag } - -- Type check: for single-target assignments, check value type matches target type - -- Skip when value type is void (RHS is a statement like while/return that doesn't produce a value) - -- Skip when there's an arity mismatch (already reported above) - if targets'.length == 1 && targets'.length == expectedOutputCount && valueTy.val != HighType.TVoid then - if let some target := targets'.head? then - let targetTy := match target.val with - | .Local ref => do - let s ← get - match s.scope.get? ref.text with - | some (_, node) => pure node.getType - | none => pure { val := HighType.Unknown, source := ref.source : HighTypeMd } - | .Declare param => pure param.type - | .Field _ fieldName => do - let s ← get - match s.scope.get? fieldName.text with - | some (_, node) => pure node.getType - | none => pure { val := HighType.Unknown, source := fieldName.source : HighTypeMd } - let tTy ← targetTy - checkSubtype source tTy valueTy + -- Compute the target's declared type, regardless of whether it's a Local, + -- a Field, or a fresh Declare. + let targetType (t : VariableMd) : ResolveM HighTypeMd := do + let s ← get + match t.val with + | .Local ref => + match s.scope.get? ref.text with + | some (_, node) => pure node.getType + | none => pure { val := .Unknown, source := ref.source } + | .Declare param => pure param.type + | .Field _ fieldName => + match s.scope.get? fieldName.text with + | some (_, node) => pure node.getType + | none => pure { val := .Unknown, source := fieldName.source } + -- Skip all checks when the RHS is a statement (TVoid) — no value to assign. + if valueTy.val != HighType.TVoid then + let targetTys ← targets'.mapM targetType + -- Build the expected type from the targets' declared types: a single + -- type when there's one target, a tuple (MultiValuedExpr) otherwise. + -- This matches the shape of `valueTy`, which is itself MultiValuedExpr + -- exactly when the RHS produces multiple values. A single tuple-vs-tuple + -- check then covers both arity and per-position type mismatches in one + -- diagnostic. + let expectedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + checkSubtype source expectedTy valueTy pure (.Assign targets' value', valueTy) | .Var (.Field target fieldName) => let (target', _) ← synthStmtExpr target diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 0b39902de0..63fed89e9b 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -260,7 +260,7 @@ every premise and conclusion unless a rule explicitly extends it (written `Γ, x Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, Return-Void-Error, Return-Multi-Error; While - *Verification statements* — Assert, Assume -- *Assignment* — Assign-Single, Assign-Multi +- *Assignment* — Assign - *Calls* — Static-Call, Static-Call-Multi, Instance-Call - *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat - *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate @@ -481,16 +481,23 @@ target is a numeric type. ### Assignment ``` - Γ(x) = T_x Γ ⊢ e ⇒ T_e T_e <: T_x -─────────────────────────────────────────────── (Assign-Single, impl) - Γ ⊢ Assign [x] e ⇒ TVoid -``` + Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e ExpectedTy <: T_e +───────────────────────────────────────────────────────────────── (Assign, impl) + Γ ⊢ Assign targets e ⇒ TVoid + where ExpectedTy = T_1 if |targets| = 1 + = MultiValuedExpr [T_1; …; T_n] otherwise ``` - Γ ⊢ targets_i = x_i Γ(x_i) = T_i Γ ⊢ e ⇒ MultiValuedExpr Us |Ts| = |Us| U_i <: T_i -───────────────────────────────────────────────────────────────────────────────────────────────────────── (Assign-Multi, impl) - Γ ⊢ Assign targets e ⇒ TVoid -``` + +The target's declared type `T_i` comes from the variable's scope entry (for +{name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) +or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. Both +single- and multi-target forms collapse into one tuple-vs-tuple check: when the RHS is a +{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`, both arity and per-position +type mismatches surface in a single diagnostic of shape *"expected '(int, int, int)', got +'(int, string)'"*. When the RHS is {name Strata.Laurel.HighType.TVoid}`TVoid` (a +side-effecting statement: `while`, `return`, …), all checks are skipped — there's no value +to assign. ### Calls From 18eb6c97186e9e9ef64db14e64caddc8328a3976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:15:49 -0400 Subject: [PATCH 091/128] fix error messages to match current type mismatch reporting --- .../Fundamentals/T22_ArityMismatch.lean | 2 +- .../Laurel/ResolutionTypeCheckTests.lean | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean index 94c0f22371..dea2d510fb 100644 --- a/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean +++ b/StrataTest/Languages/Laurel/Examples/Fundamentals/T22_ArityMismatch.lean @@ -39,7 +39,7 @@ procedure mismatch() { var x: int; assign x := twoReturns() -//^^^^^^^^^^^^^^^^^^^^^^^^ error: Assignment target count mismatch +//^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'int', got '(int, int)' }; " diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 3a9fa8f174..85318ad7e9 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -39,7 +39,7 @@ private def processResolution (input : Lean.Parser.InputContext) : IO (Array Dia def ifCondNotBool := r" function foo(x: int): int { if x then 1 else 0 -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -50,7 +50,7 @@ def assertCondNotBool := r" procedure baz() opaque { var x: int := 42; assert x -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -61,7 +61,7 @@ def assumeCondNotBool := r" procedure qux() opaque { var x: int := 42; assume x -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -72,7 +72,7 @@ def whileCondNotBool := r" procedure wh() opaque { var x: int := 1; while (x) { } -// ^ error: expected bool, but got 'int' +// ^ error: expected 'bool', got 'int' }; " @@ -84,7 +84,7 @@ procedure wh() opaque { def logicalAndNotBool := r" function foo(x: int, y: bool): bool { x && y -//^^^^^^ error: expected bool, but got 'int' +//^^^^^^ error: expected 'bool', got 'int' }; " @@ -95,8 +95,8 @@ function foo(x: int, y: bool): bool { def comparisonNotNumeric := r" function cmp(x: string, y: int): bool { +// ^^^^^^ error: '<' expected a numeric type, got 'string' x < y -//^^^^^ error: expected a numeric type, but got 'string' }; " @@ -108,7 +108,7 @@ function cmp(x: string, y: int): bool { def assignTypeMismatch := r" procedure foo() opaque { var x: int := true -//^^^^^^^^^^^^^^^^^^ error: expected 'int', but got 'bool' +//^^^^^^^^^^^^^^^^^^ error: expected 'int', got 'bool' }; " @@ -119,7 +119,7 @@ procedure foo() opaque { def returnTypeMismatch := r" function foo(): int { -// ^^^ error: expected 'int', but got 'bool' +// ^^^ error: expected 'int', got 'bool' true }; " @@ -133,7 +133,7 @@ def callArgTypeMismatch := r" function bar(x: int): int { x }; function foo(): int { bar(true) -//^^^^^^^^^ error: expected 'int', but got 'bool' +//^^^^^^^^^ error: expected 'int', got 'bool' }; " @@ -169,30 +169,30 @@ def assignTargetCountMismatch := r" procedure multi() returns (a: int, b: int) opaque; procedure test() opaque { var x: int := multi() -//^^^^^^^^^^^^^^^^^^^^^ error: Assignment target count mismatch:1 targets but right-hand side produces 2 values +//^^^^^^^^^^^^^^^^^^^^^ error: expected 'int', got '(int, int)' }; " #guard_msgs (error, drop all) in #eval testInputWithOffset "AssignTargetCountMismatch" assignTargetCountMismatch 156 processResolution -/-! ## UserDefined type pass-through (known limitation) +/-! ## UserDefined cross-type assignment (now rejected) -UserDefined types skip strict assignability checks because subtype/inheritance -relationships are not tracked during resolution. This test documents that -cross-type assignments are silently accepted today. When hierarchy tracking -lands, this test should be updated to expect a rejection. -/ +Cross-type assignments between unrelated user-defined types are rejected +because `isSubtype` is currently structural equality. Once `isSubtype` walks +`extending` chains, this test will need a related-types example to keep +exercising the success path. -/ -def userDefinedPassThrough := r" +def userDefinedCrossType := r" composite Dog { } composite Cat { } procedure test() opaque { var x: Dog := new Cat +//^^^^^^^^^^^^^^^^^^^^^ error: expected 'Dog', got 'Cat' }; " --- This should produce NO diagnostics (UserDefined types are not checked against each other) #guard_msgs (error, drop all) in -#eval testInputWithOffset "UserDefinedPassThrough" userDefinedPassThrough 170 processResolution +#eval testInputWithOffset "UserDefinedCrossType" userDefinedCrossType 170 processResolution end Laurel From 151efeb19dad0ae3d6edae58fe89b8c42295b8cf Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:29:32 +0000 Subject: [PATCH 092/128] Add multi-output procedure in expression position check and test - Add checkSingleValued helper that detects MultiValuedExpr types used in expression position (e.g., as operands to PrimitiveOp) - Emit error: "Multi-output procedure '' used in expression position" - Add ResolutionTypeTests.lean with test for assert multi(1) == 1 --- Strata/Languages/Laurel/Resolution.lean | 17 +++++++ .../Languages/Laurel/ResolutionTypeTests.lean | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 StrataTest/Languages/Laurel/ResolutionTypeTests.lean diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index d87f97cd73..367259b9ac 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -399,6 +399,20 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } +/-- Check that an expression is single-valued (not a multi-output procedure call). + Emits an error if the expression has MultiValuedExpr type. -/ +private def checkSingleValued (expr : StmtExprMd) (ty : HighTypeMd) : ResolveM Unit := do + match ty.val with + | .MultiValuedExpr _ => + let calleeName := match expr.val with + | .StaticCall callee _ => callee.text + | .InstanceCall _ callee _ => callee.text + | _ => "expression" + let diag := diagnosticFromSource expr.source + s!"Multi-output procedure '{calleeName}' used in expression position" + modify fun s => { s with errors := s.errors.push diag } + | _ => pure () + /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -543,6 +557,9 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let results ← args.mapM resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) + -- Check that no argument is a multi-output procedure call + for (arg, argTy) in results do + checkSingleValued arg argTy let resultTy := match op with | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies | .Lt | .Leq | .Gt | .Geq => HighType.TBool diff --git a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean new file mode 100644 index 0000000000..b3d10b55f0 --- /dev/null +++ b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean @@ -0,0 +1,50 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +/- +Tests that the resolution pass detects type checking errors — e.g. using a +multi-output procedure in expression position. +-/ + +import StrataTest.Util.TestDiagnostics +import Strata.DDM.Elab +import Strata.DDM.BuiltinDialects.Init +import Strata.Languages.Laurel.Grammar.LaurelGrammar +import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator +import Strata.Languages.Laurel.Resolution + +open StrataTest.Util +open Strata +open Strata.Elab (parseStrataProgramFromDialect) + +namespace Strata.Laurel + +/-- Run only parsing + resolution and return diagnostics (no SMT verification). -/ +private def processResolution (input : Lean.Parser.InputContext) : IO (Array Diagnostic) := do + let dialects := Strata.Elab.LoadedDialects.ofDialects! #[initDialect, Laurel] + let strataProgram ← parseStrataProgramFromDialect dialects Laurel.name input + let uri := Strata.Uri.file input.fileName + match Laurel.TransM.run uri (Laurel.parseProgram strataProgram) with + | .error e => throw (IO.userError s!"Translation errors: {e}") + | .ok program => + let result := resolve program + let files := Map.insert Map.empty uri input.fileMap + return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray + +/-! ## Multi-output procedure used in expression position -/ + +def multiOutputInExpr := r" +procedure multi(x: int) returns (a: int, b: int) opaque; +procedure test() opaque { + assert multi(1) == 1 +// ^^^^^^^^ error: Multi-output procedure 'multi' used in expression position +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "MultiOutputInExpr" multiOutputInExpr 42 processResolution + +end Laurel From d7e29032cb6f49a8e251b537bc0e5afd9e8dd8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:17:26 -0400 Subject: [PATCH 093/128] fix error reporting location --- Strata/Languages/Laurel/Resolution.lean | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 622b35c302..677ba564d2 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -645,8 +645,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee - for (argTy, paramTy) in argTypes.zip paramTypes do - checkSubtype source paramTy argTy + for ((a, aTy), paramTy) in (args'.zip argTypes).zip paramTypes do + checkSubtype a.source paramTy aTy pure (.StaticCall callee' args', retTy) | .PrimitiveOp op args => let results ← args.mapM synthStmtExpr @@ -662,12 +662,12 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .StrConcat => HighType.TString match op with | .And | .Or | .AndThen | .OrElse | .Not | .Implies => - for aTy in argTypes do - checkSubtype source { val := .TBool, source := aTy.source } aTy + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TBool, source := a.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => - for aTy in argTypes do + for (a, aTy) in args'.zip argTypes do unless isNumeric aTy do - typeMismatch aTy.source (some expr) "expected a numeric type" aTy + typeMismatch a.source (some expr) "expected a numeric type" aTy | .Eq | .Neq => match argTypes with | [lhsTy, rhsTy] => @@ -677,8 +677,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := modify fun s => { s with errors := s.errors.push diag } | _ => pure () | .StrConcat => - for aTy in argTypes do - checkSubtype source { val := .TString, source := aTy.source } aTy + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TString, source := a.source } aTy pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source @@ -709,9 +709,9 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (lhs', lhsTy) ← synthStmtExpr lhs let (rhs', rhsTy) ← synthStmtExpr rhs unless isReference lhsTy do - typeMismatch lhsTy.source (some expr) "expected a reference type" lhsTy + typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy unless isReference rhsTy do - typeMismatch rhsTy.source (some expr) "expected a reference type" rhsTy + typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => let (target', _) ← synthStmtExpr target @@ -731,8 +731,8 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := let (retTy, paramTypes) ← getCallInfo callee -- Skip first param (self) when matching args. let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] - for (argTy, paramTy) in argTypes.zip callParamTypes do - checkSubtype source paramTy argTy + for ((a, aTy), paramTy) in (args'.zip argTypes).zip callParamTypes do + checkSubtype a.source paramTy aTy pure (.InstanceCall target' callee' args', retTy) | .Quantifier mode param trigger body => withScope do @@ -751,7 +751,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .Fresh val => let (val', valTy) ← synthStmtExpr val unless isReference valTy do - typeMismatch valTy.source (some expr) "expected a reference type" valTy + typeMismatch val'.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } From 138993cb20fc51774bac408a2866e9437ebc7f5d Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Tue, 5 May 2026 19:48:41 +0000 Subject: [PATCH 094/128] Remove checkSingleValued; let type checks report multi-output errors naturally Instead of a dedicated 'Multi-output procedure used in expression position' error, multi-output calls in expression position now produce standard type mismatch errors like 'expected int, but got (int, int)'. - Remove checkSingleValued function and its call in PrimitiveOp - Remove MultiValuedExpr skip in checkAssignable - Add Eq/Neq operand compatibility check - Add formatType helper for nice MultiValuedExpr formatting - Skip assignment type check when arity already mismatches --- Strata/Languages/Laurel/Resolution.lean | 42 +++++++++---------- .../Languages/Laurel/ResolutionTypeTests.lean | 2 +- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 367259b9ac..cbedf8f53c 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -360,9 +360,17 @@ def resolveHighType (ty : HighTypeMd) : ResolveM HighTypeMd := do | other => pure other return { val := val', source := ty.source } +/-- Format a type for use in diagnostics. -/ +private def formatType (ty : HighTypeMd) : String := + match ty.val with + | .MultiValuedExpr tys => + let parts := tys.map (fun t => toString (formatHighTypeVal t.val)) + "(" ++ ", ".intercalate parts ++ ")" + | other => toString (formatHighTypeVal other) + /-- Emit a type mismatch diagnostic. -/ private def typeMismatch (source : Option FileRange) (expected : String) (actual : HighTypeMd) : ResolveM Unit := do - let actualStr := toString (formatHighTypeVal actual.val) + let actualStr := formatType actual let diag := diagnosticFromSource source s!"Type mismatch: expected {expected}, but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } @@ -387,32 +395,17 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) match expected.val, actual.val with | .Unknown, _ => pure () | _, .Unknown => pure () - | _, .MultiValuedExpr _ => pure () -- arity mismatch already reported separately | .UserDefined _, _ => pure () -- subtype relationships not tracked here | _, .UserDefined _ => pure () -- subtype relationships not tracked here | .TCore _, _ => pure () -- pass-through Core types not checked during resolution | _, .TCore _ => pure () -- pass-through Core types not checked during resolution | _, _ => if !highEq expected actual then - let expectedStr := toString (formatHighTypeVal expected.val) - let actualStr := toString (formatHighTypeVal actual.val) + let expectedStr := formatType expected + let actualStr := formatType actual let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } -/-- Check that an expression is single-valued (not a multi-output procedure call). - Emits an error if the expression has MultiValuedExpr type. -/ -private def checkSingleValued (expr : StmtExprMd) (ty : HighTypeMd) : ResolveM Unit := do - match ty.val with - | .MultiValuedExpr _ => - let calleeName := match expr.val with - | .StaticCall callee _ => callee.text - | .InstanceCall _ callee _ => callee.text - | _ => "expression" - let diag := diagnosticFromSource expr.source - s!"Multi-output procedure '{calleeName}' used in expression position" - modify fun s => { s with errors := s.errors.push diag } - | _ => pure () - /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -515,7 +508,8 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) modify fun s => { s with errors := s.errors.push diag } -- Type check: for single-target assignments, check value type matches target type -- Skip when value type is void (RHS is a statement like while/return that doesn't produce a value) - if targets'.length == 1 && valueTy.val != HighType.TVoid then + -- Skip when there's an arity mismatch (already reported above) + if targets'.length == 1 && targets'.length == expectedOutputCount && valueTy.val != HighType.TVoid then if let some target := targets'.head? then let targetTy := match target.val with | .Local ref => do @@ -557,9 +551,6 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) let results ← args.mapM resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) - -- Check that no argument is a multi-output procedure call - for (arg, argTy) in results do - checkSingleValued arg argTy let resultTy := match op with | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies | .Lt | .Leq | .Gt | .Geq => HighType.TBool @@ -574,7 +565,12 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) for aTy in argTypes do checkBool source aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => for aTy in argTypes do checkNumeric source aTy - | .Eq | .Neq | .StrConcat => pure () + | .Eq | .Neq => + -- Check that operands are compatible with each other + match argTypes with + | [lhsTy, rhsTy] => checkAssignable source rhsTy lhsTy + | _ => pure () + | .StrConcat => pure () pure (.PrimitiveOp op args', { val := resultTy, source := source }) | .New ref => let ref' ← resolveRef ref source diff --git a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean index b3d10b55f0..89ac1a162c 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean @@ -40,7 +40,7 @@ def multiOutputInExpr := r" procedure multi(x: int) returns (a: int, b: int) opaque; procedure test() opaque { assert multi(1) == 1 -// ^^^^^^^^ error: Multi-output procedure 'multi' used in expression position +// ^^^^^^^^^^^^^ error: expected 'int', but got '(int, int)' }; " From 4a4bbc6bbf4f3741942272ec588b64d3fd1b2685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Fri, 15 May 2026 18:22:07 -0400 Subject: [PATCH 095/128] fix location error reporting --- StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 85318ad7e9..112fa7eba9 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -84,7 +84,7 @@ procedure wh() opaque { def logicalAndNotBool := r" function foo(x: int, y: bool): bool { x && y -//^^^^^^ error: expected 'bool', got 'int' +//^ error: expected 'bool', got 'int' }; " @@ -95,8 +95,8 @@ function foo(x: int, y: bool): bool { def comparisonNotNumeric := r" function cmp(x: string, y: int): bool { -// ^^^^^^ error: '<' expected a numeric type, got 'string' x < y +//^ error: '<' expected a numeric type, got 'string' }; " @@ -133,7 +133,7 @@ def callArgTypeMismatch := r" function bar(x: int): int { x }; function foo(): int { bar(true) -//^^^^^^^^^ error: expected 'int', got 'bool' +// ^^^^ error: expected 'int', got 'bool' }; " From d9caa3732e44b897f70a168d904115081918ce72 Mon Sep 17 00:00:00 2001 From: keyboardDrummer-bot Date: Wed, 6 May 2026 17:07:52 +0000 Subject: [PATCH 096/128] Address review feedback: symmetric Eq/Neq errors, extract helper, consolidate tests - Add checkComparable helper for symmetric Eq/Neq error messages ("Operands of '==' have incompatible types 'X' and 'Y'") - Extract resolveStmtExprExpr helper to reduce repeated pattern - Add constant initializer type check in resolveConstant - Merge ResolutionTypeTests.lean into ResolutionTypeCheckTests.lean - Add tests: equality type mismatch, assignment target count mismatch, UserDefined pass-through (documents known limitation) - Update checkAssignable doc comment to mention TCore types --- Strata/Languages/Laurel/Resolution.lean | 54 ++++++--- .../Laurel/ResolutionTypeCheckTests.lean | 107 +++++++++++++----- .../Languages/Laurel/ResolutionTypeTests.lean | 50 -------- 3 files changed, 118 insertions(+), 93 deletions(-) delete mode 100644 StrataTest/Languages/Laurel/ResolutionTypeTests.lean diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index cbedf8f53c..4bfa2d39dc 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -390,7 +390,8 @@ private def checkNumeric (source : Option FileRange) (ty : HighTypeMd) : Resolve /-- Check that two types are compatible, emitting a diagnostic if not. UserDefined types are always considered compatible with each other since - subtype relationships (inheritance) are not tracked during resolution. -/ + subtype relationships (inheritance) are not tracked during resolution. + TCore types are not checked since they are pass-through types from the Core language. -/ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do match expected.val, actual.val with | .Unknown, _ => pure () @@ -406,6 +407,22 @@ private def checkAssignable (source : Option FileRange) (expected : HighTypeMd) let diag := diagnosticFromSource source s!"Type mismatch: expected '{expectedStr}', but got '{actualStr}'" modify fun s => { s with errors := s.errors.push diag } +/-- Check that two types are comparable (for == and !=), emitting a symmetric diagnostic if not. -/ +private def checkComparable (source : Option FileRange) (lhsTy : HighTypeMd) (rhsTy : HighTypeMd) : ResolveM Unit := do + match lhsTy.val, rhsTy.val with + | .Unknown, _ => pure () + | _, .Unknown => pure () + | .UserDefined _, _ => pure () + | _, .UserDefined _ => pure () + | .TCore _, _ => pure () + | _, .TCore _ => pure () + | _, _ => + if !highEq lhsTy rhsTy then + let lhsStr := formatType lhsTy + let rhsStr := formatType rhsTy + let diag := diagnosticFromSource source s!"Operands of '==' have incompatible types '{lhsStr}' and '{rhsStr}'" + modify fun s => { s with errors := s.errors.push diag } + /-- Get the type of a resolved variable reference from scope. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get @@ -566,9 +583,9 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => for aTy in argTypes do checkNumeric source aTy | .Eq | .Neq => - -- Check that operands are compatible with each other + -- Check that operands are compatible with each other (symmetric check) match argTypes with - | [lhsTy, rhsTy] => checkAssignable source rhsTy lhsTy + | [lhsTy, rhsTy] => checkComparable source lhsTy rhsTy | _ => pure () | .StrConcat => pure () pure (.PrimitiveOp op args', { val := resultTy, source := source }) @@ -653,6 +670,11 @@ def resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) termination_by exprMd decreasing_by all_goals term_by_mem +/-- Resolve a statement expression, discarding the synthesized type. + Use when only the resolved expression is needed (invariants, decreases, etc.). -/ +private def resolveStmtExprExpr (e : StmtExprMd) : ResolveM StmtExprMd := do + let (e', _) ← resolveStmtExpr e; pure e' + /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do let ty' ← resolveHighType param.type @@ -666,12 +688,12 @@ def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do let (b', ty) ← resolveStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => - let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') - let impl' ← impl.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' - let mods' ← mods.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) + let impl' ← impl.mapM resolveStmtExprExpr + let mods' ← mods.mapM resolveStmtExprExpr return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) | .Abstract posts => - let posts' ← posts.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') + let posts' ← posts.mapM (·.mapM resolveStmtExprExpr) return (.Abstract posts', { val := .TVoid, source := none }) | .External => return (.External, { val := .TVoid, source := none }) @@ -681,8 +703,8 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do withScope do let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') - let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) + let dec' ← proc.decreases.mapM resolveStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -696,7 +718,7 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, preconditions := pres', decreases := dec', @@ -722,8 +744,8 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv modify fun s => { s with instanceTypeName := some typeName.text } let inputs' ← proc.inputs.mapM resolveParameter let outputs' ← proc.outputs.mapM resolveParameter - let pres' ← proc.preconditions.mapM (·.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e') - let dec' ← proc.decreases.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let pres' ← proc.preconditions.mapM (·.mapM resolveStmtExprExpr) + let dec' ← proc.decreases.mapM resolveStmtExprExpr let (body', bodyTy) ← resolveBody proc.body if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source @@ -736,7 +758,7 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv if bodyTy.val != HighType.TVoid then checkAssignable proc.name.source singleOutput.type bodyTy | _ => pure () - let invokeOn' ← proc.invokeOn.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let invokeOn' ← proc.invokeOn.mapM resolveStmtExprExpr modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, @@ -805,7 +827,11 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM fun e => do let (e', _) ← resolveStmtExpr e; pure e' + let init' ← c.initializer.mapM fun e => do + let (e', eTy) ← resolveStmtExpr e + if eTy.val != HighType.TVoid then + checkAssignable e'.source ty' eTy + pure e' let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 01ccd40708..3a9fa8f174 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -34,7 +34,7 @@ private def processResolution (input : Lean.Parser.InputContext) : IO (Array Dia let files := Map.insert Map.empty uri input.fileMap return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray -/-! ## Non-boolean condition in if-then-else -/ +/-! ## Non-boolean conditions -/ def ifCondNotBool := r" function foo(x: int): int { @@ -44,9 +44,7 @@ function foo(x: int): int { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "IfCondNotBool" ifCondNotBool 39 processResolution - -/-! ## Non-boolean condition in assert -/ +#eval testInputWithOffset "IfCondNotBool" ifCondNotBool 44 processResolution def assertCondNotBool := r" procedure baz() opaque { @@ -57,9 +55,7 @@ procedure baz() opaque { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "AssertCondNotBool" assertCondNotBool 49 processResolution - -/-! ## Non-boolean condition in assume -/ +#eval testInputWithOffset "AssertCondNotBool" assertCondNotBool 54 processResolution def assumeCondNotBool := r" procedure qux() opaque { @@ -70,9 +66,20 @@ procedure qux() opaque { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "AssumeCondNotBool" assumeCondNotBool 59 processResolution +#eval testInputWithOffset "AssumeCondNotBool" assumeCondNotBool 64 processResolution + +def whileCondNotBool := r" +procedure wh() opaque { + var x: int := 1; + while (x) { } +// ^ error: expected bool, but got 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "WhileCondNotBool" whileCondNotBool 74 processResolution -/-! ## Non-boolean operand in logical and -/ +/-! ## Logical operator type checks -/ def logicalAndNotBool := r" function foo(x: int, y: bool): bool { @@ -82,9 +89,21 @@ function foo(x: int, y: bool): bool { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "LogicalAndNotBool" logicalAndNotBool 69 processResolution +#eval testInputWithOffset "LogicalAndNotBool" logicalAndNotBool 84 processResolution + +/-! ## Numeric operator type checks -/ + +def comparisonNotNumeric := r" +function cmp(x: string, y: int): bool { + x < y +//^^^^^ error: expected a numeric type, but got 'string' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "ComparisonNotNumeric" comparisonNotNumeric 94 processResolution -/-! ## Assignment type mismatch -/ +/-! ## Assignment type checks -/ def assignTypeMismatch := r" procedure foo() opaque { @@ -94,9 +113,9 @@ procedure foo() opaque { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "AssignTypeMismatch" assignTypeMismatch 79 processResolution +#eval testInputWithOffset "AssignTypeMismatch" assignTypeMismatch 104 processResolution -/-! ## Function return type mismatch -/ +/-! ## Function return type checks -/ def returnTypeMismatch := r" function foo(): int { @@ -106,9 +125,9 @@ function foo(): int { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "ReturnTypeMismatch" returnTypeMismatch 89 processResolution +#eval testInputWithOffset "ReturnTypeMismatch" returnTypeMismatch 114 processResolution -/-! ## Static call argument type mismatch -/ +/-! ## Call argument type checks -/ def callArgTypeMismatch := r" function bar(x: int): int { x }; @@ -119,31 +138,61 @@ function foo(): int { " #guard_msgs (error, drop all) in -#eval testInputWithOffset "CallArgTypeMismatch" callArgTypeMismatch 99 processResolution +#eval testInputWithOffset "CallArgTypeMismatch" callArgTypeMismatch 124 processResolution -/-! ## Non-boolean condition in while loop -/ +/-! ## Equality operator type checks -/ -def whileCondNotBool := r" -procedure wh() opaque { - var x: int := 1; - while (x) { } -// ^ error: expected bool, but got 'int' +def equalityTypeMismatch := r" +function cmp(x: int, y: string): bool { + x == y +//^^^^^^ error: Operands of '==' have incompatible types 'int' and 'string' }; " #guard_msgs (error, drop all) in -#eval testInputWithOffset "WhileCondNotBool" whileCondNotBool 109 processResolution +#eval testInputWithOffset "EqualityTypeMismatch" equalityTypeMismatch 134 processResolution -/-! ## Non-numeric operand in comparison -/ +/-! ## Multi-output procedures -/ -def comparisonNotNumeric := r" -function cmp(x: string, y: int): bool { - x < y -//^^^^^ error: expected a numeric type, but got 'string' +def multiOutputInExpr := r" +procedure multi(x: int) returns (a: int, b: int) opaque; +procedure test() opaque { + assert multi(1) == 1 +// ^^^^^^^^^^^^^ error: Operands of '==' have incompatible types '(int, int)' and 'int' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "MultiOutputInExpr" multiOutputInExpr 146 processResolution + +def assignTargetCountMismatch := r" +procedure multi() returns (a: int, b: int) opaque; +procedure test() opaque { + var x: int := multi() +//^^^^^^^^^^^^^^^^^^^^^ error: Assignment target count mismatch:1 targets but right-hand side produces 2 values +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "AssignTargetCountMismatch" assignTargetCountMismatch 156 processResolution + +/-! ## UserDefined type pass-through (known limitation) + +UserDefined types skip strict assignability checks because subtype/inheritance +relationships are not tracked during resolution. This test documents that +cross-type assignments are silently accepted today. When hierarchy tracking +lands, this test should be updated to expect a rejection. -/ + +def userDefinedPassThrough := r" +composite Dog { } +composite Cat { } +procedure test() opaque { + var x: Dog := new Cat }; " +-- This should produce NO diagnostics (UserDefined types are not checked against each other) #guard_msgs (error, drop all) in -#eval testInputWithOffset "ComparisonNotNumeric" comparisonNotNumeric 121 processResolution +#eval testInputWithOffset "UserDefinedPassThrough" userDefinedPassThrough 170 processResolution end Laurel diff --git a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeTests.lean deleted file mode 100644 index 89ac1a162c..0000000000 --- a/StrataTest/Languages/Laurel/ResolutionTypeTests.lean +++ /dev/null @@ -1,50 +0,0 @@ -/- - Copyright Strata Contributors - - SPDX-License-Identifier: Apache-2.0 OR MIT --/ - -/- -Tests that the resolution pass detects type checking errors — e.g. using a -multi-output procedure in expression position. --/ - -import StrataTest.Util.TestDiagnostics -import Strata.DDM.Elab -import Strata.DDM.BuiltinDialects.Init -import Strata.Languages.Laurel.Grammar.LaurelGrammar -import Strata.Languages.Laurel.Grammar.ConcreteToAbstractTreeTranslator -import Strata.Languages.Laurel.Resolution - -open StrataTest.Util -open Strata -open Strata.Elab (parseStrataProgramFromDialect) - -namespace Strata.Laurel - -/-- Run only parsing + resolution and return diagnostics (no SMT verification). -/ -private def processResolution (input : Lean.Parser.InputContext) : IO (Array Diagnostic) := do - let dialects := Strata.Elab.LoadedDialects.ofDialects! #[initDialect, Laurel] - let strataProgram ← parseStrataProgramFromDialect dialects Laurel.name input - let uri := Strata.Uri.file input.fileName - match Laurel.TransM.run uri (Laurel.parseProgram strataProgram) with - | .error e => throw (IO.userError s!"Translation errors: {e}") - | .ok program => - let result := resolve program - let files := Map.insert Map.empty uri input.fileMap - return result.errors.toList.map (fun dm => dm.toDiagnostic files) |>.toArray - -/-! ## Multi-output procedure used in expression position -/ - -def multiOutputInExpr := r" -procedure multi(x: int) returns (a: int, b: int) opaque; -procedure test() opaque { - assert multi(1) == 1 -// ^^^^^^^^^^^^^ error: expected 'int', but got '(int, int)' -}; -" - -#guard_msgs (error, drop all) in -#eval testInputWithOffset "MultiOutputInExpr" multiOutputInExpr 42 processResolution - -end Laurel From f2fea0a8b1170fefc0609c776a12f9ae16a1218e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 16:47:06 -0400 Subject: [PATCH 097/128] thread typing context through type resolution/inheritance --- Strata/Languages/Laurel/InferHoleTypes.lean | 10 ++- Strata/Languages/Laurel/Laurel.lean | 85 ++++++++++++++++++--- Strata/Languages/Laurel/Resolution.lean | 35 ++++++--- 3 files changed, 106 insertions(+), 24 deletions(-) diff --git a/Strata/Languages/Laurel/InferHoleTypes.lean b/Strata/Languages/Laurel/InferHoleTypes.lean index 026a82e5b9..248d90716d 100644 --- a/Strata/Languages/Laurel/InferHoleTypes.lean +++ b/Strata/Languages/Laurel/InferHoleTypes.lean @@ -51,6 +51,8 @@ inductive InferHoleTypesStats where structure InferHoleState where model : SemanticModel + /-- Type-relation tables used by the consistency check on pre-annotated holes. -/ + typeContext : TypeContext currentOutputType : HighTypeMd statistics : Statistics := {} diagnostics : List DiagnosticModel := [] @@ -100,7 +102,8 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol -- types disagree under consistency (gradual ~). match existingTy with | some prior => - unless isConsistent prior expectedType do + let ctx := (← get).typeContext + unless isConsistent ctx prior expectedType do modify fun s => { s with diagnostics := s.diagnostics ++ [diagnosticFromSource source s!"hole annotated with '{formatHighTypeVal prior.val}' but context expects '{formatHighTypeVal expectedType.val}'"] @@ -183,7 +186,10 @@ private def inferProcedure (proc : Procedure) : InferHoleM Procedure := do Annotate every `.Hole` in the program with a type inferred from context. -/ def inferHoleTypes (model : SemanticModel) (program : Program) : Program × List DiagnosticModel × Statistics := - let initState : InferHoleState := { model := model, currentOutputType := { val := .Unknown, source := none }} + let initState : InferHoleState := { + model := model, + typeContext := TypeContext.ofTypes program.types, + currentOutputType := { val := .Unknown, source := none } } let (procs, finalState) := (program.staticProcedures.mapM inferProcedure).run initState ({ program with staticProcedures := procs }, finalState.diagnostics, finalState.statistics) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index a8c665d64c..5b6a7ee252 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -489,24 +489,76 @@ instance : BEq HighTypeMd where deriving instance BEq for HighType -/-- Subtyping. Stub: structural equality via `highEq`. - TODO: walk `extending` chains for composites, unfold aliases, unwrap - constrained types to their base. -/ -def isSubtype (sub sup : HighTypeMd) : Bool := highEq sub sup +/-- Lookup tables threaded through subtyping/consistency checks. Built from + the program's `TypeDefinition`s by the resolution pass: + - `unfoldMap` maps an alias or constrained type's name to the type it + unwraps to (alias target / constrained base). Followed transitively to + reach a non-alias, non-constrained type. + - `extendingMap` maps a composite type's name to the *direct* parents in + its `extending` list. Walked transitively for the subtype check. -/ +structure TypeContext where + unfoldMap : Std.HashMap String HighTypeMd := {} + extendingMap : Std.HashMap String (List String) := {} + deriving Inhabited + +/-- Unfold aliases and constrained types to their underlying type. + Composites and primitives are returned unchanged. A `visited` set guards + against cycles in the alias/constrained graph (already cycle-checked + elsewhere, but keeps `unfold` safe to call independently). -/ +partial def TypeContext.unfold (ctx : TypeContext) (ty : HighTypeMd) + (visited : Std.HashSet String := {}) : HighTypeMd := + match ty.val with + | .UserDefined name => + if visited.contains name.text then ty + else match ctx.unfoldMap.get? name.text with + | some target => ctx.unfold target (visited.insert name.text) + | none => ty + | _ => ty + +/-- All ancestors of a composite type (including itself), reachable via + repeated `extending` lookups. The `fuel` cap is the number of distinct + type names ever registered, bounding the BFS even with malformed input. -/ +partial def TypeContext.ancestors (ctx : TypeContext) (name : String) : Std.HashSet String := + let rec go (acc : Std.HashSet String) (frontier : List String) : Std.HashSet String := + match frontier with + | [] => acc + | n :: rest => + if acc.contains n then go acc rest + else + let acc' := acc.insert n + let parents := (ctx.extendingMap.get? n).getD [] + go acc' (parents ++ rest) + go {} [name] + +/-- Subtyping. Walks `extending` chains for composites, unfolds aliases, and + unwraps constrained types to their base before falling back to structural + equality via `highEq`. -/ +def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := + let sub' := ctx.unfold sub + let sup' := ctx.unfold sup + match sub'.val, sup'.val with + | .UserDefined subName, .UserDefined supName => + -- After unfolding, both sides are composites (or unresolved). A composite + -- is a subtype of any type in its extending chain. + (ctx.ancestors subName.text).contains supName.text || highEq sub' sup' + | _, _ => highEq sub' sup' /-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the dynamic type and is consistent with everything; otherwise structural - equality. `TCore` is a temporary migration escape hatch. -/ -def isConsistent (a b : HighTypeMd) : Bool := - match a.val, b.val with + equality after unfolding aliases / constrained types. `TCore` is a + temporary migration escape hatch. -/ +def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := + let a' := ctx.unfold a + let b' := ctx.unfold b + match a'.val, b'.val with | .Unknown, _ | _, .Unknown => true | .TCore _, _ | _, .TCore _ => true - | _, _ => highEq a b + | _, _ => highEq a' b' /-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice this collapses to `sub ~ sup ∨ sub <: sup`. -/ -def isConsistentSubtype (sub sup : HighTypeMd) : Bool := - isConsistent sub sup || isSubtype sub sup +def isConsistentSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := + isConsistent ctx sub sup || isSubtype ctx sub sup def HighType.isBool : HighType → Bool | TBool => true @@ -645,6 +697,19 @@ def TypeDefinition.name : TypeDefinition → Identifier | .Datatype ty => ty.name | .Alias ty => ty.name +/-- Build a `TypeContext` from a list of `TypeDefinition`s. + Aliases populate `unfoldMap` with their target; constrained types populate + it with their base; composites populate `extendingMap` with their direct + parents. Datatypes contribute nothing — they're nominal and irreducible. -/ +def TypeContext.ofTypes (types : List TypeDefinition) : TypeContext := + types.foldl (init := {}) fun ctx td => + match td with + | .Alias ta => { ctx with unfoldMap := ctx.unfoldMap.insert ta.name.text ta.target } + | .Constrained ct => { ctx with unfoldMap := ctx.unfoldMap.insert ct.name.text ct.base } + | .Composite c => + { ctx with extendingMap := ctx.extendingMap.insert c.name.text (c.extending.map (·.text)) } + | .Datatype _ => ctx + structure Constant where name : Identifier type : HighTypeMd diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index cb9ab36b00..0efbe7060b 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -291,6 +291,10 @@ structure ResolveState where declaration order). `none` means no enclosing procedure. Used by `Return` to type-check the optional return value and to flag arity/shape mismatches. -/ expectedReturnTypes : Option (List HighTypeMd) := none + /-- Type-relation tables (alias/constrained unfolding + composite extending + chains) used by the subtyping/consistency checks. Built once from + `program.types` at the start of `resolve`. -/ + typeContext : TypeContext := {} @[expose] abbrev ResolveM := StateM ResolveState @@ -464,13 +468,16 @@ private def typeMismatch (source : Option FileRange) (construct : Option StmtExp actual type is already in hand (assignment, call args, body vs declared output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do - unless isConsistentSubtype actual expected do + let ctx := (← get).typeContext + unless isConsistentSubtype ctx actual expected do typeMismatch source none s!"expected '{formatType expected}'" actual /-- Test whether a type is in the set of numeric primitives. `Unknown` and - `TCore` are accepted as gradual escape hatches. Used by Op-Cmp / Op-Arith. -/ -private def isNumeric (ty : HighTypeMd) : Bool := - match ty.val with + `TCore` are accepted as gradual escape hatches. Aliases and constrained + types are unfolded first so e.g. `nat` (constrained over `int`) counts as + numeric. Used by Op-Cmp / Op-Arith. -/ +private def isNumeric (ctx : TypeContext) (ty : HighTypeMd) : Bool := + match (ctx.unfold ty).val with | .TInt | .TReal | .TFloat64 | .Unknown => true | .TCore _ => true | _ => false @@ -478,8 +485,8 @@ private def isNumeric (ty : HighTypeMd) : Bool := /-- Test whether a type is a user-defined reference type. `Unknown` and `TCore` are accepted as gradual escape hatches. Used by Fresh and ReferenceEquals, which only make sense on composite/datatype references. -/ -private def isReference (ty : HighTypeMd) : Bool := - match ty.val with +private def isReference (ctx : TypeContext) (ty : HighTypeMd) : Bool := + match (ctx.unfold ty).val with | .UserDefined _ | .Unknown => true | .TCore _ => true | _ => false @@ -672,13 +679,15 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := for (a, aTy) in args'.zip argTypes do checkSubtype a.source { val := .TBool, source := a.source } aTy | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => + let ctx := (← get).typeContext for (a, aTy) in args'.zip argTypes do - unless isNumeric aTy do + unless isNumeric ctx aTy do typeMismatch a.source (some expr) "expected a numeric type" aTy | .Eq | .Neq => match argTypes with | [lhsTy, rhsTy] => - unless isConsistent lhsTy rhsTy do + let ctx := (← get).typeContext + unless isConsistent ctx lhsTy rhsTy do let diag := diagnosticFromSource source s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" modify fun s => { s with errors := s.errors.push diag } @@ -715,9 +724,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .ReferenceEquals lhs rhs => let (lhs', lhsTy) ← synthStmtExpr lhs let (rhs', rhsTy) ← synthStmtExpr rhs - unless isReference lhsTy do + let ctx := (← get).typeContext + unless isReference ctx lhsTy do typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy - unless isReference rhsTy do + unless isReference ctx rhsTy do typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) | .AsType target ty => @@ -757,7 +767,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := pure (.Old val', valTy) | .Fresh val => let (val', valTy) ← synthStmtExpr val - unless isReference valTy do + unless isReference (← get).typeContext valTy do typeMismatch val'.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) | .Assert ⟨condExpr, summary⟩ => @@ -1246,7 +1256,8 @@ def resolve (program : Program) (existingModel: Option SemanticModel := none) : return { staticProcedures := staticProcs', staticFields := staticFields', types := types', constants := constants' } let nextId := existingModel.elim 1 (fun m => m.nextId) - let (program', finalState) := phase1.run { nextId := nextId } + let typeContext := TypeContext.ofTypes program.types + let (program', finalState) := phase1.run { nextId := nextId, typeContext } -- Phase 2: build refToDef from the resolved program (all definitions now have UUIDs) let refToDef := buildRefToDef program' { program := program', From f4fd6ffa90c61d4f4e5caba059492a437236605d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 11:30:16 -0400 Subject: [PATCH 098/128] fix field lookup --- Strata/Languages/Laurel/Resolution.lean | 35 +++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 677ba564d2..cb9ab36b00 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -272,6 +272,12 @@ structure ResolveState where nextId : Nat := 1 /-- Current lexical scope (name → definition ID). -/ scope : Scope := {} + /-- Map from definition uniqueId to its ResolvedNode. Populated alongside + `scope` whenever a definition is registered. Unlike `scope`, this map is + *not* saved/restored by `withScope` — uniqueIds are global. Used by + `getVarType` to look up types for references whose `text` doesn't match + a scope key (notably fields, which are scoped under qualified keys). -/ + idToNode : Std.HashMap Nat ResolvedNode := {} /-- Names defined at the current scope level (for duplicate detection). -/ currentScopeNames : Std.HashSet String := {} /-- Per-composite-type field scopes (type name → field name → scope entry). -/ @@ -315,8 +321,10 @@ def defineNameCheckDup (iden : Identifier) (node : ResolvedNode) (overrideResolu let id ← freshId pure ({ iden with uniqueId := some (id) }, id) - modify fun s => { s with scope := s.scope.insert resolutionName (uniqueId, node), - currentScopeNames := s.currentScopeNames.insert resolutionName } + modify fun s => { s with + scope := s.scope.insert resolutionName (uniqueId, node), + idToNode := s.idToNode.insert uniqueId node, + currentScopeNames := s.currentScopeNames.insert resolutionName } return name' /-- Resolve a reference: look up the name in scope and assign the definition's ID. @@ -476,12 +484,18 @@ private def isReference (ty : HighTypeMd) : Bool := | .TCore _ => true | _ => false -/-- Get the type of a resolved variable reference from scope. -/ +/-- Get the type of a resolved reference. Tries the lexical scope by name + first; if that misses (notably for fields, which are scoped under + qualified keys like "Container.intValue"), falls back to a uniqueId + lookup populated as definitions are registered. -/ private def getVarType (ref : Identifier) : ResolveM HighTypeMd := do let s ← get match s.scope.get? ref.text with | some (_, node) => pure node.getType - | none => pure { val := .Unknown, source := ref.source } + | none => + match ref.uniqueId.bind s.idToNode.get? with + | some node => pure node.getType + | none => pure { val := .Unknown, source := ref.source } /-- Get the call return type and parameter types for a callee from scope. -/ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List HighTypeMd) := do @@ -602,17 +616,10 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := -- Compute the target's declared type, regardless of whether it's a Local, -- a Field, or a fresh Declare. let targetType (t : VariableMd) : ResolveM HighTypeMd := do - let s ← get match t.val with - | .Local ref => - match s.scope.get? ref.text with - | some (_, node) => pure node.getType - | none => pure { val := .Unknown, source := ref.source } + | .Local ref => getVarType ref | .Declare param => pure param.type - | .Field _ fieldName => - match s.scope.get? fieldName.text with - | some (_, node) => pure node.getType - | none => pure { val := .Unknown, source := fieldName.source } + | .Field _ fieldName => getVarType fieldName -- Skip all checks when the RHS is a statement (TVoid) — no value to assign. if valueTy.val != HighType.TVoid then let targetTys ← targets'.mapM targetType @@ -630,7 +637,7 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | .Var (.Field target fieldName) => let (target', _) ← synthStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source - let ty ← getVarType fieldName + let ty ← getVarType fieldName' pure (.Var (.Field target' fieldName'), ty) | .PureFieldUpdate target fieldName newVal => let (target', targetTy) ← synthStmtExpr target From fb6fdd605f5fe4997175fd0b3500314c877388e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 16:48:29 -0400 Subject: [PATCH 099/128] drop info report of an expected downcast failure ; to fix this, we need to improve the testing facilities for Laurel --- .../Languages/Laurel/Examples/Objects/T5_inheritance.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean index 4db9a56da2..ba406b0ddc 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean @@ -98,5 +98,5 @@ procedure diamondInheritance() //} " -#guard_msgs in +#guard_msgs (drop info) in #eval testInputWithOffset "Inheritance" program 14 processLaurelFile From f5f57c19b4d8cff3e4b759bba9ecd5d33a03753d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Mon, 18 May 2026 11:50:19 -0400 Subject: [PATCH 100/128] fix silent fail --- .../Languages/Laurel/Examples/Objects/T5_inheritance.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean index ba406b0ddc..4db9a56da2 100644 --- a/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean +++ b/StrataTest/Languages/Laurel/Examples/Objects/T5_inheritance.lean @@ -98,5 +98,5 @@ procedure diamondInheritance() //} " -#guard_msgs (drop info) in +#guard_msgs in #eval testInputWithOffset "Inheritance" program 14 processLaurelFile From c28cd1c5acbdc49b8642083617e48dc196e124f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 10:28:05 -0400 Subject: [PATCH 101/128] fix typing doc direction --- docs/verso/LaurelDoc.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 63fed89e9b..1acbd5c02f 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -481,7 +481,7 @@ target is a numeric type. ### Assignment ``` - Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e ExpectedTy <: T_e + Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e T_e <: ExpectedTy ───────────────────────────────────────────────────────────────── (Assign, impl) Γ ⊢ Assign targets e ⇒ TVoid From cdfdda8cdecc88dadc62a7a111aa75e080922330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 10:32:50 -0400 Subject: [PATCH 102/128] fix documentation : subtyping is implemented --- docs/verso/LaurelDoc.lean | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 1acbd5c02f..52e2cda2b7 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -203,11 +203,13 @@ internal interface used by other rules. The relation `<:` (used in Sub) is built from three Lean functions: -- `isSubtype` — pure subtyping. The stub is structural equality via - {name Strata.Laurel.highEq}`highEq`. The eventual real version walks the `extending` - chain for {name Strata.Laurel.CompositeType}`CompositeType`, unfolds +- `isSubtype` — pure subtyping. Walks the `extending` chain for + {name Strata.Laurel.CompositeType}`CompositeType` (via + {name Strata.Laurel.TypeContext.ancestors}`TypeContext.ancestors`), unfolds {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps - {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base. + {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base (both via + {name Strata.Laurel.TypeContext.unfold}`TypeContext.unfold`), then falls back to + structural equality via {name Strata.Laurel.highEq}`highEq`. - `isConsistent` — the symmetric gradual relation `~` (Siek–Taha): {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type and is consistent with everything; otherwise structural equality. @@ -233,9 +235,8 @@ A previous iteration was synth-only with three *bivariantly-compatible* wildcard {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no assignment, call argument, or comparison involving a user type was ever rejected. The bidirectional design retires that carve-out — user-defined types are now a regular -participant in `<:`, and tightening `isSubtype` (to walk inheritance and unwrap -constrained types) gradually buys real checking on user-defined code without changing -callers. +participant in `<:`, with `isSubtype` walking inheritance chains and unwrapping aliases +and constrained types to deliver real checking on user-defined code. Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This includes {name Strata.Laurel.StmtExpr.Return}`Return`, From fbb1de394982c4448494961012087c582cdcbc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 13:14:35 -0400 Subject: [PATCH 103/128] remove special treatment of TCore --- Strata/Languages/Laurel/Laurel.lean | 5 ++--- Strata/Languages/Laurel/Resolution.lean | 16 +++++++--------- docs/verso/LaurelDoc.lean | 15 +++++---------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index 5b6a7ee252..c5f5dede7c 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -468,6 +468,7 @@ def highEq (a : HighTypeMd) (b : HighTypeMd) : Bool := match _a: a.val, _b: b.va | HighType.TSet t1, HighType.TSet t2 => highEq t1 t2 | HighType.TMap k1 v1, HighType.TMap k2 v2 => highEq k1 k2 && highEq v1 v2 | HighType.UserDefined r1, HighType.UserDefined r2 => r1.text == r2.text + | HighType.TCore s1, HighType.TCore s2 => s1 == s2 | HighType.Applied b1 args1, HighType.Applied b2 args2 => highEq b1 b2 && args1.length == args2.length && (args1.attach.zip args2 |>.all (fun (a1, a2) => highEq a1.1 a2)) | HighType.Pure b1, HighType.Pure b2 => highEq b1 b2 @@ -545,14 +546,12 @@ def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := /-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the dynamic type and is consistent with everything; otherwise structural - equality after unfolding aliases / constrained types. `TCore` is a - temporary migration escape hatch. -/ + equality after unfolding aliases / constrained types. -/ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := let a' := ctx.unfold a let b' := ctx.unfold b match a'.val, b'.val with | .Unknown, _ | _, .Unknown => true - | .TCore _, _ | _, .TCore _ => true | _, _ => highEq a' b' /-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 0efbe7060b..81d96adca6 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -472,23 +472,21 @@ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (ac unless isConsistentSubtype ctx actual expected do typeMismatch source none s!"expected '{formatType expected}'" actual -/-- Test whether a type is in the set of numeric primitives. `Unknown` and - `TCore` are accepted as gradual escape hatches. Aliases and constrained - types are unfolded first so e.g. `nat` (constrained over `int`) counts as - numeric. Used by Op-Cmp / Op-Arith. -/ +/-- Test whether a type is in the set of numeric primitives. `Unknown` is + accepted as a gradual escape hatch. Aliases and constrained types are + unfolded first so e.g. `nat` (constrained over `int`) counts as numeric. + Used by Op-Cmp / Op-Arith. -/ private def isNumeric (ctx : TypeContext) (ty : HighTypeMd) : Bool := match (ctx.unfold ty).val with | .TInt | .TReal | .TFloat64 | .Unknown => true - | .TCore _ => true | _ => false -/-- Test whether a type is a user-defined reference type. `Unknown` and `TCore` - are accepted as gradual escape hatches. Used by Fresh and ReferenceEquals, - which only make sense on composite/datatype references. -/ +/-- Test whether a type is a user-defined reference type. `Unknown` is accepted + as a gradual escape hatch. Used by Fresh and ReferenceEquals, which only + make sense on composite/datatype references. -/ private def isReference (ctx : TypeContext) (ty : HighTypeMd) : Bool := match (ctx.unfold ty).val with | .UserDefined _ | .Unknown => true - | .TCore _ => true | _ => false /-- Get the type of a resolved reference. Tries the lexical scope by name diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 52e2cda2b7..e73ab90f00 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -216,10 +216,6 @@ The relation `<:` (used in Sub) is built from three Lean functions: - `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this is the standard collapse of `∃R. T ~ R ∧ R <: U`. -{name Strata.Laurel.HighType.TCore}`TCore` is bivariantly consistent for now as a temporary -migration escape hatch from the Core language; the carve-out lives in `isConsistent` and is -intentionally temporary. - Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) @@ -228,10 +224,9 @@ flows freely into any typed slot, and any expression flows freely into a slot of fully-known types only. The symmetric `isConsistent` is used directly by Op-Eq, where the operand types must be mutually consistent (no subtype direction is privileged). -A previous iteration was synth-only with three *bivariantly-compatible* wildcards: -{name Strata.Laurel.HighType.Unknown}`Unknown`, -{name Strata.Laurel.HighType.UserDefined}`UserDefined`, and -{name Strata.Laurel.HighType.TCore}`TCore`. The +A previous iteration was synth-only with two *bivariantly-compatible* wildcards: +{name Strata.Laurel.HighType.Unknown}`Unknown` and +{name Strata.Laurel.HighType.UserDefined}`UserDefined`. The {name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no assignment, call argument, or comparison involving a user type was ever rejected. The bidirectional design retires that carve-out — user-defined types are now a regular @@ -600,8 +595,8 @@ passes `Numeric`); a proper fix needs numeric promotion or unification. Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool ``` -`isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined`, -{name Strata.Laurel.HighType.Unknown}`Unknown`, or {name Strata.Laurel.HighType.TCore}`TCore` +`isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` +or {name Strata.Laurel.HighType.Unknown}`Unknown` type. Reference equality is meaningless on primitives. Compatibility between `T_l` and `T_r` (e.g. rejecting `Cat === Dog` for unrelated user-defined types) is delegated to future tightening of `<:` — today, two distinct user-defined names already mismatch From a4674bdd092d2cdeb9b127e2b7556c4f99a9398c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 13:50:03 -0400 Subject: [PATCH 104/128] fix TCore documentation --- .../Languages/Laurel/ResolutionTypeCheckTests.lean | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index 112fa7eba9..b78f3b22df 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -176,12 +176,11 @@ procedure test() opaque { #guard_msgs (error, drop all) in #eval testInputWithOffset "AssignTargetCountMismatch" assignTargetCountMismatch 156 processResolution -/-! ## UserDefined cross-type assignment (now rejected) +/-! ## UserDefined cross-type assignment -Cross-type assignments between unrelated user-defined types are rejected -because `isSubtype` is currently structural equality. Once `isSubtype` walks -`extending` chains, this test will need a related-types example to keep -exercising the success path. -/ +Assignments between unrelated composites are rejected: `isSubtype` walks +`extending` chains, so two composites with no common ancestor are not +subtypes of each other. -/ def userDefinedCrossType := r" composite Dog { } From 77d32f1ff2ed797ec02d4fa937169b0db267c506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 13:50:31 -0400 Subject: [PATCH 105/128] uniform <=/=> and use latex rule presentation --- docs/verso/LaurelDoc.lean | 390 ++++++++++---------------------------- 1 file changed, 95 insertions(+), 295 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index e73ab90f00..140a250407 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -169,11 +169,7 @@ expression has a given expected type. Each construct picks a mode based on wheth is determined locally (synth) or by context (check). The two judgments are connected by a single change-of-direction rule, *subsumption*: -``` -Γ ⊢ e ⇒ A A <: B -───────────────────── (Sub) - Γ ⊢ e ⇐ B -``` +$$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` Subsumption is the *only* place the checker switches from check to synth mode. It fires as the default fallback in @@ -186,9 +182,10 @@ propagate through nested control flow. `synthStmtExpr` and `checkStmtExpr` are mutually recursive: synth rules invoke check on subexpressions whose expected type is known (e.g. `cond ⇐ TBool` in {name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), and `checkStmtExpr` falls back to -`synthStmtExpr` via Sub. Termination uses a lexicographic measure `(exprMd, tag)` where the -tag is `0` for synth and `1` for check; any descent into a strict subterm decreases via -`Prod.Lex.left`, while Sub calls synth on the *same* expression and decreases via +`synthStmtExpr` via \[⇐\] Sub. Termination uses a lexicographic measure `(exprMd, tag)` +where the tag is `0` for synth and `1` for check; any descent into a strict subterm +decreases via `Prod.Lex.left`, while \[⇐\] Sub calls synth on the *same* expression and +decreases via `Prod.Lex.right`. This is the standard well-founded encoding for bidirectional systems. There is also a thin `resolveStmtExpr` wrapper that calls `synthStmtExpr` and discards the @@ -201,7 +198,7 @@ internal interface used by other rules. ### Gradual typing -The relation `<:` (used in Sub) is built from three Lean functions: +The relation `<:` (used in \[⇐\] Sub) is built from three Lean functions: - `isSubtype` — pure subtyping. Walks the `extending` chain for {name Strata.Laurel.CompositeType}`CompositeType` (via @@ -216,13 +213,13 @@ The relation `<:` (used in Sub) is built from three Lean functions: - `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this is the standard collapse of `∃R. T ~ R ∧ R <: U`. -Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what +\[⇐\] Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what makes the system *gradual*: an expression of type {name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely into any typed slot, and any expression flows freely into a slot of type {name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between -fully-known types only. The symmetric `isConsistent` is used directly by Op-Eq, where the -operand types must be mutually consistent (no subtype direction is privileged). +fully-known types only. The symmetric `isConsistent` is used directly by \[⇒\] Op-Eq, where +the operand types must be mutually consistent (no subtype direction is privileged). A previous iteration was synth-only with two *bivariantly-compatible* wildcards: {name Strata.Laurel.HighType.Unknown}`Unknown` and @@ -245,130 +242,90 @@ includes {name Strata.Laurel.StmtExpr.Return}`Return`, Each construct is given as a derivation. `Γ` is the current lexical scope (see {name Strata.Laurel.ResolveState}`ResolveState`'s `scope`); it threads identically through every premise and conclusion unless a rule explicitly extends it (written `Γ, x : T`). -`(impl)` = implemented; `(planned)` = intended, not yet wired in. + +Each rule is tagged with `[⇒]` (synthesis) or `[⇐]` (checking) to make the +direction explicit. When a construct has both modes, the `-Synth` / `-Check` +suffix is dropped in favor of the prefix. ### Index -- *Subsumption* — Sub -- *Literals* — Lit-Int, Lit-Bool, Lit-String, Lit-Decimal -- *Variables* — Var-Local, Var-Field, Var-Declare -- *Control flow* — If-NoElse, If-Synth, If-Check, If-Check-NoElse; Block-Synth, - Block-Synth-Empty, Block-Check, Block-Check-Empty; Exit; Return-None, Return-Some, - Return-Void-Error, Return-Multi-Error; While -- *Verification statements* — Assert, Assume -- *Assignment* — Assign -- *Calls* — Static-Call, Static-Call-Multi, Instance-Call -- *Primitive operations* — Op-Bool, Op-Cmp, Op-Eq, Op-Arith, Op-Concat -- *Object forms* — New-Ok, New-Fallback; AsType; IsType; RefEq; PureFieldUpdate -- *Verification expressions* — Quantifier, Assigned, Old, Fresh, ProveBy -- *Self reference* — This-Inside, This-Outside -- *Untyped forms* — Abstract / All -- *ContractOf* — ContractOf-Bool, ContractOf-Set, ContractOf-Error -- *Holes* — Hole-Some, Hole-None-Synth, Hole-None-Check +- *Subsumption* — \[⇐\] Sub +- *Literals* — \[⇒\] Lit-Int, \[⇒\] Lit-Bool, \[⇒\] Lit-String, \[⇒\] Lit-Decimal +- *Variables* — \[⇒\] Var-Local, \[⇒\] Var-Field, \[⇒\] Var-Declare +- *Control flow* — \[⇒\] If-NoElse, \[⇒\] If, \[⇐\] If, \[⇐\] If-NoElse; + \[⇒\] Block, \[⇒\] Block-Empty, \[⇐\] Block, \[⇐\] Block-Empty; \[⇒\] Exit; + \[⇒\] Return-None, \[⇒\] Return-Some, \[⇒\] Return-Void-Error, + \[⇒\] Return-Multi-Error; \[⇒\] While +- *Verification statements* — \[⇒\] Assert, \[⇒\] Assume +- *Assignment* — \[⇒\] Assign +- *Calls* — \[⇒\] Static-Call, \[⇒\] Static-Call-Multi, \[⇒\] Instance-Call +- *Primitive operations* — \[⇒\] Op-Bool, \[⇒\] Op-Cmp, \[⇒\] Op-Eq, \[⇒\] Op-Arith, + \[⇒\] Op-Concat +- *Object forms* — \[⇒\] New-Ok, \[⇒\] New-Fallback; \[⇒\] AsType; \[⇒\] IsType; + \[⇒\] RefEq; \[⇒\] PureFieldUpdate +- *Verification expressions* — \[⇒\] Quantifier, \[⇒\] Assigned, \[⇒\] Old, + \[⇒\] Fresh, \[⇒\] ProveBy +- *Self reference* — \[⇒\] This-Inside, \[⇒\] This-Outside +- *Untyped forms* — \[⇒\] Abstract / All +- *ContractOf* — \[⇒\] ContractOf-Bool, \[⇒\] ContractOf-Set, \[⇒\] ContractOf-Error +- *Holes* — \[⇒\] Hole-Some, \[⇒\] Hole-None, \[⇐\] Hole-None ### Subsumption -``` -Γ ⊢ e ⇒ A A <: B -───────────────────── (Sub, impl) - Γ ⊢ e ⇐ B -``` +$$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` Fallback in `checkStmtExpr` whenever no bespoke check rule applies. ### Literals -``` -────────────────────────── (Lit-Int, impl) - Γ ⊢ LiteralInt n ⇒ TInt -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralInt}\;n \Rightarrow \mathsf{TInt}} \quad \text{([⇒] Lit-Int)}` -``` -─────────────────────────── (Lit-Bool, impl) - Γ ⊢ LiteralBool b ⇒ TBool -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralBool}\;b \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Lit-Bool)}` -``` -───────────────────────────────── (Lit-String, impl) - Γ ⊢ LiteralString s ⇒ TString -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralString}\;s \Rightarrow \mathsf{TString}} \quad \text{([⇒] Lit-String)}` -``` -────────────────────────────────── (Lit-Decimal, impl) - Γ ⊢ LiteralDecimal d ⇒ TReal -``` +$$`\frac{}{\Gamma \vdash \mathsf{LiteralDecimal}\;d \Rightarrow \mathsf{TReal}} \quad \text{([⇒] Lit-Decimal)}` ### Variables -``` - Γ(x) = T -─────────────────────────── (Var-Local, impl) - Γ ⊢ Var (.Local x) ⇒ T -``` +$$`\frac{\Gamma(x) = T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Local}\;x) \Rightarrow T} \quad \text{([⇒] Var-Local)}` -``` - Γ ⊢ e ⇒ _ Γ(f) = T_f -────────────────────────────── (Var-Field, impl) - Γ ⊢ Var (.Field e f) ⇒ T_f -``` +$$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Field}\;e\;f) \Rightarrow T_f} \quad \text{([⇒] Var-Field)}` Resolution looks `f` up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -``` - x ∉ dom(Γ) -───────────────────────────────────────── (Var-Declare, impl) - Γ ⊢ Var (.Declare ⟨x, T⟩) ⇒ TVoid ⊣ Γ, x : T -``` +$$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. ### Control flow -``` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T -───────────────────────────────────────────── (If-NoElse, impl) - Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no value when `cond` is false; without this, `x : int := if c then 5` would type-check spuriously. -``` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇒ T_t Γ ⊢ elseBr ⇒ T_e -────────────────────────────────────────────────────────────── (If-Synth, impl) - Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t} \quad \text{([⇒] If)}` Picks the then-branch type arbitrarily; the two branches are *not* compared, since a statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The -enclosing context's check (Sub, or a containing `checkSubtype` like an assignment) provides -the actual check downstream. - -``` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T Γ ⊢ elseBr ⇐ T -────────────────────────────────────────────────────────── (If-Check, impl) - Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T +enclosing context's check (\[⇐\] Sub, or a containing `checkSubtype` like an assignment) +provides the actual check downstream. +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` -Γ ⊢ cond ⇐ TBool Γ ⊢ thenBr ⇐ T TVoid <: T -───────────────────────────────────────────────────── (If-Check-NoElse, impl) - Γ ⊢ IfThenElse cond thenBr none ⇐ T -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}` -Check mode pushes `T` into both branches (rather than going through If-Synth + Sub at the -boundary). Errors fire at the offending branch instead of the surrounding `if`. Without an -else branch, the construct can only succeed when `T` admits -{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `Block-Check-Empty` +Check mode pushes `T` into both branches (rather than going through \[⇒\] If + \[⇐\] Sub at +the boundary). Errors fire at the offending branch instead of the surrounding `if`. +Without an else branch, the construct can only succeed when `T` admits +{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `\[⇐\] Block-Empty` performs for an empty block. -``` -Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇒ T -─────────────────────────────────────────────────────────────────────────── (Block-Synth, impl) - Γ ⊢ Block [s_1; …; s_n] label ⇒ T -``` +$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` `Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced by its predecessor and may itself extend it (`Var (.Declare …)` does); `s_n` is typed in @@ -379,16 +336,9 @@ Non-last statements are synthesized but their types discarded (the lax rule). Th Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` is silently accepted; flagging it belongs to a lint. -``` -───────────────────────────── (Block-Synth-Empty, impl) - Γ ⊢ Block [] label ⇒ TVoid -``` +$$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` -``` -Γ_0 = Γ Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n) Γ_{n-1} ⊢ s_n ⇐ T -─────────────────────────────────────────────────────────────────────────── (Block-Check, impl) - Γ ⊢ Block [s_1; …; s_n] label ⇐ T -``` +$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` Pushes `T` into the *last* statement rather than comparing the block's synthesized type at the boundary. Errors fire at the offending subexpression, and `T` keeps propagating through @@ -397,16 +347,9 @@ nested {name Strata.Laurel.StmtExpr.Block}`Block` / {name Strata.Laurel.StmtExpr.Hole}`Hole` / {name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. -``` - TVoid <: T -───────────────────────── (Block-Check-Empty, impl) - Γ ⊢ Block [] label ⇐ T -``` +$$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` -``` -──────────────────────── (Exit, impl) - Γ ⊢ Exit target ⇒ TVoid -``` +$$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` `Return` matches the optional return value against the enclosing procedure's declared outputs. The expected output types are threaded through @@ -416,74 +359,42 @@ outputs. The expected output types are threaded through the body. `none` means "no enclosing procedure" — e.g. resolving a constant initializer — and skips all `Return` checks. -``` -───────────────────────────── (Return-None, impl) - Γ ⊢ Return none ⇒ TVoid -``` +$$`\frac{}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None)}` A bare `return;` is allowed in any context. In a single-output procedure it acts as a Dafny-style early exit — the output parameter retains whatever was last assigned to it. -``` - Γ_proc.outputs = [T] Γ ⊢ e ⇐ T -────────────────────────────────────── (Return-Some, impl) - Γ ⊢ Return (some e) ⇒ TVoid -``` +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T] \quad \Gamma \vdash e \Leftarrow T}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-Some)}` In a single-output procedure, the value is checked against the declared output type. This closes the prior soundness gap where `return 0` in a `bool`-returning procedure went uncaught. -``` - Γ_proc.outputs = [] -───────────────────────────────── (Return-Void-Error, impl) - Γ ⊢ Return (some e) — error: "void procedure cannot return a value" - +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “void procedure cannot return a value”}} \quad \text{([⇒] Return-Void-Error)}` - Γ_proc.outputs = [T_1; …; T_n] (n ≥ 2) -────────────────────────────────────────────────────────── (Return-Multi-Error, impl) - Γ ⊢ Return (some e) — error: "multi-output procedure cannot - use 'return e'; assign to named outputs instead" -``` +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}` Multi-output procedures use named-output assignment (`r := …` on the declared output parameters). `return e` syntactically takes a single {name Strata.Laurel.StmtExpr.Return}`Option StmtExpr`, so it cannot carry multiple values; flagging it points users at the named-output convention. -``` - Γ ⊢ cond ⇐ TBool Γ ⊢ invs_i ⇐ TBool Γ ⊢ dec ⇐ ? Γ ⊢ body ⇒ _ -─────────────────────────────────────────────────────────────────────────────── (While, impl) - Γ ⊢ While cond invs dec body ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Rightarrow \_}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}` `dec` (the optional decreases clause) is resolved without a type check today; the intended target is a numeric type. ### Verification statements -``` - Γ ⊢ cond ⇐ TBool -────────────────────────────── (Assert, impl) - Γ ⊢ Assert cond ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}` -``` - Γ ⊢ cond ⇐ TBool -───────────────────────────── (Assume, impl) - Γ ⊢ Assume cond ⇒ TVoid -``` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}` ### Assignment -``` - Γ ⊢ targets_i ⇒ T_i Γ ⊢ e ⇒ T_e T_e <: ExpectedTy -───────────────────────────────────────────────────────────────── (Assign, impl) - Γ ⊢ Assign targets e ⇒ TVoid +$$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Rightarrow T_e \quad T_e <: \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assign)}` - where ExpectedTy = T_1 if |targets| = 1 - = MultiValuedExpr [T_1; …; T_n] otherwise -``` +where `ExpectedTy = T_1` if `|targets| = 1` and `MultiValuedExpr [T_1; …; T_n]` otherwise. The target's declared type `T_i` comes from the variable's scope entry (for {name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) @@ -497,26 +408,11 @@ to assign. ### Calls -``` - Γ(callee) = static-procedure with inputs Ts and outputs [T] - Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -───────────────────────────────────────────────────────────── (Static-Call, impl) - Γ ⊢ StaticCall callee args ⇒ T -``` +$$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Static-Call)}` -``` - Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n], n ≠ 1 - Γ ⊢ args ⇒ Us U_i <: T_i (pairwise) -────────────────────────────────────────────────────────────────────────────────── (Static-Call-Multi, impl) - Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n] -``` +$$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T_1; \ldots; T_n],\; n \ne 1 \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Static-Call-Multi)}` -``` - Γ ⊢ target ⇒ _ Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T] - Γ ⊢ args ⇒ Us U_i <: T_i (pairwise; self is dropped) -───────────────────────────────────────────────────────────────────────────────────────────── (Instance-Call, impl) - Γ ⊢ InstanceCall target callee args ⇒ T -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_ \quad \Gamma(\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{InstanceCall}\;\mathit{target}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Instance-Call)}` ### Primitive operations @@ -524,76 +420,36 @@ to assign. {name Strata.Laurel.HighType.TReal}`TReal`, {name Strata.Laurel.HighType.TFloat64}`TFloat64`". -``` - Γ ⊢ args_i ⇐ TBool op ∈ {And, Or, AndThen, OrElse, Not, Implies} -────────────────────────────────── (Op-Bool, impl) - Γ ⊢ PrimitiveOp op args ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TBool} \quad \mathit{op} \in \{\mathsf{And}, \mathsf{Or}, \mathsf{AndThen}, \mathsf{OrElse}, \mathsf{Not}, \mathsf{Implies}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Bool)}` -``` - Γ ⊢ args_i ⇐ Numeric op ∈ {Lt, Leq, Gt, Geq} -───────────────────────────────── (Op-Cmp, impl) - Γ ⊢ PrimitiveOp op args ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \mathit{op} \in \{\mathsf{Lt}, \mathsf{Leq}, \mathsf{Gt}, \mathsf{Geq}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Cmp)}` -``` - Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r T_l ~ T_r op ∈ {Eq, Neq} -───────────────────────────────────────────────────────── (Op-Eq, impl) - Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad T_l \sim T_r \quad \mathit{op} \in \{\mathsf{Eq}, \mathsf{Neq}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;[\mathit{lhs}; \mathit{rhs}] \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Eq)}` `~` is the consistency relation `isConsistent` — symmetric, with the {name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. -``` - Γ ⊢ args_i ⇐ Numeric Γ ⊢ args.head ⇒ T op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} -────────────────────────────────────────────────── (Op-Arith, impl) - Γ ⊢ PrimitiveOp op args ⇒ T -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \Gamma \vdash \mathit{args}.\mathsf{head} \Rightarrow T \quad \mathit{op} \in \{\mathsf{Neg}, \mathsf{Add}, \mathsf{Sub}, \mathsf{Mul}, \mathsf{Div}, \mathsf{Mod}, \mathsf{DivT}, \mathsf{ModT}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Op-Arith)}` "Result is the type of the first argument" handles `int + int → int`, `real + real → real`, etc. without unification. Known relaxation: `int + real` passes (each operand individually passes `Numeric`); a proper fix needs numeric promotion or unification. -``` - Γ ⊢ args_i ⇐ TString op = StrConcat -───────────────────────────────────── (Op-Concat, impl) - Γ ⊢ PrimitiveOp op args ⇒ TString -``` +$$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TString} \quad \mathit{op} = \mathsf{StrConcat}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TString}} \quad \text{([⇒] Op-Concat)}` ### Object forms -``` - Γ(ref) is a composite or datatype T -────────────────────────────────────────── (New-Ok, impl) - Γ ⊢ New ref ⇒ UserDefined T -``` +$$`\frac{\Gamma(\mathit{ref}) \text{ is a composite or datatype } T}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] New-Ok)}` -``` - Γ(ref) is not a composite or datatype -───────────────────────────────────────── (New-Fallback, impl) - Γ ⊢ New ref ⇒ Unknown -``` +$$`\frac{\Gamma(\mathit{ref}) \text{ is not a composite or datatype}}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] New-Fallback)}` -``` - Γ ⊢ target ⇒ _ -───────────────────────────── (AsType, impl) - Γ ⊢ AsType target T ⇒ T -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsType}\;\mathit{target}\;T \Rightarrow T} \quad \text{([⇒] AsType)}` `target` is resolved but not checked against `T` — the cast is the user's claim. -``` - Γ ⊢ target ⇒ _ -───────────────────────────────── (IsType, impl) - Γ ⊢ IsType target T ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` -``` - Γ ⊢ lhs ⇒ T_l Γ ⊢ rhs ⇒ T_r isReference T_l isReference T_r -───────────────────────────────────────────────────────────────────────────── (RefEq, impl) - Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` `isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` or {name Strata.Laurel.HighType.Unknown}`Unknown` @@ -602,67 +458,36 @@ type. Reference equality is meaningless on primitives. Compatibility between `T_ future tightening of `<:` — today, two distinct user-defined names already mismatch structurally, so the check would only fire under stronger subtyping. -``` - Γ ⊢ target ⇒ T_t Γ(f) = T_f Γ ⊢ newVal ⇐ T_f -───────────────────────────────────────────────────────────── (PureFieldUpdate, impl) - Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t -``` +$$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` `f` is resolved against `T_t` (or the enclosing instance type) and `newVal` is checked against the field's declared type. ### Verification expressions -``` - Γ, x : T ⊢ body ⇐ TBool -───────────────────────────────────────────────── (Quantifier, impl) - Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool -``` +$$`\frac{\Gamma, x : T \vdash \mathit{body} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Quantifier}\;\mathit{mode}\;\langle x, T\rangle\;\mathit{trig}\;\mathit{body} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Quantifier)}` The bound variable `x : T` is introduced in scope only for the body (and trigger). The body is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a proposition; without this, `forall x: int :: x + 1` would be silently accepted. -``` - Γ ⊢ name ⇒ _ -───────────────────────────── (Assigned, impl) - Γ ⊢ Assigned name ⇒ TBool -``` +$$`\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assigned}\;\mathit{name} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Assigned)}` -``` - Γ ⊢ v ⇒ T -───────────────── (Old, impl) - Γ ⊢ Old v ⇒ T -``` +$$`\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old)}` -``` - Γ ⊢ v ⇒ T isReference T -───────────────────────────────── (Fresh, impl) - Γ ⊢ Fresh v ⇒ TBool -``` +$$`\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}` `isReference T` is the same predicate as in {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals`. {name Strata.Laurel.StmtExpr.Fresh}`Fresh` only makes sense on heap-allocated references; `fresh(5)` is rejected. -``` - Γ ⊢ v ⇒ T Γ ⊢ proof ⇒ _ -─────────────────────────────────── (ProveBy, impl) - Γ ⊢ ProveBy v proof ⇒ T -``` +$$`\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy)}` ### Self reference -``` - Γ.instanceTypeName = some T -────────────────────────────────── (This-Inside, impl) - Γ ⊢ This ⇒ UserDefined T - +$$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{some}\;T}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] This-Inside)}` - Γ.instanceTypeName = none -────────────────────────────── (This-Outside, impl) - Γ ⊢ This ⇒ Unknown [emits "'this' is not allowed outside instance methods"] -``` +$$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{none}}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{Unknown}\;\;[\text{emits “‘this’ is not allowed outside instance methods”}]} \quad \text{([⇒] This-Outside)}` `Γ.instanceTypeName` is the {name Strata.Laurel.ResolveState}`ResolveState` field set by @@ -672,10 +497,7 @@ types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}` ### Untyped forms -``` -───────────────────────────────── (Abstract / All, impl) - Γ ⊢ Abstract / All … ⇒ Unknown -``` +$$`\frac{}{\Gamma \vdash \mathsf{Abstract}\;/\;\mathsf{All}\;\ldots \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Abstract / All)}` ### ContractOf @@ -684,18 +506,9 @@ types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}` (`Modifies`). `fn` must be a direct identifier reference to a procedure — a contract belongs to a *named* procedure, not an arbitrary expression. -``` - fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} -───────────────────────────────────────────────────────────────────────── (ContractOf-Bool, impl) - Γ ⊢ ContractOf Precondition fn ⇒ TBool - Γ ⊢ ContractOf PostCondition fn ⇒ TBool +$$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Precondition}\;\mathit{fn} \Rightarrow \mathsf{TBool} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{PostCondition}\;\mathit{fn} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] ContractOf-Bool)}` - - fn = Var (.Local id) Γ(id) ∈ {staticProcedure, instanceProcedure} -───────────────────────────────────────────────────────────────────────── (ContractOf-Set, impl) - Γ ⊢ ContractOf Reads fn ⇒ TSet Unknown - Γ ⊢ ContractOf Modifies fn ⇒ TSet Unknown -``` +$$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Reads}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{Modifies}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown}} \quad \text{([⇒] ContractOf-Set)}` `Precondition` and `PostCondition` are propositions, hence {name Strata.Laurel.HighType.TBool}`TBool`. `Reads` and `Modifies` are sets of heap-allocated @@ -703,11 +516,7 @@ locations — composite/datatype references and fields. The element type is left {name Strata.Laurel.HighType.Unknown}`Unknown` for now since the rule doesn't yet recover it from `fn`'s declared modifies/reads clauses. -``` - fn is not a procedure reference -───────────────────────────────────────────── (ContractOf-Error, impl) - Γ ⊢ ContractOf … fn — error: "'contractOf' expected a procedure reference" -``` +$$`\frac{\mathit{fn} \text{ is not a procedure reference}}{\Gamma \vdash \mathsf{ContractOf}\;\ldots\;\mathit{fn} \rightsquigarrow \text{error: “‘contractOf’ expected a procedure reference”}} \quad \text{([⇒] ContractOf-Error)}` When `fn` doesn't resolve to a procedure (e.g. it's an arbitrary expression, or resolves to a constant/variable), the diagnostic fires and the construct synthesizes @@ -719,20 +528,11 @@ exists so resolution remains exhaustive over `StmtExpr`. ### Holes -``` -──────────────────────────── (Hole-Some, impl) - Γ ⊢ Hole d (some T) ⇒ T -``` +$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T) \Rightarrow T} \quad \text{([⇒] Hole-Some)}` -``` -───────────────────────────────── (Hole-None-Synth, impl) - Γ ⊢ Hole d none ⇒ Unknown -``` +$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-None)}` -``` -───────────────────────────────────── (Hole-None-Check, impl) - Γ ⊢ Hole d none ⇐ T ↦ Hole d (some T) -``` +$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapsto\;\; \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}` In check mode, an untyped hole records the expected type `T` on the node directly. The subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it @@ -741,7 +541,7 @@ discarding it. A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended up in synth-only positions. When that pass encounters a hole whose type was already set -(by Hole-None-Check or by a user-written `?: T`), it checks the resolution-time and +(by \[⇐\] Hole-None or by a user-written `?: T`), it checks the resolution-time and inference-time types for consistency under `~`; a disagreement fires the diagnostic *"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what would otherwise be a silent overwrite. @@ -781,7 +581,7 @@ just wasted work and a maintenance hazard. ### Shrink or remove `InferHoleTypes` `InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that -Hole-None-Check writes the expected type during resolution for holes in check-mode +\[⇐\] Hole-None writes the expected type during resolution for holes in check-mode positions, the post-pass only needs to handle holes in synth-only positions (e.g. call arguments resolved through `synthStmtExpr` instead of `checkStmtExpr`). As more constructs gain bespoke check rules, fewer holes will reach `InferHoleTypes`; eventually the pass From ccc2a986d2afb58caab5985e335dfe2aa1fa6c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 15:30:10 -0400 Subject: [PATCH 106/128] extract typing rules out in helper functions for easier verso documentation --- Strata/Languages/Laurel/Resolution.lean | 1091 ++++++++++++++++------- 1 file changed, 791 insertions(+), 300 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 81d96adca6..80982bfa59 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -526,345 +526,836 @@ private def getCallInfo (callee : Identifier) : ResolveM (HighTypeMd × List Hig | some (_, .constant c) => pure (c.type, []) | _ => pure ({ val := .Unknown, source := callee.source }, []) +/-! ## Typing rules + +Each typing rule from the Laurel manual is implemented as its own helper +inside the mutual block below. Helpers are grouped by section to mirror the +*Typing rules* index in `LaurelDoc.lean`: + +- Literals — `synthLitInt`, `synthLitBool`, `synthLitString`, `synthLitDecimal` +- Variables — `synthVarLocal`, `synthVarField`, `synthVarDeclare` +- Control flow — `synthIfThenElse`, `synthBlock`, `synthWhile`, `synthExit`, + `synthReturn`, `checkBlock`, `checkIfThenElse` +- Verification statements — `synthAssert`, `synthAssume` +- Assignment — `synthAssign` +- Calls — `synthStaticCall`, `synthInstanceCall` +- Primitive operations — `synthPrimitiveOp` +- Object forms — `synthNew`, `synthAsType`, `synthIsType`, `synthRefEq`, + `synthPureFieldUpdate` +- Verification expressions — `synthQuantifier`, `synthAssigned`, `synthOld`, + `synthFresh`, `synthProveBy` +- Self reference — `synthThis` +- Untyped forms — `synthAbstract`, `synthAll` +- ContractOf — `synthContractOf` +- Holes — `synthHole`, `checkHoleNone` + +The dispatch functions `synthStmtExpr` and `checkStmtExpr` simply pattern-match +on the constructor and delegate to the corresponding helper. -/ + +-- The `h : exprMd.val = .Foo args ...` parameters on the recursive helpers +-- look unused to the linter, but each one is referenced by that helper's +-- `decreasing_by` tactic to relate `sizeOf args` to `sizeOf exprMd`. +set_option linter.unusedVariables false in mutual + +-- ### Dispatch + +/-- Synth-mode resolution: resolve `e` and synthesize its `HighType`. + Each constructor delegates to its rule's helper. -/ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do - match _: exprMd with + match h_node: exprMd with | AstNode.mk expr source => - let (val', ty) ← match _: expr with + let (val', ty) ← match h_expr: expr with | .IfThenElse cond thenBr elseBr => - -- Condition is checked against TBool. The result type is TVoid when the - -- else branch is absent (statement form: the then-branch's value is - -- discarded), otherwise the then-branch's synthesized type. We don't - -- compare the two branches against each other since statement-position - -- ifs commonly mix a value branch with a TVoid branch (return/exit). - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let (thenBr', thenTy) ← synthStmtExpr thenBr - let elseBr' ← elseBr.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') - let resultTy := match elseBr with - | none => { val := .TVoid, source := source } - | some _ => thenTy - pure (.IfThenElse cond' thenBr' elseBr', resultTy) + synthIfThenElse exprMd cond thenBr elseBr (by rw [h_node]) | .Block stmts label => - -- Synth-mode block: non-last statements have their synthesized type discarded - -- (lax rule, matches Java/Python/JS expression-statement semantics). - -- The last statement's synthesized type becomes the block's type. - withScope do - let results ← stmts.mapM synthStmtExpr - let stmts' := results.map (·.1) - let lastTy := match results.getLast? with - | some (_, ty) => ty - | none => { val := .TVoid, source := source } - pure (.Block stmts' label, lastTy) + synthBlock exprMd stmts label (by rw [h_node]) | .While cond invs dec body => - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let invs' ← invs.attach.mapM (fun a => have := a.property; do - checkStmtExpr a.val { val := .TBool, source := a.val.source }) - let dec' ← dec.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') - let (body', _) ← synthStmtExpr body - pure (.While cond' invs' dec' body', { val := .TVoid, source := source }) - | .Exit target => pure (.Exit target, { val := .TVoid, source := source }) - | .Return val => do - -- Match the optional return value against the enclosing procedure's - -- declared outputs. `expectedReturnTypes = none` means we're not inside a - -- procedure body (e.g. resolving a constant initializer); skip the check. - let expected := (← get).expectedReturnTypes - let val' ← val.attach.mapM (fun a => have := a.property; do - match expected with - | some [singleOutput] => checkStmtExpr a.val singleOutput - | _ => let (e', _) ← synthStmtExpr a.val; pure e') - -- Arity/shape diagnostics independent of the value's own type. - match val, expected with - | none, some [] => pure () - | none, some [_] => pure () -- Dafny-style early exit - | none, some _ => pure () -- multi-output: bare return is fine - | some _, some [] => - let diag := diagnosticFromSource source - "void procedure cannot return a value" - modify fun s => { s with errors := s.errors.push diag } - | some _, some [_] => pure () -- value already checked above - | some _, some _ => - let diag := diagnosticFromSource source - "multi-output procedure cannot use 'return e'; assign to named outputs instead" - modify fun s => { s with errors := s.errors.push diag } - | _, none => pure () -- no enclosing procedure - pure (.Return val', { val := .TVoid, source := source }) - | .LiteralInt v => pure (.LiteralInt v, { val := .TInt, source := source }) - | .LiteralBool v => pure (.LiteralBool v, { val := .TBool, source := source }) - | .LiteralString v => pure (.LiteralString v, { val := .TString, source := source }) - | .LiteralDecimal v => pure (.LiteralDecimal v, { val := .TReal, source := source }) - | .Var (.Local ref) => - let ref' ← resolveRef ref source - let ty ← getVarType ref - pure (.Var (.Local ref'), ty) - | .Var (.Declare param) => - let ty' ← resolveHighType param.type - let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) - | .Assign targets value => - let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do - let ⟨vv, vs⟩ := v - match vv with - | .Local ref => - let ref' ← resolveRef ref source - pure (⟨.Local ref', vs⟩ : VariableMd) - | .Field target fieldName => - let (target', _) ← synthStmtExpr target - let fieldName' ← resolveFieldRef target' fieldName source - pure (⟨.Field target' fieldName', vs⟩ : VariableMd) - | .Declare param => - let ty' ← resolveHighType param.type - let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← synthStmtExpr value - -- Compute the target's declared type, regardless of whether it's a Local, - -- a Field, or a fresh Declare. - let targetType (t : VariableMd) : ResolveM HighTypeMd := do - match t.val with - | .Local ref => getVarType ref - | .Declare param => pure param.type - | .Field _ fieldName => getVarType fieldName - -- Skip all checks when the RHS is a statement (TVoid) — no value to assign. - if valueTy.val != HighType.TVoid then - let targetTys ← targets'.mapM targetType - -- Build the expected type from the targets' declared types: a single - -- type when there's one target, a tuple (MultiValuedExpr) otherwise. - -- This matches the shape of `valueTy`, which is itself MultiValuedExpr - -- exactly when the RHS produces multiple values. A single tuple-vs-tuple - -- check then covers both arity and per-position type mismatches in one - -- diagnostic. - let expectedTy : HighTypeMd := match targetTys with - | [single] => single - | _ => { val := .MultiValuedExpr targetTys, source := source } - checkSubtype source expectedTy valueTy - pure (.Assign targets' value', valueTy) + synthWhile exprMd cond invs dec body (by rw [h_node]) + | .Exit target => pure (synthExit target source) + | .Return val => + synthReturn exprMd source val (by rw [h_node]) + | .LiteralInt v => pure (synthLitInt v source) + | .LiteralBool v => pure (synthLitBool v source) + | .LiteralString v => pure (synthLitString v source) + | .LiteralDecimal v => pure (synthLitDecimal v source) + | .Var (.Local ref) => synthVarLocal ref source + | .Var (.Declare param) => synthVarDeclare param source | .Var (.Field target fieldName) => - let (target', _) ← synthStmtExpr target - let fieldName' ← resolveFieldRef target' fieldName source - let ty ← getVarType fieldName' - pure (.Var (.Field target' fieldName'), ty) + synthVarField exprMd target fieldName source (by rw [h_node]) + | .Assign targets value => + synthAssign exprMd targets value source (by rw [h_node]) | .PureFieldUpdate target fieldName newVal => - let (target', targetTy) ← synthStmtExpr target - let fieldName' ← resolveFieldRef target' fieldName source - let fieldTy ← getVarType fieldName' - let newVal' ← checkStmtExpr newVal fieldTy - pure (.PureFieldUpdate target' fieldName' newVal', targetTy) + synthPureFieldUpdate exprMd target fieldName newVal (by rw [h_node]) | .StaticCall callee args => - let callee' ← resolveRef callee source - (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let results ← args.mapM synthStmtExpr - let args' := results.map (·.1) - let argTypes := results.map (·.2) - let (retTy, paramTypes) ← getCallInfo callee - for ((a, aTy), paramTy) in (args'.zip argTypes).zip paramTypes do - checkSubtype a.source paramTy aTy - pure (.StaticCall callee' args', retTy) + synthStaticCall exprMd callee args source (by rw [h_node]) | .PrimitiveOp op args => - let results ← args.mapM synthStmtExpr - let args' := results.map (·.1) - let argTypes := results.map (·.2) - let resultTy := match op with - | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies - | .Lt | .Leq | .Gt | .Geq => HighType.TBool - | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT => - match argTypes.head? with - | some headTy => headTy.val - | none => HighType.TInt - | .StrConcat => HighType.TString - match op with - | .And | .Or | .AndThen | .OrElse | .Not | .Implies => - for (a, aTy) in args'.zip argTypes do - checkSubtype a.source { val := .TBool, source := a.source } aTy - | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => - let ctx := (← get).typeContext - for (a, aTy) in args'.zip argTypes do - unless isNumeric ctx aTy do - typeMismatch a.source (some expr) "expected a numeric type" aTy - | .Eq | .Neq => - match argTypes with - | [lhsTy, rhsTy] => - let ctx := (← get).typeContext - unless isConsistent ctx lhsTy rhsTy do - let diag := diagnosticFromSource source - s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" - modify fun s => { s with errors := s.errors.push diag } - | _ => pure () - | .StrConcat => - for (a, aTy) in args'.zip argTypes do - checkSubtype a.source { val := .TString, source := a.source } aTy - pure (.PrimitiveOp op args', { val := resultTy, source := source }) - | .New ref => - let ref' ← resolveRef ref source - (expected := #[.compositeType, .datatypeDefinition]) - -- If the reference resolved to the wrong kind, use Unknown type to avoid cascading errors - let s ← get - let kindOk : Bool := match s.scope.get? ref.text with - | some (_, node) => node.kind == .unresolved || - (#[ResolvedNodeKind.compositeType, .datatypeDefinition].contains node.kind) - | none => true - let ty := if kindOk then { val := HighType.UserDefined ref', source := source } - else { val := HighType.Unknown, source := source } - pure (.New ref', ty) - | .This => - let s ← get - match s.instanceTypeName with - | some typeName => - let typeId : Identifier := - match s.scope.get? typeName with - | some (uid, _) => { text := typeName, uniqueId := some uid, source := source } - | none => { text := typeName, source := source } - pure (.This, { val := .UserDefined typeId, source := source }) - | none => - let diag := diagnosticFromSource source "'this' is not allowed outside instance methods" - modify fun s => { s with errors := s.errors.push diag } - pure (.This, { val := .Unknown, source := source }) + synthPrimitiveOp exprMd expr op args source h_expr (by rw [h_node]) + | .New ref => synthNew ref source + | .This => synthThis source | .ReferenceEquals lhs rhs => - let (lhs', lhsTy) ← synthStmtExpr lhs - let (rhs', rhsTy) ← synthStmtExpr rhs - let ctx := (← get).typeContext - unless isReference ctx lhsTy do - typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy - unless isReference ctx rhsTy do - typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy - pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) + synthRefEq exprMd expr lhs rhs source h_expr (by rw [h_node]) | .AsType target ty => - let (target', _) ← synthStmtExpr target - let ty' ← resolveHighType ty - pure (.AsType target' ty', ty') + synthAsType exprMd target ty (by rw [h_node]) | .IsType target ty => - let (target', _) ← synthStmtExpr target - let ty' ← resolveHighType ty - pure (.IsType target' ty', { val := .TBool, source := source }) + synthIsType exprMd target ty source (by rw [h_node]) | .InstanceCall target callee args => - let (target', _) ← synthStmtExpr target - let callee' ← resolveRef callee source - (expected := #[.instanceProcedure, .staticProcedure]) - let results ← args.mapM synthStmtExpr - let args' := results.map (·.1) - let argTypes := results.map (·.2) - let (retTy, paramTypes) ← getCallInfo callee - -- Skip first param (self) when matching args. - let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] - for ((a, aTy), paramTy) in (args'.zip argTypes).zip callParamTypes do - checkSubtype a.source paramTy aTy - pure (.InstanceCall target' callee' args', retTy) + synthInstanceCall exprMd target callee args source (by rw [h_node]) | .Quantifier mode param trigger body => - withScope do - let paramTy' ← resolveHighType param.type - let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') - let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do - let (e', _) ← synthStmtExpr pv.val; pure e') - let body' ← checkStmtExpr body { val := .TBool, source := body.source } - pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) + synthQuantifier exprMd mode param trigger body source (by rw [h_node]) | .Assigned name => - let (name', _) ← synthStmtExpr name - pure (.Assigned name', { val := .TBool, source := source }) + synthAssigned exprMd name source (by rw [h_node]) | .Old val => - let (val', valTy) ← synthStmtExpr val - pure (.Old val', valTy) + synthOld exprMd val (by rw [h_node]) | .Fresh val => - let (val', valTy) ← synthStmtExpr val - unless isReference (← get).typeContext valTy do - typeMismatch val'.source (some expr) "expected a reference type" valTy - pure (.Fresh val', { val := .TBool, source := source }) + synthFresh exprMd expr val source h_expr (by rw [h_node]) | .Assert ⟨condExpr, summary⟩ => - let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } - pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) + synthAssert exprMd condExpr summary source (by rw [h_node]) | .Assume cond => - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - pure (.Assume cond', { val := .TVoid, source := source }) + synthAssume exprMd cond source (by rw [h_node]) | .ProveBy val proof => - let (val', valTy) ← synthStmtExpr val - let (proof', _) ← synthStmtExpr proof - pure (.ProveBy val' proof', valTy) + synthProveBy exprMd val proof (by rw [h_node]) | .ContractOf ty fn => - -- `fn` must be a direct identifier reference resolving to a procedure. - -- Anything else (arbitrary expressions, references to non-procedures) is - -- ill-formed: a contract belongs to a *named* procedure. - let (fn', _) ← synthStmtExpr fn - let s ← get - let fnIsProcRef : Bool := match fn'.val with - | .Var (.Local ref) => - match s.scope.get? ref.text with - | some (_, node) => - node.kind == .staticProcedure || - node.kind == .instanceProcedure || - node.kind == .unresolved - | none => true -- unresolved name already reported - | _ => false - unless fnIsProcRef do - let diag := diagnosticFromSource fn.source - "'contractOf' expected a procedure reference" - modify fun s => { s with errors := s.errors.push diag } - -- Result type: Bool for pre/postconditions, set of heap references for - -- reads/modifies. The element type of the set is left as Unknown for now - -- since the rule doesn't recover it from `fn`. - let resultTy : HighType := match ty with - | .Precondition | .PostCondition => .TBool - | .Reads | .Modifies => .TSet { val := .Unknown, source := none } - pure (.ContractOf ty fn', { val := resultTy, source := source }) - | .Abstract => pure (.Abstract, { val := .Unknown, source := source }) - | .All => pure (.All, { val := .Unknown, source := source }) - | .Hole det type => match type with - | some ty => - let ty' ← resolveHighType ty - pure (.Hole det ty', ty') - | none => pure (.Hole det none, { val := .Unknown, source := source }) + synthContractOf exprMd ty fn source (by rw [h_node]) + | .Abstract => pure (synthAbstract source) + | .All => pure (synthAll source) + | .Hole det type => synthHole det type source return ({ val := val', source := source }, ty) - termination_by (exprMd, 0) + termination_by (exprMd, 2) decreasing_by all_goals first | (apply Prod.Lex.left; term_by_mem) + | (try subst h_node; apply Prod.Lex.right; decide) | (apply Prod.Lex.right; decide) -/-- Check-mode resolution: resolve `e` and verify its type is a consistent - subtype of `expected`. Bidirectional rules for individual constructs push - `expected` into subexpressions; everything else falls back to subsumption - (synth, then `isConsistentSubtype actual expected`). -/ +/-- Check-mode resolution (rule **Sub** at the boundary): resolve `e` and + verify its type is a consistent subtype of `expected`. Bidirectional rules + for individual constructs push `expected` into subexpressions; everything + else falls back to subsumption (synth, then `isConsistentSubtype actual + expected`). -/ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do - match _: exprMd with + match h_node: exprMd with | AstNode.mk expr source => - match _: expr with + match h_expr: expr with | .Block stmts label => - -- Bespoke check rule: discard non-last statement types (lax), push - -- `expected` into the last statement. Empty block reduces to subsumption - -- of TVoid against `expected`. - withScope do - let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do - have : s ∈ stmts := List.dropLast_subset stmts hMem - let (s', _) ← synthStmtExpr s; pure s') - match _lastResult: stmts.getLast? with - | none => - checkSubtype source expected { val := .TVoid, source := source } - pure { val := .Block init' label, source := source } - | some last => - have := List.mem_of_getLast? _lastResult - let last' ← checkStmtExpr last expected - pure { val := .Block (init' ++ [last']) label, source := source } + checkBlock exprMd stmts label expected source (by rw [h_node]) | .IfThenElse cond thenBr elseBr => - -- Push `expected` into both branches (rather than going through the synth - -- rule + Sub at the boundary). Without an else branch, fall back to - -- subsumption of TVoid against `expected`. - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let thenBr' ← checkStmtExpr thenBr expected - let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) - if elseBr.isNone then - checkSubtype source expected { val := .TVoid, source := source } - pure { val := .IfThenElse cond' thenBr' elseBr', source := source } - | .Hole det none => - -- Untyped hole in check mode: record the expected type on the node so - -- downstream passes don't have to infer it again. Subsumption is trivial - -- (Unknown <: T always holds). - pure { val := .Hole det (some expected), source := source } + checkIfThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) + | .Hole det none => pure (checkHoleNone det expected source) | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← synthStmtExpr exprMd checkSubtype source expected actual pure e' - termination_by (exprMd, 1) + termination_by (exprMd, 3) decreasing_by all_goals first | (apply Prod.Lex.left; term_by_mem) | (try subst_eqs; apply Prod.Lex.right; decide) + | (try subst h_node; apply Prod.Lex.right; decide) + | (apply Prod.Lex.right; decide) + +-- ### Literals + +/-- Rule **Lit-Int**: `Γ ⊢ LiteralInt n ⇒ TInt`. -/ +def synthLitInt (v : Int) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralInt v, { val := .TInt, source := source }) + +/-- Rule **Lit-Bool**: `Γ ⊢ LiteralBool b ⇒ TBool`. -/ +def synthLitBool (v : Bool) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralBool v, { val := .TBool, source := source }) + +/-- Rule **Lit-String**: `Γ ⊢ LiteralString s ⇒ TString`. -/ +def synthLitString (v : String) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralString v, { val := .TString, source := source }) + +/-- Rule **Lit-Decimal**: `Γ ⊢ LiteralDecimal d ⇒ TReal`. -/ +def synthLitDecimal (v : Decimal) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.LiteralDecimal v, { val := .TReal, source := source }) + +-- ### Variables + +/-- Rule **Var-Local**: `Γ(x) = T ⊢ Var (.Local x) ⇒ T`. Resolves `ref` against + the lexical scope and reads its declared type. -/ +def synthVarLocal (ref : Identifier) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let ref' ← resolveRef ref source + let ty ← getVarType ref + pure (.Var (.Local ref'), ty) + +/-- Rule **Var-Declare**: extends the surrounding scope with `x : T` and + synthesizes `TVoid` (the declaration itself produces no value). -/ +def synthVarDeclare (param : Parameter) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let ty' ← resolveHighType param.type + let name' ← defineNameCheckDup param.name (.var param.name ty') + pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) + +/-- Rule **Var-Field**: `Γ ⊢ e ⇒ _, Γ(f) = T_f ⊢ Var (.Field e f) ⇒ T_f`. + `f` is looked up against the type of `e` (or the enclosing instance type + for `self.f`); the typing rule itself is path-agnostic. -/ +def synthVarField (exprMd : StmtExprMd) + (target : StmtExprMd) (fieldName : Identifier) (source : Option FileRange) + (h : exprMd.val = .Var (.Field target fieldName)) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName source + let ty ← getVarType fieldName' + pure (.Var (.Field target' fieldName'), ty) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +-- ### Control flow + +/-- Rules **If-NoElse** / **If-Synth**: `cond` is checked against `TBool`. + With no else branch, the construct is a statement — `thenBr` is checked + against `TVoid` and the result is `TVoid`, so `if c then 5` is rejected. + With an else branch, the then-branch's synthesized type is returned; the + two branches are *not* compared against each other, since a statement- + position `if` often pairs a value branch with `return`/`exit`/`assert`. -/ +def synthIfThenElse (exprMd : StmtExprMd) + (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) + (h : exprMd.val = .IfThenElse cond thenBr elseBr) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let voidTy : HighTypeMd := { val := .TVoid, source := exprMd.source } + match elseBr with + | none => + let thenBr' ← checkStmtExpr thenBr voidTy + pure (.IfThenElse cond' thenBr' none, voidTy) + | some e => + let (thenBr', thenTy) ← synthStmtExpr thenBr + let (elseBr', _) ← synthStmtExpr e + pure (.IfThenElse cond' thenBr' (some elseBr'), thenTy) + termination_by (exprMd, 1) + decreasing_by + all_goals first + | (apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + try omega) + | (apply Prod.Lex.right; decide) + +/-- Rules **Block-Synth** / **Block-Synth-Empty**: non-last statements are + synthesized but their types discarded (the lax rule, matching + Java/Python/JS expression-statement semantics); the last statement's type + becomes the block's type, or `TVoid` for an empty block. The block opens + a fresh nested scope. -/ +def synthBlock (exprMd : StmtExprMd) + (stmts : List StmtExprMd) (label : Option String) + (h : exprMd.val = .Block stmts label) : + ResolveM (StmtExpr × HighTypeMd) := do + withScope do + let results ← stmts.mapM synthStmtExpr + let stmts' := results.map (·.1) + let lastTy := match results.getLast? with + | some (_, ty) => ty + | none => { val := .TVoid, source := exprMd.source } + pure (.Block stmts' label, lastTy) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + have := List.sizeOf_lt_of_mem ‹_ ∈ stmts› + omega + +/-- Rule **While**: `cond ⇐ TBool`, each invariant `⇐ TBool`, optional + `decreases` is resolved without a type check (intended target is numeric), + body is synthesized; the construct itself synthesizes `TVoid`. -/ +def synthWhile (exprMd : StmtExprMd) + (cond : StmtExprMd) (invs : List StmtExprMd) + (dec : Option StmtExprMd) (body : StmtExprMd) + (h : exprMd.val = .While cond invs dec body) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let invs' ← invs.attach.mapM (fun a => have := a.property; do + checkStmtExpr a.val { val := .TBool, source := a.val.source }) + let dec' ← dec.attach.mapM (fun a => have := a.property; do + let (e', _) ← synthStmtExpr a.val; pure e') + let (body', _) ← synthStmtExpr body + pure (.While cond' invs' dec' body', { val := .TVoid, source := exprMd.source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try (have := List.sizeOf_lt_of_mem ‹_ ∈ invs›) + try simp_all + omega + +/-- Rule **Exit**: `Γ ⊢ Exit target ⇒ TVoid`. -/ +def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := + (.Exit target, { val := .TVoid, source := source }) + +/-- Rules **Return-None** / **Return-Some** / **Return-Void-Error** / + **Return-Multi-Error**: matches the optional return value against the + enclosing procedure's declared outputs (`expectedReturnTypes`). `none` + means "no enclosing procedure" — e.g. resolving a constant initializer — + and skips all `Return` checks. -/ +def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) + (val : Option StmtExprMd) + (h : exprMd.val = .Return val) : + ResolveM (StmtExpr × HighTypeMd) := do + let expected := (← get).expectedReturnTypes + let val' ← val.attach.mapM (fun a => have := a.property; do + match expected with + | some [singleOutput] => checkStmtExpr a.val singleOutput + | _ => let (e', _) ← synthStmtExpr a.val; pure e') + -- Arity/shape diagnostics independent of the value's own type. + match val, expected with + | none, some [] => pure () + | none, some [_] => pure () -- Dafny-style early exit + | none, some _ => pure () -- multi-output: bare return is fine + | some _, some [] => + let diag := diagnosticFromSource source + "void procedure cannot return a value" + modify fun s => { s with errors := s.errors.push diag } + | some _, some [_] => pure () -- value already checked above + | some _, some _ => + let diag := diagnosticFromSource source + "multi-output procedure cannot use 'return e'; assign to named outputs instead" + modify fun s => { s with errors := s.errors.push diag } + | _, none => pure () -- no enclosing procedure + pure (.Return val', { val := .TVoid, source := source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + simp_all + omega + +/-- Rules **Block-Check** / **Block-Check-Empty**: pushes `expected` into the + *last* statement rather than comparing the block's synthesized type at the + boundary. Errors fire at the offending subexpression, and `T` keeps + propagating through nested `Block` / `IfThenElse` / `Hole` / `Quantifier`. + Empty blocks reduce to a subsumption check of `TVoid` against `expected`. -/ +def checkBlock (exprMd : StmtExprMd) + (stmts : List StmtExprMd) (label : Option String) + (expected : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .Block stmts label) : ResolveM StmtExprMd := do + withScope do + let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do + have : s ∈ stmts := List.dropLast_subset stmts hMem + let (s', _) ← synthStmtExpr s; pure s') + match _lastResult: stmts.getLast? with + | none => + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .Block init' label, source := source } + | some last => + have := List.mem_of_getLast? _lastResult + let last' ← checkStmtExpr last expected + pure { val := .Block (init' ++ [last']) label, source := source } + termination_by (exprMd, 0) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try (have := List.sizeOf_lt_of_mem ‹_ ∈ stmts›) + try simp_all + omega + +/-- Rules **If-Check** / **If-Check-NoElse**: pushes `expected` into both + branches (rather than going through If-Synth + Sub at the boundary). + Errors fire at the offending branch instead of the surrounding `if`. + Without an else branch, the construct can only succeed when `T` admits + `TVoid`. -/ +def checkIfThenElse (exprMd : StmtExprMd) + (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) + (expected : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .IfThenElse cond thenBr elseBr) : ResolveM StmtExprMd := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let thenBr' ← checkStmtExpr thenBr expected + let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) + if elseBr.isNone then + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .IfThenElse cond' thenBr' elseBr', source := source } + termination_by (exprMd, 0) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +-- ### Verification statements + +/-- Rule **Assert**: `cond` is checked against `TBool`; the construct + synthesizes `TVoid`. -/ +def synthAssert (exprMd : StmtExprMd) + (condExpr : StmtExprMd) (summary : Option String) (source : Option FileRange) + (h : exprMd.val = .Assert ⟨condExpr, summary⟩) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } + pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +/-- Rule **Assume**: `cond` is checked against `TBool`; the construct + synthesizes `TVoid`. -/ +def synthAssume (exprMd : StmtExprMd) + (cond : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Assume cond) : + ResolveM (StmtExpr × HighTypeMd) := do + let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + pure (.Assume cond', { val := .TVoid, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +-- ### Assignment + +/-- Rule **Assign**: each target's declared type `T_i` (from `Local`, + `Field`, or fresh `Declare`) is collapsed into a tuple `ExpectedTy` + (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) + and checked against the RHS's synthesized type. When the RHS is a + statement (`TVoid`) — `while`, `return`, … — all checks are skipped: + there's no value to assign. -/ +def synthAssign (exprMd : StmtExprMd) + (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Assign targets value) : + ResolveM (StmtExpr × HighTypeMd) := do + let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do + let ⟨vv, vs⟩ := v + match vv with + | .Local ref => + let ref' ← resolveRef ref source + pure (⟨.Local ref', vs⟩ : VariableMd) + | .Field target fieldName => + let (target', _) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName source + pure (⟨.Field target' fieldName', vs⟩ : VariableMd) + | .Declare param => + let ty' ← resolveHighType param.type + let name' ← defineNameCheckDup param.name (.var param.name ty') + pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) + let (value', valueTy) ← synthStmtExpr value + let targetType (t : VariableMd) : ResolveM HighTypeMd := do + match t.val with + | .Local ref => getVarType ref + | .Declare param => pure param.type + | .Field _ fieldName => getVarType fieldName + if valueTy.val != HighType.TVoid then + let targetTys ← targets'.mapM targetType + let expectedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + checkSubtype source expectedTy valueTy + pure (.Assign targets' value', valueTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) + omega + +-- ### Calls + +/-- Rules **Static-Call** / **Static-Call-Multi**: callee is resolved against + the expected kinds (parameter, static procedure, datatype constructor, + constant); each argument is synthesized and checked against the + corresponding parameter type. The result type is the (possibly + multi-valued) declared output type from `getCallInfo`. -/ +def synthStaticCall (exprMd : StmtExprMd) + (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .StaticCall callee args) : + ResolveM (StmtExpr × HighTypeMd) := do + let callee' ← resolveRef callee source + (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) + let results ← args.mapM synthStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + for ((a, aTy), paramTy) in (args'.zip argTypes).zip paramTypes do + checkSubtype a.source paramTy aTy + pure (.StaticCall callee' args', retTy) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + have := List.sizeOf_lt_of_mem ‹_ ∈ args› + omega + +/-- Rule **Instance-Call**: target is synthesized; callee resolves to an + instance or static procedure; arguments are checked pairwise against the + callee's parameter types after dropping `self`. -/ +def synthInstanceCall (exprMd : StmtExprMd) + (target : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) + (source : Option FileRange) + (h : exprMd.val = .InstanceCall target callee args) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let callee' ← resolveRef callee source + (expected := #[.instanceProcedure, .staticProcedure]) + let results ← args.mapM synthStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let (retTy, paramTypes) ← getCallInfo callee + let callParamTypes := match paramTypes with | _ :: rest => rest | [] => [] + for ((a, aTy), paramTy) in (args'.zip argTypes).zip callParamTypes do + checkSubtype a.source paramTy aTy + pure (.InstanceCall target' callee' args', retTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try (have := List.sizeOf_lt_of_mem ‹_ ∈ args›) + try simp_all + omega + +-- ### Primitive operations + +/-- Rules **Op-Bool** / **Op-Cmp** / **Op-Eq** / **Op-Arith** / **Op-Concat**: + each operator family has its own argument-type discipline and result + type. Arguments are synthesized first, then the per-family check fires + (`⇐ TBool` for booleans, `Numeric` for arithmetic/comparison, consistency + `~` for equality, `⇐ TString` for concatenation). The result type is + `TBool` for booleans/comparisons/equality, the head argument's type for + arithmetic, `TString` for concatenation. -/ +def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) + (op : Operation) (args : List StmtExprMd) (source : Option FileRange) + (h_expr : expr = .PrimitiveOp op args) + (h : exprMd.val = .PrimitiveOp op args) : + ResolveM (StmtExpr × HighTypeMd) := do + let _ := h_expr -- carries the constructor identity for `expr` in diagnostics + let results ← args.mapM synthStmtExpr + let args' := results.map (·.1) + let argTypes := results.map (·.2) + let resultTy := match op with + | .Eq | .Neq | .And | .Or | .AndThen | .OrElse | .Not | .Implies + | .Lt | .Leq | .Gt | .Geq => HighType.TBool + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT => + match argTypes.head? with + | some headTy => headTy.val + | none => HighType.TInt + | .StrConcat => HighType.TString + match op with + | .And | .Or | .AndThen | .OrElse | .Not | .Implies => + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TBool, source := a.source } aTy + | .Neg | .Add | .Sub | .Mul | .Div | .Mod | .DivT | .ModT | .Lt | .Leq | .Gt | .Geq => + let ctx := (← get).typeContext + for (a, aTy) in args'.zip argTypes do + unless isNumeric ctx aTy do + typeMismatch a.source (some expr) "expected a numeric type" aTy + | .Eq | .Neq => + match argTypes with + | [lhsTy, rhsTy] => + let ctx := (← get).typeContext + unless isConsistent ctx lhsTy rhsTy do + let diag := diagnosticFromSource source + s!"Operands of '{op}' have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + modify fun s => { s with errors := s.errors.push diag } + | _ => pure () + | .StrConcat => + for (a, aTy) in args'.zip argTypes do + checkSubtype a.source { val := .TString, source := a.source } aTy + pure (.PrimitiveOp op args', { val := resultTy, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + have := List.sizeOf_lt_of_mem ‹_ ∈ args› + omega + +-- ### Object forms + +/-- Rules **New-Ok** / **New-Fallback**: when `ref` resolves to a composite or + datatype, the type is `UserDefined ref`; otherwise `Unknown` (suppresses + cascading errors after the kind diagnostic has already fired). -/ +def synthNew (ref : Identifier) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let ref' ← resolveRef ref source + (expected := #[.compositeType, .datatypeDefinition]) + let s ← get + let kindOk : Bool := match s.scope.get? ref.text with + | some (_, node) => node.kind == .unresolved || + (#[ResolvedNodeKind.compositeType, .datatypeDefinition].contains node.kind) + | none => true + let ty := if kindOk then { val := HighType.UserDefined ref', source := source } + else { val := HighType.Unknown, source := source } + pure (.New ref', ty) + +/-- Rule **AsType**: `target` is resolved but not checked against `T` — the + cast is the user's claim. The synthesized type is `T`. -/ +def synthAsType (exprMd : StmtExprMd) + (target : StmtExprMd) (ty : HighTypeMd) + (h : exprMd.val = .AsType target ty) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let ty' ← resolveHighType ty + pure (.AsType target' ty', ty') + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **IsType**: `target` is resolved; the synthesized type is `TBool`. -/ +def synthIsType (exprMd : StmtExprMd) + (target : StmtExprMd) (ty : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .IsType target ty) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', _) ← synthStmtExpr target + let ty' ← resolveHighType ty + pure (.IsType target' ty', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **RefEq**: both operands must be reference types (`UserDefined` or + `Unknown`). Reference equality is meaningless on primitives. -/ +def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) + (lhs rhs : StmtExprMd) (source : Option FileRange) + (h_expr : expr = .ReferenceEquals lhs rhs) + (h : exprMd.val = .ReferenceEquals lhs rhs) : + ResolveM (StmtExpr × HighTypeMd) := do + let _ := h_expr + let (lhs', lhsTy) ← synthStmtExpr lhs + let (rhs', rhsTy) ← synthStmtExpr rhs + let ctx := (← get).typeContext + unless isReference ctx lhsTy do + typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy + unless isReference ctx rhsTy do + typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy + pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **PureFieldUpdate**: `target` is synthesized, `f` resolved against + `T_t` (or the enclosing instance type), and `newVal` checked against the + field's declared type. The synthesized type is `T_t` — updating a field + on a pure type produces a new value of the same type. -/ +def synthPureFieldUpdate (exprMd : StmtExprMd) + (target : StmtExprMd) (fieldName : Identifier) (newVal : StmtExprMd) + (h : exprMd.val = .PureFieldUpdate target fieldName newVal) : + ResolveM (StmtExpr × HighTypeMd) := do + let (target', targetTy) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName target.source + let fieldTy ← getVarType fieldName' + let newVal' ← checkStmtExpr newVal fieldTy + pure (.PureFieldUpdate target' fieldName' newVal', targetTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +-- ### Verification expressions + +/-- Rule **Quantifier**: opens a fresh scope, binds `x : T`, resolves the + optional trigger, and checks the body against `TBool`. The construct + itself synthesizes `TBool` since a quantifier is a proposition. -/ +def synthQuantifier (exprMd : StmtExprMd) + (mode : QuantifierMode) (param : Parameter) + (trigger : Option StmtExprMd) (body : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Quantifier mode param trigger body) : + ResolveM (StmtExpr × HighTypeMd) := do + withScope do + let paramTy' ← resolveHighType param.type + let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') + let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do + let (e', _) ← synthStmtExpr pv.val; pure e') + let body' ← checkStmtExpr body { val := .TBool, source := body.source } + pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + omega + +/-- Rule **Assigned**: `name` is synthesized; the construct synthesizes + `TBool`. -/ +def synthAssigned (exprMd : StmtExprMd) + (name : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .Assigned name) : + ResolveM (StmtExpr × HighTypeMd) := do + let (name', _) ← synthStmtExpr name + pure (.Assigned name', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **Old**: `Γ ⊢ v ⇒ T ⊢ Old v ⇒ T`. -/ +def synthOld (exprMd : StmtExprMd) + (val : StmtExprMd) + (h : exprMd.val = .Old val) : + ResolveM (StmtExpr × HighTypeMd) := do + let (val', valTy) ← synthStmtExpr val + pure (.Old val', valTy) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **Fresh**: `v` is synthesized and must have a reference type + (`UserDefined` or `Unknown`). The construct itself synthesizes `TBool`. -/ +def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) + (val : StmtExprMd) (source : Option FileRange) + (h_expr : expr = .Fresh val) + (h : exprMd.val = .Fresh val) : + ResolveM (StmtExpr × HighTypeMd) := do + let _ := h_expr + let (val', valTy) ← synthStmtExpr val + unless isReference (← get).typeContext valTy do + typeMismatch val'.source (some expr) "expected a reference type" valTy + pure (.Fresh val', { val := .TBool, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +/-- Rule **ProveBy**: `v` and `proof` are both synthesized; the construct's + type is `v`'s type — `proof` is a hint for downstream verification. -/ +def synthProveBy (exprMd : StmtExprMd) + (val proof : StmtExprMd) + (h : exprMd.val = .ProveBy val proof) : + ResolveM (StmtExpr × HighTypeMd) := do + let (val', valTy) ← synthStmtExpr val + let (proof', _) ← synthStmtExpr proof + pure (.ProveBy val' proof', valTy) + termination_by (exprMd, 1) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +-- ### Self reference + +/-- Rules **This-Inside** / **This-Outside**: when `instanceTypeName` is set + (we're inside an instance method), `This` synthesizes `UserDefined T`; + otherwise an error is emitted and the type collapses to `Unknown`. -/ +def synthThis (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + let s ← get + match s.instanceTypeName with + | some typeName => + let typeId : Identifier := + match s.scope.get? typeName with + | some (uid, _) => { text := typeName, uniqueId := some uid, source := source } + | none => { text := typeName, source := source } + pure (.This, { val := .UserDefined typeId, source := source }) + | none => + let diag := diagnosticFromSource source "'this' is not allowed outside instance methods" + modify fun s => { s with errors := s.errors.push diag } + pure (.This, { val := .Unknown, source := source }) + +-- ### Untyped forms + +/-- Rule **Abstract**: synthesizes `Unknown`. -/ +def synthAbstract (source : Option FileRange) : StmtExpr × HighTypeMd := + (.Abstract, { val := .Unknown, source := source }) + +/-- Rule **All**: synthesizes `Unknown`. -/ +def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := + (.All, { val := .Unknown, source := source }) + +-- ### ContractOf + +/-- Rules **ContractOf-Bool** / **ContractOf-Set** / **ContractOf-Error**: + `fn` must be a direct identifier reference resolving to a procedure; + anything else is ill-formed (a contract belongs to a *named* procedure). + Pre/postconditions are propositions (`TBool`); reads/modifies are sets of + heap references with element type `Unknown` for now. -/ +def synthContractOf (exprMd : StmtExprMd) + (ty : ContractType) (fn : StmtExprMd) (source : Option FileRange) + (h : exprMd.val = .ContractOf ty fn) : + ResolveM (StmtExpr × HighTypeMd) := do + let (fn', _) ← synthStmtExpr fn + let s ← get + let fnIsProcRef : Bool := match fn'.val with + | .Var (.Local ref) => + match s.scope.get? ref.text with + | some (_, node) => + node.kind == .staticProcedure || + node.kind == .instanceProcedure || + node.kind == .unresolved + | none => true -- unresolved name already reported + | _ => false + unless fnIsProcRef do + let diag := diagnosticFromSource fn.source + "'contractOf' expected a procedure reference" + modify fun s => { s with errors := s.errors.push diag } + let resultTy : HighType := match ty with + | .Precondition | .PostCondition => .TBool + | .Reads | .Modifies => .TSet { val := .Unknown, source := none } + pure (.ContractOf ty fn', { val := resultTy, source := source }) + termination_by (exprMd, 1) + decreasing_by + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + omega + +-- ### Holes + +/-- Rules **Hole-Some** / **Hole-None-Synth**: a typed hole synthesizes its + annotation; an untyped hole in synth position synthesizes `Unknown`. -/ +def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : + ResolveM (StmtExpr × HighTypeMd) := do + match type with + | some ty => + let ty' ← resolveHighType ty + pure (.Hole det ty', ty') + | none => pure (.Hole det none, { val := .Unknown, source := source }) + +/-- Rule **Hole-None-Check**: an untyped hole in check mode records the + expected type on the node so downstream passes don't have to infer it + again. The subsumption check is trivial (`Unknown <: T` always holds), so + this rule never fails — it just preserves the type information available + at the check-mode boundary. -/ +def checkHoleNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : + StmtExprMd := + { val := .Hole det (some expected), source := source } + end /-- Resolve a statement expression, discarding the synthesized type. From 3d61b0479dd1b8e35cafd1d7da449635e5f6fc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 15:30:24 -0400 Subject: [PATCH 107/128] better if-then-else typing discipline --- docs/verso/LaurelDoc.lean | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 140a250407..a53f161f60 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -302,11 +302,12 @@ remainder of the enclosing scope. ### Control flow -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow \mathsf{TVoid}}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no -value when `cond` is false; without this, `x : int := if c then 5` would type-check -spuriously. +value when `cond` is false; the then-branch is checked against +{name Strata.Laurel.HighType.TVoid}`TVoid` so `x : int := if c then 5` is rejected at the +branch rather than slipping through to a downstream subsumption. $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t} \quad \text{([⇒] If)}` From 665e88f3c01a419f8da2eaa117b3764673251693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 16:38:23 -0400 Subject: [PATCH 108/128] if then else type synthesis --- Strata/Languages/Laurel/Laurel.lean | 37 +++++++++++++++++++ Strata/Languages/Laurel/Resolution.lean | 15 +++++--- .../Laurel/ResolutionTypeCheckTests.lean | 21 +++++++++++ docs/verso/LaurelDoc.lean | 15 +++++--- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index c5f5dede7c..771788958a 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -559,6 +559,43 @@ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := def isConsistentSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := isConsistent ctx sub sup || isSubtype ctx sub sup +/-- BFS through `extendingMap` starting from `name` and stopping at the first + type that is also in `targetAncestors`. Used by `joinTypes` to find a + common ancestor between two composites; `visited` cuts off cycles. -/ +partial def TypeContext.firstCommonAncestor (ctx : TypeContext) + (name : String) (targetAncestors : Std.HashSet String) : Option String := + let rec go (frontier : List String) (visited : Std.HashSet String) : Option String := + match frontier with + | [] => none + | n :: rest => + if visited.contains n then go rest visited + else if targetAncestors.contains n then some n + else + let parents := (ctx.extendingMap.get? n).getD [] + go (rest ++ parents) (visited.insert n) + go [name] {} + +/-- Least upper bound for the if-then-else synthesis rule. When `a` and `b` + are subtype-related, returns the larger; for unrelated composites, walks + `extending` chains for the first common ancestor. When no common + supertype exists (e.g. unrelated primitives, or a value branch paired + with a `TVoid` `return`/`exit`), falls back to `a` — the enclosing + context's `checkSubtype` then surfaces any mismatch against the + then-branch's type, preserving the historical statement-form behavior. -/ +def joinTypes (ctx : TypeContext) (a b : HighTypeMd) : HighTypeMd := + if isConsistentSubtype ctx a b then b + else if isConsistentSubtype ctx b a then a + else + let a' := ctx.unfold a + let b' := ctx.unfold b + match a'.val, b'.val with + | .UserDefined aName, .UserDefined bName => + match ctx.firstCommonAncestor aName.text (ctx.ancestors bName.text) with + | some name => + { val := .UserDefined { text := name, source := none }, source := a.source } + | none => a + | _, _ => a + def HighType.isBool : HighType → Bool | TBool => true | _ => false diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 80982bfa59..75bbbabb0d 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -713,9 +713,13 @@ def synthVarField (exprMd : StmtExprMd) /-- Rules **If-NoElse** / **If-Synth**: `cond` is checked against `TBool`. With no else branch, the construct is a statement — `thenBr` is checked against `TVoid` and the result is `TVoid`, so `if c then 5` is rejected. - With an else branch, the then-branch's synthesized type is returned; the - two branches are *not* compared against each other, since a statement- - position `if` often pairs a value branch with `return`/`exit`/`assert`. -/ + With an else branch, the result type is the join (LUB) of the two + branches' synthesized types, so `if c then new Left else new Right` + synthesizes the common ancestor `Top` rather than committing to one + branch arbitrarily. When no common supertype exists (e.g. a value branch + paired with a `TVoid` `return`/`exit`), `joinTypes` falls back to the + then-branch's type and the enclosing context's check surfaces any + mismatch downstream. -/ def synthIfThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : @@ -728,8 +732,9 @@ def synthIfThenElse (exprMd : StmtExprMd) pure (.IfThenElse cond' thenBr' none, voidTy) | some e => let (thenBr', thenTy) ← synthStmtExpr thenBr - let (elseBr', _) ← synthStmtExpr e - pure (.IfThenElse cond' thenBr' (some elseBr'), thenTy) + let (elseBr', elseTy) ← synthStmtExpr e + let ctx := (← get).typeContext + pure (.IfThenElse cond' thenBr' (some elseBr'), joinTypes ctx thenTy elseTy) termination_by (exprMd, 1) decreasing_by all_goals first diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index b78f3b22df..c674bf0fe4 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -194,4 +194,25 @@ procedure test() opaque { #guard_msgs (error, drop all) in #eval testInputWithOffset "UserDefinedCrossType" userDefinedCrossType 170 processResolution +/-! ## If-then-else branch join + +When the two branches have different but subtype-related types, the construct +synthesizes their join (least upper bound) — not the then-branch arbitrarily. +So `if c then new Left else new Right`, with `Left, Right <: Top`, synthesizes +`Top` and an assignment to a `Left`-typed variable is rejected. -/ + +def ifBranchJoinToCommonAncestor := r" +composite Top { } +composite Left extends Top { } +composite Right extends Top { } +procedure test(c: bool) opaque { + var x: Top := if c then new Left else new Right; + var y: Left := if c then new Left else new Right +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'Left', got 'Top' +}; +" + +#guard_msgs (error, drop all) in +#eval testInputWithOffset "IfBranchJoinToCommonAncestor" ifBranchJoinToCommonAncestor 198 processResolution + end Laurel diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index a53f161f60..e87db76d31 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -309,12 +309,15 @@ value when `cond` is false; the then-branch is checked against {name Strata.Laurel.HighType.TVoid}`TVoid` so `x : int := if c then 5` is rejected at the branch rather than slipping through to a downstream subsumption. -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t} \quad \text{([⇒] If)}` - -Picks the then-branch type arbitrarily; the two branches are *not* compared, since a -statement-position `if` often pairs a value branch with a `return`/`exit`/`assert`. The -enclosing context's check (\[⇐\] Sub, or a containing `checkSubtype` like an assignment) -provides the actual check downstream. +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If)}` + +The result is the join (least upper bound) of the two branch types, so +`if c then small else big` synthesizes the common supertype rather than committing to one +branch arbitrarily. The join walks `extending` chains for composites; when no common +supertype exists (e.g. a value branch paired with a `TVoid` `return`/`exit`), it falls +back to `T_t` and the enclosing context's check (\[⇐\] Sub, or a containing +`checkSubtype` like an assignment) surfaces any mismatch downstream against the +then-branch's type. $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` From 95763e243b11c554ef262d234c4f64c3659a8444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 16:40:58 -0400 Subject: [PATCH 109/128] move test to appropriate location --- .../Examples/Objects/T9_IfBranchJoin.lean | 35 +++++++++++++++++++ .../Laurel/ResolutionTypeCheckTests.lean | 21 ----------- 2 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean diff --git a/StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean b/StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean new file mode 100644 index 0000000000..9149d2e647 --- /dev/null +++ b/StrataTest/Languages/Laurel/Examples/Objects/T9_IfBranchJoin.lean @@ -0,0 +1,35 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +import StrataTest.Util.TestDiagnostics +import StrataTest.Languages.Laurel.TestExamples + +open StrataTest.Util + +namespace Strata +namespace Laurel + +/- +When the two branches of an `if/else` have different but subtype-related +types, the construct synthesizes their join (least upper bound) — not the +then-branch arbitrarily. So `if c then new Left else new Right`, with +`Left, Right <: Top`, synthesizes `Top`. Storing it in a `Top`-typed +variable succeeds, but storing it in a `Left`-typed variable is rejected. +-/ + +def program := r" +composite Top { } +composite Left extends Top { } +composite Right extends Top { } +procedure test(c: bool) opaque { + var x: Top := if c then new Left else new Right; + var y: Left := if c then new Left else new Right +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'Left', got 'Top' +}; +" + +#guard_msgs (drop info) in +#eval testInputWithOffset "IfBranchJoin" program 22 processLaurelFile diff --git a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean index c674bf0fe4..b78f3b22df 100644 --- a/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean +++ b/StrataTest/Languages/Laurel/ResolutionTypeCheckTests.lean @@ -194,25 +194,4 @@ procedure test() opaque { #guard_msgs (error, drop all) in #eval testInputWithOffset "UserDefinedCrossType" userDefinedCrossType 170 processResolution -/-! ## If-then-else branch join - -When the two branches have different but subtype-related types, the construct -synthesizes their join (least upper bound) — not the then-branch arbitrarily. -So `if c then new Left else new Right`, with `Left, Right <: Top`, synthesizes -`Top` and an assignment to a `Left`-typed variable is rejected. -/ - -def ifBranchJoinToCommonAncestor := r" -composite Top { } -composite Left extends Top { } -composite Right extends Top { } -procedure test(c: bool) opaque { - var x: Top := if c then new Left else new Right; - var y: Left := if c then new Left else new Right -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: expected 'Left', got 'Top' -}; -" - -#guard_msgs (error, drop all) in -#eval testInputWithOffset "IfBranchJoinToCommonAncestor" ifBranchJoinToCommonAncestor 198 processResolution - end Laurel From 034110555e32d23f45e3f69909adeb65ec2043f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 17:03:17 -0400 Subject: [PATCH 110/128] very strict dereference comparison --- Strata/Languages/Laurel/Resolution.lean | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 75bbbabb0d..01443dee39 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -1152,6 +1152,10 @@ def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy unless isReference ctx rhsTy do typeMismatch rhs'.source (some expr) "expected a reference type" rhsTy + unless isConsistent ctx lhsTy rhsTy do + let diag := diagnosticFromSource source + s!"'{expr.constrName}' operands have incompatible types '{formatType lhsTy}' and '{formatType rhsTy}'" + modify fun s => { s with errors := s.errors.push diag } pure (.ReferenceEquals lhs' rhs', { val := .TBool, source := source }) termination_by (exprMd, 1) decreasing_by From 3fbb542d1a2f17c887e5603a466479b9b0043916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Tue, 19 May 2026 17:04:00 -0400 Subject: [PATCH 111/128] consistent references when comparing --- docs/verso/LaurelDoc.lean | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index e87db76d31..34b5772920 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -453,14 +453,15 @@ $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsT $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` -$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` +$$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r \quad T_l \sim T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` `isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` or {name Strata.Laurel.HighType.Unknown}`Unknown` -type. Reference equality is meaningless on primitives. Compatibility between `T_l` and -`T_r` (e.g. rejecting `Cat === Dog` for unrelated user-defined types) is delegated to -future tightening of `<:` — today, two distinct user-defined names already mismatch -structurally, so the check would only fire under stronger subtyping. +type. Reference equality is meaningless on primitives. The operands must also be +consistent under `~` (Siek–Taha consistency), matching the rule applied by +{name Strata.Laurel.Operation.Eq}`==`: two distinct user-defined types like `Cat` and +`Dog` are rejected, while either side being `Unknown` is accepted as a gradual escape +hatch. $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` From 25028250463e067b0c6b9d3126b44fa42b3be442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 09:33:07 -0400 Subject: [PATCH 112/128] fix assign by creating a checking rule --- Strata/Languages/Laurel/Resolution.lean | 56 ++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 01443dee39..ddaf0df040 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -640,6 +640,8 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE checkBlock exprMd stmts label expected source (by rw [h_node]) | .IfThenElse cond thenBr elseBr => checkIfThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) + | .Assign targets value => + checkAssign exprMd targets value expected source (by rw [h_node]) | .Hole det none => pure (checkHoleNone det expected source) | _ => -- Subsumption fallback: synth then check `actual <: expected`. @@ -933,7 +935,10 @@ def synthAssume (exprMd : StmtExprMd) (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) and checked against the RHS's synthesized type. When the RHS is a statement (`TVoid`) — `while`, `return`, … — all checks are skipped: - there's no value to assign. -/ + there's no value to assign. The construct synthesizes the RHS's type, + so that expression-position assignments like `x ++ (y := s)` see a + string in the second operand; statement-position uses are accommodated + by `checkAssign`, which accepts `TVoid` as the expected type. -/ def synthAssign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : @@ -975,6 +980,55 @@ def synthAssign (exprMd : StmtExprMd) try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) omega +/-- Rule **Assign-Check**: an assignment in statement position (checked + against `TVoid`) discards its RHS value, so the synthesized type is not + compared against `expected`. This lets `b := 1` appear as the last + statement of a block in an else-less `if` (whose branch is checked + against `TVoid`) without firing a subsumption error against the RHS's + type. For non-`TVoid` expected types, falls back to subsumption. -/ +def checkAssign (exprMd : StmtExprMd) + (targets : List VariableMd) (value : StmtExprMd) + (expected : HighTypeMd) (source : Option FileRange) + (h : exprMd.val = .Assign targets value) : ResolveM StmtExprMd := do + let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do + let ⟨vv, vs⟩ := v + match vv with + | .Local ref => + let ref' ← resolveRef ref source + pure (⟨.Local ref', vs⟩ : VariableMd) + | .Field target fieldName => + let (target', _) ← synthStmtExpr target + let fieldName' ← resolveFieldRef target' fieldName source + pure (⟨.Field target' fieldName', vs⟩ : VariableMd) + | .Declare param => + let ty' ← resolveHighType param.type + let name' ← defineNameCheckDup param.name (.var param.name ty') + pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) + let (value', valueTy) ← synthStmtExpr value + let targetType (t : VariableMd) : ResolveM HighTypeMd := do + match t.val with + | .Local ref => getVarType ref + | .Declare param => pure param.type + | .Field _ fieldName => getVarType fieldName + if valueTy.val != HighType.TVoid then + let targetTys ← targets'.mapM targetType + let assignedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + checkSubtype source assignedTy valueTy + unless expected.val matches .TVoid do + checkSubtype source expected valueTy + pure { val := .Assign targets' value', source := source } + termination_by (exprMd, 0) + decreasing_by + all_goals + apply Prod.Lex.left + have hsz := exprMd.sizeOf_val_lt + simp [h] at hsz + try simp_all + try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) + omega + -- ### Calls /-- Rules **Static-Call** / **Static-Call-Multi**: callee is resolved against From c9ce9405fd1c1bc94c267533151d40aa95a89154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 10:05:43 -0400 Subject: [PATCH 113/128] documentation is moved to in-code docstrings --- Strata/Languages/Laurel/Laurel.lean | 38 +++- Strata/Languages/Laurel/Resolution.lean | 205 +++++++++++++----- docs/verso/LaurelDoc.lean | 262 ++++++++---------------- 3 files changed, 272 insertions(+), 233 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index 771788958a..05268233df 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -531,9 +531,14 @@ partial def TypeContext.ancestors (ctx : TypeContext) (name : String) : Std.Hash go acc' (parents ++ rest) go {} [name] -/-- Subtyping. Walks `extending` chains for composites, unfolds aliases, and - unwraps constrained types to their base before falling back to structural - equality via `highEq`. -/ +/-- Pure subtyping `<:`. Walks the `extending` chain for `CompositeType` + (via `TypeContext.ancestors`), unfolds `TypeAlias` to its target, and + unwraps `ConstrainedType` to its base (both via `TypeContext.unfold`), + then falls back to structural equality via `highEq`. + + Used together with `isConsistent` to form `isConsistentSubtype`, which + is what the bidirectional checker invokes at every check-mode boundary + (rule `[⇐] Sub`). -/ def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := let sub' := ctx.unfold sub let sup' := ctx.unfold sup @@ -544,9 +549,13 @@ def isSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := (ctx.ancestors subName.text).contains supName.text || highEq sub' sup' | _, _ => highEq sub' sup' -/-- Consistency (Siek–Taha): the symmetric gradual relation. `Unknown` is the - dynamic type and is consistent with everything; otherwise structural - equality after unfolding aliases / constrained types. -/ +/-- Consistency `~` (Siek–Taha): the symmetric gradual relation. `Unknown` + is the dynamic type and is consistent with everything; otherwise + structural equality after unfolding aliases / constrained types. + + Used directly by `[⇒] Op-Eq`, where the operand types must be mutually + consistent (no subtype direction is privileged), and as one half of + `isConsistentSubtype`. -/ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := let a' := ctx.unfold a let b' := ctx.unfold b @@ -555,7 +564,22 @@ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := | _, _ => highEq a' b' /-- Consistent subtyping: `∃ R. sub ~ R ∧ R <: sup`. For our flat lattice - this collapses to `sub ~ sup ∨ sub <: sup`. -/ + this collapses to `sub ~ sup ∨ sub <: sup` — the standard collapse. + + Used by rule `[⇐] Sub` (and every bespoke check rule). That single + choice is what makes the system *gradual*: an expression of type + `Unknown` (a hole, an unresolved name, a `Hole _ none`) flows freely + into any typed slot, and any expression flows freely into a slot of + type `Unknown`. Strict checking is applied between fully-known types + only. + + A previous iteration was synth-only with two *bivariantly-compatible* + wildcards: `Unknown` and `UserDefined`. The `UserDefined` carve-out was + load-bearing: no assignment, call argument, or comparison involving a + user type was ever rejected. The bidirectional design retires that + carve-out — user-defined types are now a regular participant in `<:`, + with `isSubtype` walking inheritance chains and unwrapping aliases + and constrained types to deliver real checking on user-defined code. -/ def isConsistentSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := isConsistent ctx sub sup || isSubtype ctx sub sup diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index ddaf0df040..970e32d0ad 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -560,8 +560,20 @@ mutual -- ### Dispatch -/-- Synth-mode resolution: resolve `e` and synthesize its `HighType`. - Each constructor delegates to its rule's helper. -/ +/-- Synth-mode resolution: resolve `e` and synthesize its `HighType`, + written `Γ ⊢ e ⇒ T`. Each constructor delegates to its rule's helper. + + Synthesis returns a type inferred from the expression itself; checking + (`checkStmtExpr`) verifies that the expression has a given expected + type. Each construct picks a mode based on whether its type is + determined locally (synth) or by context (check). Synth rules invoke + check on subexpressions whose expected type is known (e.g. + `cond ⇐ TBool` in `IfThenElse`); `checkStmtExpr` falls back to + `synthStmtExpr` via subsumption (rule `[⇐] Sub`). The two functions + are mutually recursive, with termination on a lexicographic measure + `(exprMd, tag)` — tag `0` for check, `1` for synth — so that + subsumption (which calls synth on the *same* expression) can decrease + via `Prod.Lex.right`. -/ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match h_node: exprMd with | AstNode.mk expr source => @@ -628,10 +640,22 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := | (apply Prod.Lex.right; decide) /-- Check-mode resolution (rule **Sub** at the boundary): resolve `e` and - verify its type is a consistent subtype of `expected`. Bidirectional rules - for individual constructs push `expected` into subexpressions; everything - else falls back to subsumption (synth, then `isConsistentSubtype actual - expected`). -/ + verify its type is a consistent subtype of `expected`, written + `Γ ⊢ e ⇐ T`. Bidirectional rules for individual constructs (`Block`, + `IfThenElse`, `Assign`, `Hole`) push `expected` into subexpressions + rather than bouncing through synthesis, which keeps error messages + localized and lets the expected type propagate through nested control + flow. Everything else falls back to subsumption — synthesize, then + verify `isConsistentSubtype actual expected`. + + The right principle for new call sites is: when the position has a + known expected type (`TBool` for conditions, numeric for `decreases`, + the declared output for a constant initializer or a functional body), + use `checkStmtExpr`. When it doesn't, use `resolveStmtExpr` (a thin + wrapper that calls `synthStmtExpr` and discards the synthesized type, + used at sites where typing is not enforced — verification annotations, + modifies/reads clauses). `synthStmtExpr` itself is mostly an internal + interface used by other rules. -/ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do match h_node: exprMd with | AstNode.mk expr source => @@ -714,14 +738,19 @@ def synthVarField (exprMd : StmtExprMd) /-- Rules **If-NoElse** / **If-Synth**: `cond` is checked against `TBool`. With no else branch, the construct is a statement — `thenBr` is checked - against `TVoid` and the result is `TVoid`, so `if c then 5` is rejected. + against `TVoid` and the result is `TVoid`, so `x : int := if c then 5` + is rejected at the branch rather than slipping through to a downstream + subsumption. + With an else branch, the result type is the join (LUB) of the two - branches' synthesized types, so `if c then new Left else new Right` - synthesizes the common ancestor `Top` rather than committing to one - branch arbitrarily. When no common supertype exists (e.g. a value branch - paired with a `TVoid` `return`/`exit`), `joinTypes` falls back to the - then-branch's type and the enclosing context's check surfaces any - mismatch downstream. -/ + branches' synthesized types, so `if c then small else big` synthesizes + the common supertype rather than committing to one branch arbitrarily; + `if c then new Left else new Right` synthesizes the common ancestor. + When no common supertype exists (e.g. a value branch paired with a + `TVoid` `return`/`exit`), `joinTypes` falls back to the then-branch's + type and the enclosing context's check (`[⇐] Sub`, or a containing + `checkSubtype` like an assignment) surfaces any mismatch downstream + against the then-branch's type. -/ def synthIfThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : @@ -747,11 +776,14 @@ def synthIfThenElse (exprMd : StmtExprMd) try omega) | (apply Prod.Lex.right; decide) -/-- Rules **Block-Synth** / **Block-Synth-Empty**: non-last statements are - synthesized but their types discarded (the lax rule, matching - Java/Python/JS expression-statement semantics); the last statement's type +/-- Rules **Block-Synth** / **Block-Synth-Empty**: each statement is resolved + in the scope produced by its predecessor and may itself extend it + (`Var (.Declare …)` does); non-last statements are synthesized but their + types discarded (the lax rule, matching Java/Python/JS where `f(x);` is + normal even when `f` returns a value — trade-off: `5;` is silently + accepted, flagging it belongs to a lint). The last statement's type becomes the block's type, or `TVoid` for an empty block. The block opens - a fresh nested scope. -/ + a fresh nested scope, so bindings introduced inside don't escape. -/ def synthBlock (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (h : exprMd.val = .Block stmts label) : @@ -772,8 +804,9 @@ def synthBlock (exprMd : StmtExprMd) omega /-- Rule **While**: `cond ⇐ TBool`, each invariant `⇐ TBool`, optional - `decreases` is resolved without a type check (intended target is numeric), - body is synthesized; the construct itself synthesizes `TVoid`. -/ + `decreases` is resolved without a type check today (the intended target + is a numeric type), body is synthesized; the construct itself + synthesizes `TVoid`. -/ def synthWhile (exprMd : StmtExprMd) (cond : StmtExprMd) (invs : List StmtExprMd) (dec : Option StmtExprMd) (body : StmtExprMd) @@ -802,9 +835,22 @@ def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTy /-- Rules **Return-None** / **Return-Some** / **Return-Void-Error** / **Return-Multi-Error**: matches the optional return value against the - enclosing procedure's declared outputs (`expectedReturnTypes`). `none` - means "no enclosing procedure" — e.g. resolving a constant initializer — - and skips all `Return` checks. -/ + enclosing procedure's declared outputs. The expected output types are + threaded through `ResolveState.expectedReturnTypes`, set from + `proc.outputs` by `resolveProcedure` / `resolveInstanceProcedure` for + the duration of the body; `none` means "no enclosing procedure" — e.g. + resolving a constant initializer — and skips all `Return` checks. + + A bare `return;` is allowed in any context. In a single-output procedure + it acts as a Dafny-style early exit — the output parameter retains + whatever was last assigned to it. In a single-output procedure, `return e` + is checked against the declared output type (closing the prior soundness + gap where `return 0` in a `bool`-returning procedure went uncaught). + + Multi-output procedures use named-output assignment (`r := …` on the + declared output parameters); `return e` syntactically takes a single + `Option StmtExpr` and cannot carry multiple values, so it is flagged with + a diagnostic pointing users at the named-output convention. -/ def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) (val : Option StmtExprMd) (h : exprMd.val = .Return val) : @@ -841,9 +887,11 @@ def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) /-- Rules **Block-Check** / **Block-Check-Empty**: pushes `expected` into the *last* statement rather than comparing the block's synthesized type at the - boundary. Errors fire at the offending subexpression, and `T` keeps - propagating through nested `Block` / `IfThenElse` / `Hole` / `Quantifier`. - Empty blocks reduce to a subsumption check of `TVoid` against `expected`. -/ + boundary. Errors fire at the offending subexpression, and `expected` + keeps propagating through nested `Block` / `IfThenElse` / `Hole` / + `Quantifier`. Empty blocks reduce to a subsumption check of `TVoid` + against `expected` — the same check `[⇐] Block-Empty` performs when + `T` admits `TVoid`. -/ def checkBlock (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (expected : HighTypeMd) (source : Option FileRange) @@ -873,8 +921,9 @@ def checkBlock (exprMd : StmtExprMd) /-- Rules **If-Check** / **If-Check-NoElse**: pushes `expected` into both branches (rather than going through If-Synth + Sub at the boundary). Errors fire at the offending branch instead of the surrounding `if`. - Without an else branch, the construct can only succeed when `T` admits - `TVoid`. -/ + Without an else branch, the construct can only succeed when `expected` + admits `TVoid` — the same subsumption check `[⇐] Block-Empty` performs + for an empty block. -/ def checkIfThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) @@ -933,12 +982,16 @@ def synthAssume (exprMd : StmtExprMd) /-- Rule **Assign**: each target's declared type `T_i` (from `Local`, `Field`, or fresh `Declare`) is collapsed into a tuple `ExpectedTy` (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) - and checked against the RHS's synthesized type. When the RHS is a - statement (`TVoid`) — `while`, `return`, … — all checks are skipped: - there's no value to assign. The construct synthesizes the RHS's type, - so that expression-position assignments like `x ++ (y := s)` see a - string in the second operand; statement-position uses are accommodated - by `checkAssign`, which accepts `TVoid` as the expected type. -/ + and checked against the RHS's synthesized type. Both single- and + multi-target forms collapse into one tuple-vs-tuple check: when the RHS + is a `MultiValuedExpr`, both arity and per-position type mismatches + surface in a single diagnostic of shape *"expected '(int, int, int)', + got '(int, string)'"*. When the RHS is `TVoid` (a side-effecting + statement: `while`, `return`, …), all checks are skipped — there's no + value to assign. The construct synthesizes the RHS's type, so that + expression-position assignments like `x ++ (y := s)` see a string in + the second operand; statement-position uses are accommodated by + `checkAssign`, which accepts `TVoid` as the expected type. -/ def synthAssign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : @@ -1090,11 +1143,17 @@ def synthInstanceCall (exprMd : StmtExprMd) /-- Rules **Op-Bool** / **Op-Cmp** / **Op-Eq** / **Op-Arith** / **Op-Concat**: each operator family has its own argument-type discipline and result - type. Arguments are synthesized first, then the per-family check fires - (`⇐ TBool` for booleans, `Numeric` for arithmetic/comparison, consistency - `~` for equality, `⇐ TString` for concatenation). The result type is - `TBool` for booleans/comparisons/equality, the head argument's type for - arithmetic, `TString` for concatenation. -/ + type. Arguments are synthesized first, then the per-family check fires: + `⇐ TBool` for booleans, `Numeric` (consistent with `TInt`, `TReal`, or + `TFloat64`) for arithmetic/comparison, consistency `~` for equality + (symmetric — no subtype direction is privileged), `⇐ TString` for + concatenation. The result type is `TBool` for + booleans/comparisons/equality, the head argument's type for arithmetic + ("result is the type of the first argument" handles `int + int → int`, + `real + real → real`, etc. without unification — known relaxation: + `int + real` passes since each operand individually passes `Numeric`; + a proper fix needs numeric promotion or unification), `TString` for + concatenation. -/ def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) (op : Operation) (args : List StmtExprMd) (source : Option FileRange) (h_expr : expr = .PrimitiveOp op args) @@ -1161,7 +1220,9 @@ def synthNew (ref : Identifier) (source : Option FileRange) : pure (.New ref', ty) /-- Rule **AsType**: `target` is resolved but not checked against `T` — the - cast is the user's claim. The synthesized type is `T`. -/ + cast is the user's claim. The synthesized type is `T`. + + `IsType` is the runtime test counterpart and synthesizes `TBool`. -/ def synthAsType (exprMd : StmtExprMd) (target : StmtExprMd) (ty : HighTypeMd) (h : exprMd.val = .AsType target ty) : @@ -1192,7 +1253,12 @@ def synthIsType (exprMd : StmtExprMd) omega /-- Rule **RefEq**: both operands must be reference types (`UserDefined` or - `Unknown`). Reference equality is meaningless on primitives. -/ + `Unknown`) — reference equality is meaningless on primitives. The + operands must also be mutually consistent (the symmetric `isConsistent`), + so `Cat === Dog` is rejected when `Cat` and `Dog` are unrelated + user-defined types, while `Cat === Animal` is accepted when `Cat` + extends `Animal` (the gradual `Unknown` wildcard makes either side + flow freely against the other). -/ def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) (lhs rhs : StmtExprMd) (source : Option FileRange) (h_expr : expr = .ReferenceEquals lhs rhs) @@ -1242,9 +1308,11 @@ def synthPureFieldUpdate (exprMd : StmtExprMd) -- ### Verification expressions -/-- Rule **Quantifier**: opens a fresh scope, binds `x : T`, resolves the - optional trigger, and checks the body against `TBool`. The construct - itself synthesizes `TBool` since a quantifier is a proposition. -/ +/-- Rule **Quantifier**: opens a fresh scope, binds `x : T` (in scope only + for the body and trigger), resolves the optional trigger, and checks + the body against `TBool` since a quantifier is a proposition. Without + that body check, `forall x: int :: x + 1` would be silently accepted. + The construct itself synthesizes `TBool`. -/ def synthQuantifier (exprMd : StmtExprMd) (mode : QuantifierMode) (param : Parameter) (trigger : Option StmtExprMd) (body : StmtExprMd) (source : Option FileRange) @@ -1296,7 +1364,9 @@ def synthOld (exprMd : StmtExprMd) omega /-- Rule **Fresh**: `v` is synthesized and must have a reference type - (`UserDefined` or `Unknown`). The construct itself synthesizes `TBool`. -/ + (`UserDefined` or `Unknown`) — `Fresh` only makes sense on + heap-allocated references, so `fresh(5)` is rejected. The construct + itself synthesizes `TBool`. -/ def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) (val : StmtExprMd) (source : Option FileRange) (h_expr : expr = .Fresh val) @@ -1334,8 +1404,13 @@ def synthProveBy (exprMd : StmtExprMd) -- ### Self reference /-- Rules **This-Inside** / **This-Outside**: when `instanceTypeName` is set - (we're inside an instance method), `This` synthesizes `UserDefined T`; - otherwise an error is emitted and the type collapses to `Unknown`. -/ + (we're inside an instance method, populated on `ResolveState` by + `resolveInstanceProcedure` for the duration of an instance method body), + `This` synthesizes `UserDefined T`. With it, `this.field` and + instance-method dispatch synthesize real types instead of being + wildcarded through `Unknown`. Otherwise an error is emitted ("'this' + is not allowed outside instance methods") and the type collapses to + `Unknown` to suppress cascading errors. -/ def synthThis (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let s ← get @@ -1364,10 +1439,25 @@ def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := -- ### ContractOf /-- Rules **ContractOf-Bool** / **ContractOf-Set** / **ContractOf-Error**: - `fn` must be a direct identifier reference resolving to a procedure; - anything else is ill-formed (a contract belongs to a *named* procedure). - Pre/postconditions are propositions (`TBool`); reads/modifies are sets of - heap references with element type `Unknown` for now. -/ + `ContractOf ty fn` extracts a procedure's contract clause as a value: + its preconditions (`Precondition`), postconditions (`PostCondition`), + reads set (`Reads`), or modifies set (`Modifies`). `fn` must be a + direct identifier reference resolving to a procedure — a contract + belongs to a *named* procedure, not an arbitrary expression. Anything + else fires the diagnostic *"'contractOf' expected a procedure + reference"* and the construct synthesizes `Unknown` to suppress + cascading errors. + + `Precondition` and `PostCondition` are propositions, hence `TBool`. + `Reads` and `Modifies` are sets of heap-allocated locations — + composite/datatype references and fields. The element type is left as + `Unknown` for now since the rule doesn't yet recover it from `fn`'s + declared modifies/reads clauses. + + The constructor is reserved for future use — Laurel's grammar has no + `contractOf` production today, and the translator emits "not yet + implemented" for it. The typing rule exists so resolution remains + exhaustive over `StmtExpr`. -/ def synthContractOf (exprMd : StmtExprMd) (ty : ContractType) (fn : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .ContractOf ty fn) : @@ -1412,9 +1502,18 @@ def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange /-- Rule **Hole-None-Check**: an untyped hole in check mode records the expected type on the node so downstream passes don't have to infer it - again. The subsumption check is trivial (`Unknown <: T` always holds), so - this rule never fails — it just preserves the type information available - at the check-mode boundary. -/ + again. The subsumption check is trivial (`Unknown <: T` always holds), + so this rule never fails — it just preserves the type information + available at the check-mode boundary instead of discarding it. + + A separate `InferHoleTypes` pass still runs after resolution to + annotate holes that ended up in synth-only positions. When that pass + encounters a hole whose type was already set (by `[⇐] Hole-None` or by + a user-written `?: T`), it checks the resolution-time and + inference-time types for consistency under `~`; a disagreement fires + the diagnostic *"hole annotated with 'T_resolution' but context + expects 'T_inference'"*, surfacing what would otherwise be a silent + overwrite. -/ def checkHoleNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : StmtExprMd := { val := .Hole det (some expected), source := source } diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 34b5772920..7583d4d079 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -165,70 +165,31 @@ There are two operations on expressions, written here in standard bidirectional ``` Synthesis returns a type inferred from the expression itself; checking verifies that the -expression has a given expected type. Each construct picks a mode based on whether its type -is determined locally (synth) or by context (check). The two judgments are connected by a -single change-of-direction rule, *subsumption*: +expression has a given expected type. Each construct picks a mode based on whether its +type is determined locally (synth) or by context (check). The two judgments are connected +by a single change-of-direction rule, *subsumption*: $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` -Subsumption is the *only* place the checker switches from check to synth mode. It fires as -the default fallback in -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr` for every construct without a bespoke -check rule: synthesize the expression's type, then verify the result is a subtype of the -expected type. Bespoke check rules push the expected type *into* subexpressions instead of -bouncing through synthesis, which keeps error messages localized and lets the expected type -propagate through nested control flow. - -`synthStmtExpr` and `checkStmtExpr` are mutually recursive: synth rules invoke check on -subexpressions whose expected type is known (e.g. `cond ⇐ TBool` in -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse`), and `checkStmtExpr` falls back to -`synthStmtExpr` via \[⇐\] Sub. Termination uses a lexicographic measure `(exprMd, tag)` -where the tag is `0` for synth and `1` for check; any descent into a strict subterm -decreases via `Prod.Lex.left`, while \[⇐\] Sub calls synth on the *same* expression and -decreases via -`Prod.Lex.right`. This is the standard well-founded encoding for bidirectional systems. - -There is also a thin `resolveStmtExpr` wrapper that calls `synthStmtExpr` and discards the -synthesized type. It's used at sites where typing is not enforced (verification annotations, -modifies/reads clauses). The right principle for new call sites is: when the position has a -known expected type ({name Strata.Laurel.HighType.TBool}`TBool` for conditions, numeric for -`decreases`, the declared output for a constant initializer or a functional body), use -`checkStmtExpr`. When it doesn't, use `resolveStmtExpr`. `synthStmtExpr` itself is mostly an -internal interface used by other rules. +The two judgments are implemented as +{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and +{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`: + +{docstring Strata.Laurel.synthStmtExpr} + +{docstring Strata.Laurel.checkStmtExpr} ### Gradual typing -The relation `<:` (used in \[⇐\] Sub) is built from three Lean functions: - -- `isSubtype` — pure subtyping. Walks the `extending` chain for - {name Strata.Laurel.CompositeType}`CompositeType` (via - {name Strata.Laurel.TypeContext.ancestors}`TypeContext.ancestors`), unfolds - {name Strata.Laurel.TypeAlias}`TypeAlias` to its target, and unwraps - {name Strata.Laurel.ConstrainedType}`ConstrainedType` to its base (both via - {name Strata.Laurel.TypeContext.unfold}`TypeContext.unfold`), then falls back to - structural equality via {name Strata.Laurel.highEq}`highEq`. -- `isConsistent` — the symmetric gradual relation `~` (Siek–Taha): - {name Strata.Laurel.HighType.Unknown}`Unknown` is the dynamic type and is consistent with - everything; otherwise structural equality. -- `isConsistentSubtype` — defined as `isConsistent ∨ isSubtype`. For our flat lattice this - is the standard collapse of `∃R. T ~ R ∧ R <: U`. - -\[⇐\] Sub (and every bespoke check rule) uses `isConsistentSubtype`. That single choice is what -makes the system *gradual*: an expression of type -{name Strata.Laurel.HighType.Unknown}`Unknown` (a hole, an unresolved name, a `Hole _ none`) -flows freely into any typed slot, and any expression flows freely into a slot of type -{name Strata.Laurel.HighType.Unknown}`Unknown`. Strict checking is applied between -fully-known types only. The symmetric `isConsistent` is used directly by \[⇒\] Op-Eq, where -the operand types must be mutually consistent (no subtype direction is privileged). - -A previous iteration was synth-only with two *bivariantly-compatible* wildcards: -{name Strata.Laurel.HighType.Unknown}`Unknown` and -{name Strata.Laurel.HighType.UserDefined}`UserDefined`. The -{name Strata.Laurel.HighType.UserDefined}`UserDefined` carve-out was load-bearing: no -assignment, call argument, or comparison involving a user type was ever rejected. The -bidirectional design retires that carve-out — user-defined types are now a regular -participant in `<:`, with `isSubtype` walking inheritance chains and unwrapping aliases -and constrained types to deliver real checking on user-defined code. +The relation `<:` (used in \[⇐\] Sub) is built from three Lean functions — +{name Strata.Laurel.isSubtype}`isSubtype`, {name Strata.Laurel.isConsistent}`isConsistent`, +and {name Strata.Laurel.isConsistentSubtype}`isConsistentSubtype`: + +{docstring Strata.Laurel.isSubtype} + +{docstring Strata.Laurel.isConsistent} + +{docstring Strata.Laurel.isConsistentSubtype} Side-effecting constructs synthesize {name Strata.Laurel.HighType.TVoid}`TVoid`. This includes {name Strata.Laurel.StmtExpr.Return}`Return`, @@ -270,64 +231,64 @@ suffix is dropped in favor of the prefix. - *ContractOf* — \[⇒\] ContractOf-Bool, \[⇒\] ContractOf-Set, \[⇒\] ContractOf-Error - *Holes* — \[⇒\] Hole-Some, \[⇒\] Hole-None, \[⇐\] Hole-None +Each LaTeX rule below is followed by the docstring of the helper that implements it +(grouped when one helper covers multiple rules). + ### Subsumption $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` -Fallback in `checkStmtExpr` whenever no bespoke check rule applies. +Fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` whenever no bespoke check +rule applies. ### Literals $$`\frac{}{\Gamma \vdash \mathsf{LiteralInt}\;n \Rightarrow \mathsf{TInt}} \quad \text{([⇒] Lit-Int)}` +{docstring Strata.Laurel.synthLitInt} + $$`\frac{}{\Gamma \vdash \mathsf{LiteralBool}\;b \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Lit-Bool)}` +{docstring Strata.Laurel.synthLitBool} + $$`\frac{}{\Gamma \vdash \mathsf{LiteralString}\;s \Rightarrow \mathsf{TString}} \quad \text{([⇒] Lit-String)}` +{docstring Strata.Laurel.synthLitString} + $$`\frac{}{\Gamma \vdash \mathsf{LiteralDecimal}\;d \Rightarrow \mathsf{TReal}} \quad \text{([⇒] Lit-Decimal)}` +{docstring Strata.Laurel.synthLitDecimal} + ### Variables $$`\frac{\Gamma(x) = T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Local}\;x) \Rightarrow T} \quad \text{([⇒] Var-Local)}` +{docstring Strata.Laurel.synthVarLocal} + $$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Field}\;e\;f) \Rightarrow T_f} \quad \text{([⇒] Var-Field)}` -Resolution looks `f` up against the type of `e` (or the enclosing instance type for -`self.f`); the typing rule itself is path-agnostic. +{docstring Strata.Laurel.synthVarField} $$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. +{docstring Strata.Laurel.synthVarDeclare} + ### Control flow $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow \mathsf{TVoid}}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` -The construct synthesizes {name Strata.Laurel.HighType.TVoid}`TVoid` because there is no -value when `cond` is false; the then-branch is checked against -{name Strata.Laurel.HighType.TVoid}`TVoid` so `x : int := if c then 5` is rejected at the -branch rather than slipping through to a downstream subsumption. - $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If)}` -The result is the join (least upper bound) of the two branch types, so -`if c then small else big` synthesizes the common supertype rather than committing to one -branch arbitrarily. The join walks `extending` chains for composites; when no common -supertype exists (e.g. a value branch paired with a `TVoid` `return`/`exit`), it falls -back to `T_t` and the enclosing context's check (\[⇐\] Sub, or a containing -`checkSubtype` like an assignment) surfaces any mismatch downstream against the -then-branch's type. +{docstring Strata.Laurel.synthIfThenElse} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}` -Check mode pushes `T` into both branches (rather than going through \[⇒\] If + \[⇐\] Sub at -the boundary). Errors fire at the offending branch instead of the surrounding `if`. -Without an else branch, the construct can only succeed when `T` admits -{name Strata.Laurel.HighType.TVoid}`TVoid` — the same subsumption check `\[⇐\] Block-Empty` -performs for an empty block. +{docstring Strata.Laurel.checkIfThenElse} $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` @@ -336,79 +297,56 @@ predecessor and may itself extend it (`Var (.Declare …)` does); `s_n` is typed `Γ_{n-1}`. Bindings introduced inside the block don't escape — `Γ` is what surrounds the block. -Non-last statements are synthesized but their types discarded (the lax rule). This matches -Java/Python/JS where `f(x);` is normal even when `f` returns a value. The trade-off: `5;` -is silently accepted; flagging it belongs to a lint. - $$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` -$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` +{docstring Strata.Laurel.synthBlock} -Pushes `T` into the *last* statement rather than comparing the block's synthesized type at -the boundary. Errors fire at the offending subexpression, and `T` keeps propagating through -nested {name Strata.Laurel.StmtExpr.Block}`Block` / -{name Strata.Laurel.StmtExpr.IfThenElse}`IfThenElse` / -{name Strata.Laurel.StmtExpr.Hole}`Hole` / -{name Strata.Laurel.StmtExpr.Quantifier}`Quantifier`. +$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` $$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` +{docstring Strata.Laurel.checkBlock} + $$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` -`Return` matches the optional return value against the enclosing procedure's declared -outputs. The expected output types are threaded through -{name Strata.Laurel.ResolveState}`ResolveState`'s `expectedReturnTypes`, set from -`proc.outputs` by {name Strata.Laurel.resolveProcedure}`resolveProcedure` / -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of -the body. `none` means "no enclosing procedure" — e.g. resolving a constant initializer — -and skips all `Return` checks. +{docstring Strata.Laurel.synthExit} $$`\frac{}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None)}` -A bare `return;` is allowed in any context. In a single-output procedure it acts as a -Dafny-style early exit — the output parameter retains whatever was last assigned to it. - $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T] \quad \Gamma \vdash e \Leftarrow T}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-Some)}` -In a single-output procedure, the value is checked against the declared output type. This -closes the prior soundness gap where `return 0` in a `bool`-returning procedure went -uncaught. - $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “void procedure cannot return a value”}} \quad \text{([⇒] Return-Void-Error)}` $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}` -Multi-output procedures use named-output assignment (`r := …` on the declared output -parameters). `return e` syntactically takes a single -{name Strata.Laurel.StmtExpr.Return}`Option StmtExpr`, so it cannot carry multiple values; -flagging it points users at the named-output convention. +{docstring Strata.Laurel.synthReturn} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Rightarrow \_}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}` -`dec` (the optional decreases clause) is resolved without a type check today; the intended -target is a numeric type. +{docstring Strata.Laurel.synthWhile} ### Verification statements $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}` +{docstring Strata.Laurel.synthAssert} + $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}` +{docstring Strata.Laurel.synthAssume} + ### Assignment $$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Rightarrow T_e \quad T_e <: \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assign)}` where `ExpectedTy = T_1` if `|targets| = 1` and `MultiValuedExpr [T_1; …; T_n]` otherwise. - The target's declared type `T_i` comes from the variable's scope entry (for {name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) -or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. Both -single- and multi-target forms collapse into one tuple-vs-tuple check: when the RHS is a -{name Strata.Laurel.HighType.MultiValuedExpr}`MultiValuedExpr`, both arity and per-position -type mismatches surface in a single diagnostic of shape *"expected '(int, int, int)', got -'(int, string)'"*. When the RHS is {name Strata.Laurel.HighType.TVoid}`TVoid` (a -side-effecting statement: `while`, `return`, …), all checks are skipped — there's no value -to assign. +or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. + +{docstring Strata.Laurel.synthAssign} + +{docstring Strata.Laurel.checkAssign} ### Calls @@ -416,8 +354,12 @@ $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T_1; \ldots; T_n],\; n \ne 1 \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Static-Call-Multi)}` +{docstring Strata.Laurel.synthStaticCall} + $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_ \quad \Gamma(\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{InstanceCall}\;\mathit{target}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Instance-Call)}` +{docstring Strata.Laurel.synthInstanceCall} + ### Primitive operations `Numeric` abbreviates "consistent with one of {name Strata.Laurel.HighType.TInt}`TInt`, @@ -430,106 +372,88 @@ $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \mathit $$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad T_l \sim T_r \quad \mathit{op} \in \{\mathsf{Eq}, \mathsf{Neq}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;[\mathit{lhs}; \mathit{rhs}] \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Eq)}` -`~` is the consistency relation `isConsistent` — symmetric, with the -{name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. - $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \Gamma \vdash \mathit{args}.\mathsf{head} \Rightarrow T \quad \mathit{op} \in \{\mathsf{Neg}, \mathsf{Add}, \mathsf{Sub}, \mathsf{Mul}, \mathsf{Div}, \mathsf{Mod}, \mathsf{DivT}, \mathsf{ModT}\}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Op-Arith)}` -"Result is the type of the first argument" handles `int + int → int`, `real + real → real`, -etc. without unification. Known relaxation: `int + real` passes (each operand individually -passes `Numeric`); a proper fix needs numeric promotion or unification. - $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TString} \quad \mathit{op} = \mathsf{StrConcat}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TString}} \quad \text{([⇒] Op-Concat)}` +{docstring Strata.Laurel.synthPrimitiveOp} + ### Object forms $$`\frac{\Gamma(\mathit{ref}) \text{ is a composite or datatype } T}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] New-Ok)}` $$`\frac{\Gamma(\mathit{ref}) \text{ is not a composite or datatype}}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] New-Fallback)}` +{docstring Strata.Laurel.synthNew} + $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsType}\;\mathit{target}\;T \Rightarrow T} \quad \text{([⇒] AsType)}` -`target` is resolved but not checked against `T` — the cast is the user's claim. +{docstring Strata.Laurel.synthAsType} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` +{docstring Strata.Laurel.synthIsType} + $$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r \quad T_l \sim T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` `isReference T` holds when `T` is a {name Strata.Laurel.HighType.UserDefined}`UserDefined` -or {name Strata.Laurel.HighType.Unknown}`Unknown` -type. Reference equality is meaningless on primitives. The operands must also be -consistent under `~` (Siek–Taha consistency), matching the rule applied by -{name Strata.Laurel.Operation.Eq}`==`: two distinct user-defined types like `Cat` and -`Dog` are rejected, while either side being `Unknown` is accepted as a gradual escape -hatch. +or {name Strata.Laurel.HighType.Unknown}`Unknown` type. `~` is the consistency relation +{name Strata.Laurel.isConsistent}`isConsistent` — symmetric, with the +{name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. + +{docstring Strata.Laurel.synthRefEq} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` -`f` is resolved against `T_t` (or the enclosing instance type) and `newVal` is checked -against the field's declared type. +{docstring Strata.Laurel.synthPureFieldUpdate} ### Verification expressions $$`\frac{\Gamma, x : T \vdash \mathit{body} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Quantifier}\;\mathit{mode}\;\langle x, T\rangle\;\mathit{trig}\;\mathit{body} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Quantifier)}` -The bound variable `x : T` is introduced in scope only for the body (and trigger). The body -is checked against {name Strata.Laurel.HighType.TBool}`TBool` since a quantifier is a -proposition; without this, `forall x: int :: x + 1` would be silently accepted. +{docstring Strata.Laurel.synthQuantifier} $$`\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assigned}\;\mathit{name} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Assigned)}` +{docstring Strata.Laurel.synthAssigned} + $$`\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old)}` +{docstring Strata.Laurel.synthOld} + $$`\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}` -`isReference T` is the same predicate as in {name Strata.Laurel.StmtExpr.ReferenceEquals}`ReferenceEquals`. -{name Strata.Laurel.StmtExpr.Fresh}`Fresh` only makes sense on heap-allocated references; -`fresh(5)` is rejected. +{docstring Strata.Laurel.synthFresh} $$`\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy)}` +{docstring Strata.Laurel.synthProveBy} + ### Self reference $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{some}\;T}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] This-Inside)}` $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{none}}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{Unknown}\;\;[\text{emits “‘this’ is not allowed outside instance methods”}]} \quad \text{([⇒] This-Outside)}` -`Γ.instanceTypeName` is the -{name Strata.Laurel.ResolveState}`ResolveState` field set by -{name Strata.Laurel.resolveInstanceProcedure}`resolveInstanceProcedure` for the duration of -an instance method body. With it, `this.field` and instance-method dispatch synthesize real -types instead of being wildcarded through {name Strata.Laurel.HighType.Unknown}`Unknown`. +{docstring Strata.Laurel.synthThis} ### Untyped forms $$`\frac{}{\Gamma \vdash \mathsf{Abstract}\;/\;\mathsf{All}\;\ldots \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Abstract / All)}` -### ContractOf +{docstring Strata.Laurel.synthAbstract} -`ContractOf ty fn` extracts a procedure's contract clause as a value: its preconditions -(`Precondition`), postconditions (`PostCondition`), reads set (`Reads`), or modifies set -(`Modifies`). `fn` must be a direct identifier reference to a procedure — a contract belongs -to a *named* procedure, not an arbitrary expression. +{docstring Strata.Laurel.synthAll} + +### ContractOf $$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Precondition}\;\mathit{fn} \Rightarrow \mathsf{TBool} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{PostCondition}\;\mathit{fn} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] ContractOf-Bool)}` $$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Reads}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown} \quad\quad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{Modifies}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown}} \quad \text{([⇒] ContractOf-Set)}` -`Precondition` and `PostCondition` are propositions, hence -{name Strata.Laurel.HighType.TBool}`TBool`. `Reads` and `Modifies` are sets of heap-allocated -locations — composite/datatype references and fields. The element type is left as -{name Strata.Laurel.HighType.Unknown}`Unknown` for now since the rule doesn't yet recover it -from `fn`'s declared modifies/reads clauses. - $$`\frac{\mathit{fn} \text{ is not a procedure reference}}{\Gamma \vdash \mathsf{ContractOf}\;\ldots\;\mathit{fn} \rightsquigarrow \text{error: “‘contractOf’ expected a procedure reference”}} \quad \text{([⇒] ContractOf-Error)}` -When `fn` doesn't resolve to a procedure (e.g. it's an arbitrary expression, or resolves to -a constant/variable), the diagnostic fires and the construct synthesizes -{name Strata.Laurel.HighType.Unknown}`Unknown` to suppress cascading errors. - -The constructor is reserved for future use — Laurel's grammar has no `contractOf` -production today, and the translator emits "not yet implemented" for it. The typing rule -exists so resolution remains exhaustive over `StmtExpr`. +{docstring Strata.Laurel.synthContractOf} ### Holes @@ -537,19 +461,11 @@ $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T) \Rightarrow T} \qu $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-None)}` +{docstring Strata.Laurel.synthHole} + $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapsto\;\; \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}` -In check mode, an untyped hole records the expected type `T` on the node directly. The -subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it -just preserves the type information that's available at the check-mode boundary instead of -discarding it. - -A separate `InferHoleTypes` pass still runs after resolution to annotate holes that ended -up in synth-only positions. When that pass encounters a hole whose type was already set -(by \[⇐\] Hole-None or by a user-written `?: T`), it checks the resolution-time and -inference-time types for consistency under `~`; a disagreement fires the diagnostic -*"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what -would otherwise be a silent overwrite. +{docstring Strata.Laurel.checkHoleNone} ## Future structural changes From d2de9dac9e2b139460589aece4350837e4170d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 14:16:41 -0400 Subject: [PATCH 114/128] namespace scoping to make code less verbose --- Strata/Languages/Laurel/Resolution.lean | 307 ++++++++++++------------ docs/verso/LaurelDoc.lean | 90 +++---- 2 files changed, 201 insertions(+), 196 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 970e32d0ad..080142d15c 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -38,7 +38,7 @@ Walks the AST under `ResolveM`, a state monad over `ResolveState`. Phase 1: - opens fresh nested scopes via `withScope` for blocks, quantifiers, procedure bodies, and constrained-type constraint/witness expressions, - synthesizes a `HighType` for every `StmtExpr` and checks it (via - `checkStmtExpr` for fresh subexpressions, or `checkSubtype` when a type is + `Check.resolveStmtExpr` for fresh subexpressions, or `checkSubtype` when a type is already in hand) on assignments, call arguments, condition positions, functional bodies, and constant initializers. @@ -466,7 +466,7 @@ private def typeMismatch (source : Option FileRange) (construct : Option StmtExp /-- Type-level subtype check: emits the standard "expected/got" diagnostic when `actual` is not a consistent subtype of `expected`. Used at sites where the actual type is already in hand (assignment, call args, body vs declared - output) — equivalent to `checkStmtExpr e expected` but without re-synthesizing. -/ + output) — equivalent to `Check.resolveStmtExpr e expected` but without re-synthesizing. -/ private def checkSubtype (source : Option FileRange) (expected : HighTypeMd) (actual : HighTypeMd) : ResolveM Unit := do let ctx := (← get).typeContext unless isConsistentSubtype ctx actual expected do @@ -532,26 +532,28 @@ Each typing rule from the Laurel manual is implemented as its own helper inside the mutual block below. Helpers are grouped by section to mirror the *Typing rules* index in `LaurelDoc.lean`: -- Literals — `synthLitInt`, `synthLitBool`, `synthLitString`, `synthLitDecimal` -- Variables — `synthVarLocal`, `synthVarField`, `synthVarDeclare` -- Control flow — `synthIfThenElse`, `synthBlock`, `synthWhile`, `synthExit`, - `synthReturn`, `checkBlock`, `checkIfThenElse` -- Verification statements — `synthAssert`, `synthAssume` -- Assignment — `synthAssign` -- Calls — `synthStaticCall`, `synthInstanceCall` -- Primitive operations — `synthPrimitiveOp` -- Object forms — `synthNew`, `synthAsType`, `synthIsType`, `synthRefEq`, - `synthPureFieldUpdate` -- Verification expressions — `synthQuantifier`, `synthAssigned`, `synthOld`, - `synthFresh`, `synthProveBy` -- Self reference — `synthThis` -- Untyped forms — `synthAbstract`, `synthAll` -- ContractOf — `synthContractOf` -- Holes — `synthHole`, `checkHoleNone` - -The dispatch functions `synthStmtExpr` and `checkStmtExpr` simply pattern-match +- Literals — `Synth.litInt`, `Synth.litBool`, `Synth.litString`, `Synth.litDecimal` +- Variables — `Synth.varLocal`, `Synth.varField`, `Synth.varDeclare` +- Control flow — `Synth.ifThenElse`, `Synth.block`, `Synth.while`, `Synth.exit`, + `Synth.return`, `Check.block`, `Check.ifThenElse` +- Verification statements — `Synth.assert`, `Synth.assume` +- Assignment — `Synth.assign`, `Check.assign` +- Calls — `Synth.staticCall`, `Synth.instanceCall` +- Primitive operations — `Synth.primitiveOp` +- Object forms — `Synth.new`, `Synth.asType`, `Synth.isType`, `Synth.refEq`, + `Synth.pureFieldUpdate` +- Verification expressions — `Synth.quantifier`, `Synth.assigned`, `Synth.old`, + `Synth.fresh`, `Synth.proveBy` +- Self reference — `Synth.this` +- Untyped forms — `Synth.abstract`, `Synth.all` +- ContractOf — `Synth.contractOf` +- Holes — `Synth.hole`, `Check.holeNone` + +The dispatch functions `Synth.resolveStmtExpr` and `Check.resolveStmtExpr` simply pattern-match on the constructor and delegate to the corresponding helper. -/ +namespace Resolution + -- The `h : exprMd.val = .Foo args ...` parameters on the recursive helpers -- look unused to the linter, but each one is referenced by that helper's -- `decreasing_by` tactic to relate `sizeOf args` to `sizeOf exprMd`. @@ -564,74 +566,74 @@ mutual written `Γ ⊢ e ⇒ T`. Each constructor delegates to its rule's helper. Synthesis returns a type inferred from the expression itself; checking - (`checkStmtExpr`) verifies that the expression has a given expected + (`Check.resolveStmtExpr`) verifies that the expression has a given expected type. Each construct picks a mode based on whether its type is determined locally (synth) or by context (check). Synth rules invoke check on subexpressions whose expected type is known (e.g. - `cond ⇐ TBool` in `IfThenElse`); `checkStmtExpr` falls back to - `synthStmtExpr` via subsumption (rule `[⇐] Sub`). The two functions + `cond ⇐ TBool` in `IfThenElse`); `Check.resolveStmtExpr` falls back to + `Synth.resolveStmtExpr` via subsumption (rule `[⇐] Sub`). The two functions are mutually recursive, with termination on a lexicographic measure `(exprMd, tag)` — tag `0` for check, `1` for synth — so that subsumption (which calls synth on the *same* expression) can decrease via `Prod.Lex.right`. -/ -def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do +def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match h_node: exprMd with | AstNode.mk expr source => let (val', ty) ← match h_expr: expr with | .IfThenElse cond thenBr elseBr => - synthIfThenElse exprMd cond thenBr elseBr (by rw [h_node]) + Synth.ifThenElse exprMd cond thenBr elseBr (by rw [h_node]) | .Block stmts label => - synthBlock exprMd stmts label (by rw [h_node]) + Synth.block exprMd stmts label (by rw [h_node]) | .While cond invs dec body => - synthWhile exprMd cond invs dec body (by rw [h_node]) - | .Exit target => pure (synthExit target source) + Synth.while exprMd cond invs dec body (by rw [h_node]) + | .Exit target => pure (Synth.exit target source) | .Return val => - synthReturn exprMd source val (by rw [h_node]) - | .LiteralInt v => pure (synthLitInt v source) - | .LiteralBool v => pure (synthLitBool v source) - | .LiteralString v => pure (synthLitString v source) - | .LiteralDecimal v => pure (synthLitDecimal v source) - | .Var (.Local ref) => synthVarLocal ref source - | .Var (.Declare param) => synthVarDeclare param source + Synth.return exprMd source val (by rw [h_node]) + | .LiteralInt v => pure (Synth.litInt v source) + | .LiteralBool v => pure (Synth.litBool v source) + | .LiteralString v => pure (Synth.litString v source) + | .LiteralDecimal v => pure (Synth.litDecimal v source) + | .Var (.Local ref) => Synth.varLocal ref source + | .Var (.Declare param) => Synth.varDeclare param source | .Var (.Field target fieldName) => - synthVarField exprMd target fieldName source (by rw [h_node]) + Synth.varField exprMd target fieldName source (by rw [h_node]) | .Assign targets value => - synthAssign exprMd targets value source (by rw [h_node]) + Synth.assign exprMd targets value source (by rw [h_node]) | .PureFieldUpdate target fieldName newVal => - synthPureFieldUpdate exprMd target fieldName newVal (by rw [h_node]) + Synth.pureFieldUpdate exprMd target fieldName newVal (by rw [h_node]) | .StaticCall callee args => - synthStaticCall exprMd callee args source (by rw [h_node]) + Synth.staticCall exprMd callee args source (by rw [h_node]) | .PrimitiveOp op args => - synthPrimitiveOp exprMd expr op args source h_expr (by rw [h_node]) - | .New ref => synthNew ref source - | .This => synthThis source + Synth.primitiveOp exprMd expr op args source h_expr (by rw [h_node]) + | .New ref => Synth.new ref source + | .This => Synth.this source | .ReferenceEquals lhs rhs => - synthRefEq exprMd expr lhs rhs source h_expr (by rw [h_node]) + Synth.refEq exprMd expr lhs rhs source h_expr (by rw [h_node]) | .AsType target ty => - synthAsType exprMd target ty (by rw [h_node]) + Synth.asType exprMd target ty (by rw [h_node]) | .IsType target ty => - synthIsType exprMd target ty source (by rw [h_node]) + Synth.isType exprMd target ty source (by rw [h_node]) | .InstanceCall target callee args => - synthInstanceCall exprMd target callee args source (by rw [h_node]) + Synth.instanceCall exprMd target callee args source (by rw [h_node]) | .Quantifier mode param trigger body => - synthQuantifier exprMd mode param trigger body source (by rw [h_node]) + Synth.quantifier exprMd mode param trigger body source (by rw [h_node]) | .Assigned name => - synthAssigned exprMd name source (by rw [h_node]) + Synth.assigned exprMd name source (by rw [h_node]) | .Old val => - synthOld exprMd val (by rw [h_node]) + Synth.old exprMd val (by rw [h_node]) | .Fresh val => - synthFresh exprMd expr val source h_expr (by rw [h_node]) + Synth.fresh exprMd expr val source h_expr (by rw [h_node]) | .Assert ⟨condExpr, summary⟩ => - synthAssert exprMd condExpr summary source (by rw [h_node]) + Synth.assert exprMd condExpr summary source (by rw [h_node]) | .Assume cond => - synthAssume exprMd cond source (by rw [h_node]) + Synth.assume exprMd cond source (by rw [h_node]) | .ProveBy val proof => - synthProveBy exprMd val proof (by rw [h_node]) + Synth.proveBy exprMd val proof (by rw [h_node]) | .ContractOf ty fn => - synthContractOf exprMd ty fn source (by rw [h_node]) - | .Abstract => pure (synthAbstract source) - | .All => pure (synthAll source) - | .Hole det type => synthHole det type source + Synth.contractOf exprMd ty fn source (by rw [h_node]) + | .Abstract => pure (Synth.abstract source) + | .All => pure (Synth.all source) + | .Hole det type => Synth.hole det type source return ({ val := val', source := source }, ty) termination_by (exprMd, 2) decreasing_by all_goals first @@ -651,25 +653,25 @@ def synthStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := The right principle for new call sites is: when the position has a known expected type (`TBool` for conditions, numeric for `decreases`, the declared output for a constant initializer or a functional body), - use `checkStmtExpr`. When it doesn't, use `resolveStmtExpr` (a thin - wrapper that calls `synthStmtExpr` and discards the synthesized type, + use `Check.resolveStmtExpr`. When it doesn't, use `resolveStmtExpr` (a thin + wrapper that calls `Synth.resolveStmtExpr` and discards the synthesized type, used at sites where typing is not enforced — verification annotations, - modifies/reads clauses). `synthStmtExpr` itself is mostly an internal + modifies/reads clauses). `Synth.resolveStmtExpr` itself is mostly an internal interface used by other rules. -/ -def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do +def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do match h_node: exprMd with | AstNode.mk expr source => match h_expr: expr with | .Block stmts label => - checkBlock exprMd stmts label expected source (by rw [h_node]) + Check.block exprMd stmts label expected source (by rw [h_node]) | .IfThenElse cond thenBr elseBr => - checkIfThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) + Check.ifThenElse exprMd cond thenBr elseBr expected source (by rw [h_node]) | .Assign targets value => - checkAssign exprMd targets value expected source (by rw [h_node]) - | .Hole det none => pure (checkHoleNone det expected source) + Check.assign exprMd targets value expected source (by rw [h_node]) + | .Hole det none => pure (Check.holeNone det expected source) | _ => -- Subsumption fallback: synth then check `actual <: expected`. - let (e', actual) ← synthStmtExpr exprMd + let (e', actual) ← Synth.resolveStmtExpr exprMd checkSubtype source expected actual pure e' termination_by (exprMd, 3) @@ -682,26 +684,26 @@ def checkStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtE -- ### Literals /-- Rule **Lit-Int**: `Γ ⊢ LiteralInt n ⇒ TInt`. -/ -def synthLitInt (v : Int) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litInt (v : Int) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralInt v, { val := .TInt, source := source }) /-- Rule **Lit-Bool**: `Γ ⊢ LiteralBool b ⇒ TBool`. -/ -def synthLitBool (v : Bool) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litBool (v : Bool) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralBool v, { val := .TBool, source := source }) /-- Rule **Lit-String**: `Γ ⊢ LiteralString s ⇒ TString`. -/ -def synthLitString (v : String) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litString (v : String) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralString v, { val := .TString, source := source }) /-- Rule **Lit-Decimal**: `Γ ⊢ LiteralDecimal d ⇒ TReal`. -/ -def synthLitDecimal (v : Decimal) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.litDecimal (v : Decimal) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralDecimal v, { val := .TReal, source := source }) -- ### Variables /-- Rule **Var-Local**: `Γ(x) = T ⊢ Var (.Local x) ⇒ T`. Resolves `ref` against the lexical scope and reads its declared type. -/ -def synthVarLocal (ref : Identifier) (source : Option FileRange) : +def Synth.varLocal (ref : Identifier) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ref' ← resolveRef ref source let ty ← getVarType ref @@ -709,7 +711,7 @@ def synthVarLocal (ref : Identifier) (source : Option FileRange) : /-- Rule **Var-Declare**: extends the surrounding scope with `x : T` and synthesizes `TVoid` (the declaration itself produces no value). -/ -def synthVarDeclare (param : Parameter) (source : Option FileRange) : +def Synth.varDeclare (param : Parameter) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') @@ -718,11 +720,11 @@ def synthVarDeclare (param : Parameter) (source : Option FileRange) : /-- Rule **Var-Field**: `Γ ⊢ e ⇒ _, Γ(f) = T_f ⊢ Var (.Field e f) ⇒ T_f`. `f` is looked up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -/ -def synthVarField (exprMd : StmtExprMd) +def Synth.varField (exprMd : StmtExprMd) (target : StmtExprMd) (fieldName : Identifier) (source : Option FileRange) (h : exprMd.val = .Var (.Field target fieldName)) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source let ty ← getVarType fieldName' pure (.Var (.Field target' fieldName'), ty) @@ -751,19 +753,19 @@ def synthVarField (exprMd : StmtExprMd) type and the enclosing context's check (`[⇐] Sub`, or a containing `checkSubtype` like an assignment) surfaces any mismatch downstream against the then-branch's type. -/ -def synthIfThenElse (exprMd : StmtExprMd) +def Synth.ifThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } let voidTy : HighTypeMd := { val := .TVoid, source := exprMd.source } match elseBr with | none => - let thenBr' ← checkStmtExpr thenBr voidTy + let thenBr' ← Check.resolveStmtExpr thenBr voidTy pure (.IfThenElse cond' thenBr' none, voidTy) | some e => - let (thenBr', thenTy) ← synthStmtExpr thenBr - let (elseBr', elseTy) ← synthStmtExpr e + let (thenBr', thenTy) ← Synth.resolveStmtExpr thenBr + let (elseBr', elseTy) ← Synth.resolveStmtExpr e let ctx := (← get).typeContext pure (.IfThenElse cond' thenBr' (some elseBr'), joinTypes ctx thenTy elseTy) termination_by (exprMd, 1) @@ -784,12 +786,12 @@ def synthIfThenElse (exprMd : StmtExprMd) accepted, flagging it belongs to a lint). The last statement's type becomes the block's type, or `TVoid` for an empty block. The block opens a fresh nested scope, so bindings introduced inside don't escape. -/ -def synthBlock (exprMd : StmtExprMd) +def Synth.block (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (h : exprMd.val = .Block stmts label) : ResolveM (StmtExpr × HighTypeMd) := do withScope do - let results ← stmts.mapM synthStmtExpr + let results ← stmts.mapM Synth.resolveStmtExpr let stmts' := results.map (·.1) let lastTy := match results.getLast? with | some (_, ty) => ty @@ -807,17 +809,17 @@ def synthBlock (exprMd : StmtExprMd) `decreases` is resolved without a type check today (the intended target is a numeric type), body is synthesized; the construct itself synthesizes `TVoid`. -/ -def synthWhile (exprMd : StmtExprMd) +def Synth.while (exprMd : StmtExprMd) (cond : StmtExprMd) (invs : List StmtExprMd) (dec : Option StmtExprMd) (body : StmtExprMd) (h : exprMd.val = .While cond invs dec body) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } let invs' ← invs.attach.mapM (fun a => have := a.property; do - checkStmtExpr a.val { val := .TBool, source := a.val.source }) + Check.resolveStmtExpr a.val { val := .TBool, source := a.val.source }) let dec' ← dec.attach.mapM (fun a => have := a.property; do - let (e', _) ← synthStmtExpr a.val; pure e') - let (body', _) ← synthStmtExpr body + let (e', _) ← Synth.resolveStmtExpr a.val; pure e') + let (body', _) ← Synth.resolveStmtExpr body pure (.While cond' invs' dec' body', { val := .TVoid, source := exprMd.source }) termination_by (exprMd, 1) decreasing_by @@ -830,7 +832,7 @@ def synthWhile (exprMd : StmtExprMd) omega /-- Rule **Exit**: `Γ ⊢ Exit target ⇒ TVoid`. -/ -def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.exit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := (.Exit target, { val := .TVoid, source := source }) /-- Rules **Return-None** / **Return-Some** / **Return-Void-Error** / @@ -851,15 +853,15 @@ def synthExit (target : String) (source : Option FileRange) : StmtExpr × HighTy declared output parameters); `return e` syntactically takes a single `Option StmtExpr` and cannot carry multiple values, so it is flagged with a diagnostic pointing users at the named-output convention. -/ -def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) +def Synth.return (exprMd : StmtExprMd) (source : Option FileRange) (val : Option StmtExprMd) (h : exprMd.val = .Return val) : ResolveM (StmtExpr × HighTypeMd) := do let expected := (← get).expectedReturnTypes let val' ← val.attach.mapM (fun a => have := a.property; do match expected with - | some [singleOutput] => checkStmtExpr a.val singleOutput - | _ => let (e', _) ← synthStmtExpr a.val; pure e') + | some [singleOutput] => Check.resolveStmtExpr a.val singleOutput + | _ => let (e', _) ← Synth.resolveStmtExpr a.val; pure e') -- Arity/shape diagnostics independent of the value's own type. match val, expected with | none, some [] => pure () @@ -892,21 +894,21 @@ def synthReturn (exprMd : StmtExprMd) (source : Option FileRange) `Quantifier`. Empty blocks reduce to a subsumption check of `TVoid` against `expected` — the same check `[⇐] Block-Empty` performs when `T` admits `TVoid`. -/ -def checkBlock (exprMd : StmtExprMd) +def Check.block (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Block stmts label) : ResolveM StmtExprMd := do withScope do let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do have : s ∈ stmts := List.dropLast_subset stmts hMem - let (s', _) ← synthStmtExpr s; pure s') + let (s', _) ← Synth.resolveStmtExpr s; pure s') match _lastResult: stmts.getLast? with | none => checkSubtype source expected { val := .TVoid, source := source } pure { val := .Block init' label, source := source } | some last => have := List.mem_of_getLast? _lastResult - let last' ← checkStmtExpr last expected + let last' ← Check.resolveStmtExpr last expected pure { val := .Block (init' ++ [last']) label, source := source } termination_by (exprMd, 0) decreasing_by @@ -924,13 +926,13 @@ def checkBlock (exprMd : StmtExprMd) Without an else branch, the construct can only succeed when `expected` admits `TVoid` — the same subsumption check `[⇐] Block-Empty` performs for an empty block. -/ -def checkIfThenElse (exprMd : StmtExprMd) +def Check.ifThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .IfThenElse cond thenBr elseBr) : ResolveM StmtExprMd := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } - let thenBr' ← checkStmtExpr thenBr expected - let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => checkStmtExpr e expected) + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } + let thenBr' ← Check.resolveStmtExpr thenBr expected + let elseBr' ← elseBr.attach.mapM (fun ⟨e, _⟩ => Check.resolveStmtExpr e expected) if elseBr.isNone then checkSubtype source expected { val := .TVoid, source := source } pure { val := .IfThenElse cond' thenBr' elseBr', source := source } @@ -947,11 +949,11 @@ def checkIfThenElse (exprMd : StmtExprMd) /-- Rule **Assert**: `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ -def synthAssert (exprMd : StmtExprMd) +def Synth.assert (exprMd : StmtExprMd) (condExpr : StmtExprMd) (summary : Option String) (source : Option FileRange) (h : exprMd.val = .Assert ⟨condExpr, summary⟩) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr condExpr { val := .TBool, source := condExpr.source } + let cond' ← Check.resolveStmtExpr condExpr { val := .TBool, source := condExpr.source } pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) termination_by (exprMd, 1) decreasing_by @@ -963,11 +965,11 @@ def synthAssert (exprMd : StmtExprMd) /-- Rule **Assume**: `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ -def synthAssume (exprMd : StmtExprMd) +def Synth.assume (exprMd : StmtExprMd) (cond : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assume cond) : ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← checkStmtExpr cond { val := .TBool, source := cond.source } + let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } pure (.Assume cond', { val := .TVoid, source := source }) termination_by (exprMd, 1) decreasing_by @@ -991,8 +993,8 @@ def synthAssume (exprMd : StmtExprMd) value to assign. The construct synthesizes the RHS's type, so that expression-position assignments like `x ++ (y := s)` see a string in the second operand; statement-position uses are accommodated by - `checkAssign`, which accepts `TVoid` as the expected type. -/ -def synthAssign (exprMd : StmtExprMd) + `Check.assign`, which accepts `TVoid` as the expected type. -/ +def Synth.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : ResolveM (StmtExpr × HighTypeMd) := do @@ -1003,14 +1005,14 @@ def synthAssign (exprMd : StmtExprMd) let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← synthStmtExpr value + let (value', valueTy) ← Synth.resolveStmtExpr value let targetType (t : VariableMd) : ResolveM HighTypeMd := do match t.val with | .Local ref => getVarType ref @@ -1039,7 +1041,7 @@ def synthAssign (exprMd : StmtExprMd) statement of a block in an else-less `if` (whose branch is checked against `TVoid`) without firing a subsumption error against the RHS's type. For non-`TVoid` expected types, falls back to subsumption. -/ -def checkAssign (exprMd : StmtExprMd) +def Check.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : ResolveM StmtExprMd := do @@ -1050,14 +1052,14 @@ def checkAssign (exprMd : StmtExprMd) let ref' ← resolveRef ref source pure (⟨.Local ref', vs⟩ : VariableMd) | .Field target fieldName => - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName source pure (⟨.Field target' fieldName', vs⟩ : VariableMd) | .Declare param => let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← synthStmtExpr value + let (value', valueTy) ← Synth.resolveStmtExpr value let targetType (t : VariableMd) : ResolveM HighTypeMd := do match t.val with | .Local ref => getVarType ref @@ -1089,13 +1091,13 @@ def checkAssign (exprMd : StmtExprMd) constant); each argument is synthesized and checked against the corresponding parameter type. The result type is the (possibly multi-valued) declared output type from `getCallInfo`. -/ -def synthStaticCall (exprMd : StmtExprMd) +def Synth.staticCall (exprMd : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) (h : exprMd.val = .StaticCall callee args) : ResolveM (StmtExpr × HighTypeMd) := do let callee' ← resolveRef callee source (expected := #[.parameter, .staticProcedure, .datatypeConstructor, .constant]) - let results ← args.mapM synthStmtExpr + let results ← args.mapM Synth.resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -1113,15 +1115,15 @@ def synthStaticCall (exprMd : StmtExprMd) /-- Rule **Instance-Call**: target is synthesized; callee resolves to an instance or static procedure; arguments are checked pairwise against the callee's parameter types after dropping `self`. -/ -def synthInstanceCall (exprMd : StmtExprMd) +def Synth.instanceCall (exprMd : StmtExprMd) (target : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) (h : exprMd.val = .InstanceCall target callee args) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let callee' ← resolveRef callee source (expected := #[.instanceProcedure, .staticProcedure]) - let results ← args.mapM synthStmtExpr + let results ← args.mapM Synth.resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let (retTy, paramTypes) ← getCallInfo callee @@ -1154,13 +1156,13 @@ def synthInstanceCall (exprMd : StmtExprMd) `int + real` passes since each operand individually passes `Numeric`; a proper fix needs numeric promotion or unification), `TString` for concatenation. -/ -def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) +def Synth.primitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) (op : Operation) (args : List StmtExprMd) (source : Option FileRange) (h_expr : expr = .PrimitiveOp op args) (h : exprMd.val = .PrimitiveOp op args) : ResolveM (StmtExpr × HighTypeMd) := do let _ := h_expr -- carries the constructor identity for `expr` in diagnostics - let results ← args.mapM synthStmtExpr + let results ← args.mapM Synth.resolveStmtExpr let args' := results.map (·.1) let argTypes := results.map (·.2) let resultTy := match op with @@ -1206,7 +1208,7 @@ def synthPrimitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) /-- Rules **New-Ok** / **New-Fallback**: when `ref` resolves to a composite or datatype, the type is `UserDefined ref`; otherwise `Unknown` (suppresses cascading errors after the kind diagnostic has already fired). -/ -def synthNew (ref : Identifier) (source : Option FileRange) : +def Synth.new (ref : Identifier) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ref' ← resolveRef ref source (expected := #[.compositeType, .datatypeDefinition]) @@ -1223,11 +1225,11 @@ def synthNew (ref : Identifier) (source : Option FileRange) : cast is the user's claim. The synthesized type is `T`. `IsType` is the runtime test counterpart and synthesizes `TBool`. -/ -def synthAsType (exprMd : StmtExprMd) +def Synth.asType (exprMd : StmtExprMd) (target : StmtExprMd) (ty : HighTypeMd) (h : exprMd.val = .AsType target ty) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let ty' ← resolveHighType ty pure (.AsType target' ty', ty') termination_by (exprMd, 1) @@ -1238,11 +1240,11 @@ def synthAsType (exprMd : StmtExprMd) omega /-- Rule **IsType**: `target` is resolved; the synthesized type is `TBool`. -/ -def synthIsType (exprMd : StmtExprMd) +def Synth.isType (exprMd : StmtExprMd) (target : StmtExprMd) (ty : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .IsType target ty) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', _) ← synthStmtExpr target + let (target', _) ← Synth.resolveStmtExpr target let ty' ← resolveHighType ty pure (.IsType target' ty', { val := .TBool, source := source }) termination_by (exprMd, 1) @@ -1259,14 +1261,14 @@ def synthIsType (exprMd : StmtExprMd) user-defined types, while `Cat === Animal` is accepted when `Cat` extends `Animal` (the gradual `Unknown` wildcard makes either side flow freely against the other). -/ -def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) +def Synth.refEq (exprMd : StmtExprMd) (expr : StmtExpr) (lhs rhs : StmtExprMd) (source : Option FileRange) (h_expr : expr = .ReferenceEquals lhs rhs) (h : exprMd.val = .ReferenceEquals lhs rhs) : ResolveM (StmtExpr × HighTypeMd) := do let _ := h_expr - let (lhs', lhsTy) ← synthStmtExpr lhs - let (rhs', rhsTy) ← synthStmtExpr rhs + let (lhs', lhsTy) ← Synth.resolveStmtExpr lhs + let (rhs', rhsTy) ← Synth.resolveStmtExpr rhs let ctx := (← get).typeContext unless isReference ctx lhsTy do typeMismatch lhs'.source (some expr) "expected a reference type" lhsTy @@ -1289,14 +1291,14 @@ def synthRefEq (exprMd : StmtExprMd) (expr : StmtExpr) `T_t` (or the enclosing instance type), and `newVal` checked against the field's declared type. The synthesized type is `T_t` — updating a field on a pure type produces a new value of the same type. -/ -def synthPureFieldUpdate (exprMd : StmtExprMd) +def Synth.pureFieldUpdate (exprMd : StmtExprMd) (target : StmtExprMd) (fieldName : Identifier) (newVal : StmtExprMd) (h : exprMd.val = .PureFieldUpdate target fieldName newVal) : ResolveM (StmtExpr × HighTypeMd) := do - let (target', targetTy) ← synthStmtExpr target + let (target', targetTy) ← Synth.resolveStmtExpr target let fieldName' ← resolveFieldRef target' fieldName target.source let fieldTy ← getVarType fieldName' - let newVal' ← checkStmtExpr newVal fieldTy + let newVal' ← Check.resolveStmtExpr newVal fieldTy pure (.PureFieldUpdate target' fieldName' newVal', targetTy) termination_by (exprMd, 1) decreasing_by @@ -1313,7 +1315,7 @@ def synthPureFieldUpdate (exprMd : StmtExprMd) the body against `TBool` since a quantifier is a proposition. Without that body check, `forall x: int :: x + 1` would be silently accepted. The construct itself synthesizes `TBool`. -/ -def synthQuantifier (exprMd : StmtExprMd) +def Synth.quantifier (exprMd : StmtExprMd) (mode : QuantifierMode) (param : Parameter) (trigger : Option StmtExprMd) (body : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Quantifier mode param trigger body) : @@ -1322,8 +1324,8 @@ def synthQuantifier (exprMd : StmtExprMd) let paramTy' ← resolveHighType param.type let paramName' ← defineNameCheckDup param.name (.quantifierVar param.name paramTy') let trigger' ← trigger.attach.mapM (fun pv => have := pv.property; do - let (e', _) ← synthStmtExpr pv.val; pure e') - let body' ← checkStmtExpr body { val := .TBool, source := body.source } + let (e', _) ← Synth.resolveStmtExpr pv.val; pure e') + let body' ← Check.resolveStmtExpr body { val := .TBool, source := body.source } pure (.Quantifier mode ⟨paramName', paramTy'⟩ trigger' body', { val := .TBool, source := source }) termination_by (exprMd, 1) decreasing_by @@ -1336,11 +1338,11 @@ def synthQuantifier (exprMd : StmtExprMd) /-- Rule **Assigned**: `name` is synthesized; the construct synthesizes `TBool`. -/ -def synthAssigned (exprMd : StmtExprMd) +def Synth.assigned (exprMd : StmtExprMd) (name : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assigned name) : ResolveM (StmtExpr × HighTypeMd) := do - let (name', _) ← synthStmtExpr name + let (name', _) ← Synth.resolveStmtExpr name pure (.Assigned name', { val := .TBool, source := source }) termination_by (exprMd, 1) decreasing_by @@ -1350,11 +1352,11 @@ def synthAssigned (exprMd : StmtExprMd) omega /-- Rule **Old**: `Γ ⊢ v ⇒ T ⊢ Old v ⇒ T`. -/ -def synthOld (exprMd : StmtExprMd) +def Synth.old (exprMd : StmtExprMd) (val : StmtExprMd) (h : exprMd.val = .Old val) : ResolveM (StmtExpr × HighTypeMd) := do - let (val', valTy) ← synthStmtExpr val + let (val', valTy) ← Synth.resolveStmtExpr val pure (.Old val', valTy) termination_by (exprMd, 1) decreasing_by @@ -1367,13 +1369,13 @@ def synthOld (exprMd : StmtExprMd) (`UserDefined` or `Unknown`) — `Fresh` only makes sense on heap-allocated references, so `fresh(5)` is rejected. The construct itself synthesizes `TBool`. -/ -def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) +def Synth.fresh (exprMd : StmtExprMd) (expr : StmtExpr) (val : StmtExprMd) (source : Option FileRange) (h_expr : expr = .Fresh val) (h : exprMd.val = .Fresh val) : ResolveM (StmtExpr × HighTypeMd) := do let _ := h_expr - let (val', valTy) ← synthStmtExpr val + let (val', valTy) ← Synth.resolveStmtExpr val unless isReference (← get).typeContext valTy do typeMismatch val'.source (some expr) "expected a reference type" valTy pure (.Fresh val', { val := .TBool, source := source }) @@ -1386,12 +1388,12 @@ def synthFresh (exprMd : StmtExprMd) (expr : StmtExpr) /-- Rule **ProveBy**: `v` and `proof` are both synthesized; the construct's type is `v`'s type — `proof` is a hint for downstream verification. -/ -def synthProveBy (exprMd : StmtExprMd) +def Synth.proveBy (exprMd : StmtExprMd) (val proof : StmtExprMd) (h : exprMd.val = .ProveBy val proof) : ResolveM (StmtExpr × HighTypeMd) := do - let (val', valTy) ← synthStmtExpr val - let (proof', _) ← synthStmtExpr proof + let (val', valTy) ← Synth.resolveStmtExpr val + let (proof', _) ← Synth.resolveStmtExpr proof pure (.ProveBy val' proof', valTy) termination_by (exprMd, 1) decreasing_by @@ -1411,7 +1413,7 @@ def synthProveBy (exprMd : StmtExprMd) wildcarded through `Unknown`. Otherwise an error is emitted ("'this' is not allowed outside instance methods") and the type collapses to `Unknown` to suppress cascading errors. -/ -def synthThis (source : Option FileRange) : +def Synth.this (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let s ← get match s.instanceTypeName with @@ -1429,11 +1431,11 @@ def synthThis (source : Option FileRange) : -- ### Untyped forms /-- Rule **Abstract**: synthesizes `Unknown`. -/ -def synthAbstract (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.abstract (source : Option FileRange) : StmtExpr × HighTypeMd := (.Abstract, { val := .Unknown, source := source }) /-- Rule **All**: synthesizes `Unknown`. -/ -def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := +def Synth.all (source : Option FileRange) : StmtExpr × HighTypeMd := (.All, { val := .Unknown, source := source }) -- ### ContractOf @@ -1458,11 +1460,11 @@ def synthAll (source : Option FileRange) : StmtExpr × HighTypeMd := `contractOf` production today, and the translator emits "not yet implemented" for it. The typing rule exists so resolution remains exhaustive over `StmtExpr`. -/ -def synthContractOf (exprMd : StmtExprMd) +def Synth.contractOf (exprMd : StmtExprMd) (ty : ContractType) (fn : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .ContractOf ty fn) : ResolveM (StmtExpr × HighTypeMd) := do - let (fn', _) ← synthStmtExpr fn + let (fn', _) ← Synth.resolveStmtExpr fn let s ← get let fnIsProcRef : Bool := match fn'.val with | .Var (.Local ref) => @@ -1492,7 +1494,7 @@ def synthContractOf (exprMd : StmtExprMd) /-- Rules **Hole-Some** / **Hole-None-Synth**: a typed hole synthesizes its annotation; an untyped hole in synth position synthesizes `Unknown`. -/ -def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : +def Synth.hole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do match type with | some ty => @@ -1514,16 +1516,19 @@ def synthHole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange the diagnostic *"hole annotated with 'T_resolution' but context expects 'T_inference'"*, surfacing what would otherwise be a silent overwrite. -/ -def checkHoleNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : +def Check.holeNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : StmtExprMd := { val := .Hole det (some expected), source := source } -end +end -- mutual +end Resolution + +open Resolution /-- Resolve a statement expression, discarding the synthesized type. Use when only the resolved expression is needed (invariants, decreases, etc.). -/ private def resolveStmtExpr (e : StmtExprMd) : ResolveM StmtExprMd := do - let (e', _) ← synthStmtExpr e; pure e' + let (e', _) ← Synth.resolveStmtExpr e; pure e' /-- Resolve a parameter: assign a fresh ID and add to scope. -/ def resolveParameter (param : Parameter) : ResolveM Parameter := do @@ -1535,7 +1540,7 @@ def resolveParameter (param : Parameter) : ResolveM Parameter := do def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do match body with | .Transparent b => - let (b', ty) ← synthStmtExpr b + let (b', ty) ← Synth.resolveStmtExpr b return (.Transparent b', ty) | .Opaque posts impl mods => let posts' ← posts.mapM (·.mapM resolveStmtExpr) @@ -1656,8 +1661,8 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do -- in scope when resolving the constraint and witness expressions. let (valueName', constraint', witness') ← withScope do let valueName' ← defineNameCheckDup ct.valueName (.quantifierVar ct.valueName base') - let (constraint', _) ← synthStmtExpr ct.constraint - let (witness', _) ← synthStmtExpr ct.witness + let (constraint', _) ← Synth.resolveStmtExpr ct.constraint + let (witness', _) ← Synth.resolveStmtExpr ct.witness return (valueName', constraint', witness') return .Constrained { name := ctName', base := base', valueName := valueName', constraint := constraint', witness := witness' } @@ -1683,7 +1688,7 @@ def resolveTypeDefinition (td : TypeDefinition) : ResolveM TypeDefinition := do /-- Resolve a constant definition. -/ def resolveConstant (c : Constant) : ResolveM Constant := do let ty' ← resolveHighType c.type - let init' ← c.initializer.mapM (checkStmtExpr · ty') + let init' ← c.initializer.mapM (Check.resolveStmtExpr · ty') let name' ← resolveRef c.name return { name := name', type := ty', initializer := init' } diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 7583d4d079..b9d1070f42 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -160,8 +160,8 @@ mismatches against the surrounding context become diagnostics. The implementatio There are two operations on expressions, written here in standard bidirectional notation: ``` -Γ ⊢ e ⇒ T -- "e synthesizes T" (synthStmtExpr) -Γ ⊢ e ⇐ T -- "e checks against T" (checkStmtExpr) +Γ ⊢ e ⇒ T -- "e synthesizes T" (Synth.resolveStmtExpr) +Γ ⊢ e ⇐ T -- "e checks against T" (Check.resolveStmtExpr) ``` Synthesis returns a type inferred from the expression itself; checking verifies that the @@ -172,12 +172,12 @@ by a single change-of-direction rule, *subsumption*: $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` The two judgments are implemented as -{name Strata.Laurel.synthStmtExpr}`synthStmtExpr` and -{name Strata.Laurel.checkStmtExpr}`checkStmtExpr`: +{name Strata.Laurel.Resolution.Synth.resolveStmtExpr}`Synth.resolveStmtExpr` and +{name Strata.Laurel.Resolution.Check.resolveStmtExpr}`Check.resolveStmtExpr`: -{docstring Strata.Laurel.synthStmtExpr} +{docstring Strata.Laurel.Resolution.Synth.resolveStmtExpr} -{docstring Strata.Laurel.checkStmtExpr} +{docstring Strata.Laurel.Resolution.Check.resolveStmtExpr} ### Gradual typing @@ -238,43 +238,43 @@ Each LaTeX rule below is followed by the docstring of the helper that implements $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` -Fallback in {name Strata.Laurel.checkStmtExpr}`checkStmtExpr` whenever no bespoke check +Fallback in {name Strata.Laurel.Resolution.Check.resolveStmtExpr}`Check.resolveStmtExpr` whenever no bespoke check rule applies. ### Literals $$`\frac{}{\Gamma \vdash \mathsf{LiteralInt}\;n \Rightarrow \mathsf{TInt}} \quad \text{([⇒] Lit-Int)}` -{docstring Strata.Laurel.synthLitInt} +{docstring Strata.Laurel.Resolution.Synth.litInt} $$`\frac{}{\Gamma \vdash \mathsf{LiteralBool}\;b \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Lit-Bool)}` -{docstring Strata.Laurel.synthLitBool} +{docstring Strata.Laurel.Resolution.Synth.litBool} $$`\frac{}{\Gamma \vdash \mathsf{LiteralString}\;s \Rightarrow \mathsf{TString}} \quad \text{([⇒] Lit-String)}` -{docstring Strata.Laurel.synthLitString} +{docstring Strata.Laurel.Resolution.Synth.litString} $$`\frac{}{\Gamma \vdash \mathsf{LiteralDecimal}\;d \Rightarrow \mathsf{TReal}} \quad \text{([⇒] Lit-Decimal)}` -{docstring Strata.Laurel.synthLitDecimal} +{docstring Strata.Laurel.Resolution.Synth.litDecimal} ### Variables $$`\frac{\Gamma(x) = T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Local}\;x) \Rightarrow T} \quad \text{([⇒] Var-Local)}` -{docstring Strata.Laurel.synthVarLocal} +{docstring Strata.Laurel.Resolution.Synth.varLocal} $$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Field}\;e\;f) \Rightarrow T_f} \quad \text{([⇒] Var-Field)}` -{docstring Strata.Laurel.synthVarField} +{docstring Strata.Laurel.Resolution.Synth.varField} $$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` `⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the remainder of the enclosing scope. -{docstring Strata.Laurel.synthVarDeclare} +{docstring Strata.Laurel.Resolution.Synth.varDeclare} ### Control flow @@ -282,13 +282,13 @@ $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vda $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If)}` -{docstring Strata.Laurel.synthIfThenElse} +{docstring Strata.Laurel.Resolution.Synth.ifThenElse} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}` -{docstring Strata.Laurel.checkIfThenElse} +{docstring Strata.Laurel.Resolution.Check.ifThenElse} $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` @@ -299,17 +299,17 @@ block. $$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` -{docstring Strata.Laurel.synthBlock} +{docstring Strata.Laurel.Resolution.Synth.block} $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` $$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` -{docstring Strata.Laurel.checkBlock} +{docstring Strata.Laurel.Resolution.Check.block} $$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` -{docstring Strata.Laurel.synthExit} +{docstring Strata.Laurel.Resolution.Synth.exit} $$`\frac{}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None)}` @@ -319,21 +319,21 @@ $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Ret $$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}` -{docstring Strata.Laurel.synthReturn} +{docstring Strata.Laurel.Resolution.Synth.return} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Rightarrow \_}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}` -{docstring Strata.Laurel.synthWhile} +{docstring Strata.Laurel.Resolution.Synth.while} ### Verification statements $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}` -{docstring Strata.Laurel.synthAssert} +{docstring Strata.Laurel.Resolution.Synth.assert} $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}` -{docstring Strata.Laurel.synthAssume} +{docstring Strata.Laurel.Resolution.Synth.assume} ### Assignment @@ -344,9 +344,9 @@ The target's declared type `T_i` comes from the variable's scope entry (for {name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. -{docstring Strata.Laurel.synthAssign} +{docstring Strata.Laurel.Resolution.Synth.assign} -{docstring Strata.Laurel.checkAssign} +{docstring Strata.Laurel.Resolution.Check.assign} ### Calls @@ -354,11 +354,11 @@ $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text $$`\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T_1; \ldots; T_n],\; n \ne 1 \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{StaticCall}\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Static-Call-Multi)}` -{docstring Strata.Laurel.synthStaticCall} +{docstring Strata.Laurel.Resolution.Synth.staticCall} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_ \quad \Gamma(\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and outputs } [T] \quad \Gamma \vdash \mathit{args} \Rightarrow Us \quad U_i <: T_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{InstanceCall}\;\mathit{target}\;\mathit{callee}\;\mathit{args} \Rightarrow T} \quad \text{([⇒] Instance-Call)}` -{docstring Strata.Laurel.synthInstanceCall} +{docstring Strata.Laurel.Resolution.Synth.instanceCall} ### Primitive operations @@ -376,7 +376,7 @@ $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathit{Numeric} \quad \Gamma $$`\frac{\Gamma \vdash \mathit{args}_i \Leftarrow \mathsf{TString} \quad \mathit{op} = \mathsf{StrConcat}}{\Gamma \vdash \mathsf{PrimitiveOp}\;\mathit{op}\;\mathit{args} \Rightarrow \mathsf{TString}} \quad \text{([⇒] Op-Concat)}` -{docstring Strata.Laurel.synthPrimitiveOp} +{docstring Strata.Laurel.Resolution.Synth.primitiveOp} ### Object forms @@ -384,15 +384,15 @@ $$`\frac{\Gamma(\mathit{ref}) \text{ is a composite or datatype } T}{\Gamma \vda $$`\frac{\Gamma(\mathit{ref}) \text{ is not a composite or datatype}}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] New-Fallback)}` -{docstring Strata.Laurel.synthNew} +{docstring Strata.Laurel.Resolution.Synth.new} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{AsType}\;\mathit{target}\;T \Rightarrow T} \quad \text{([⇒] AsType)}` -{docstring Strata.Laurel.synthAsType} +{docstring Strata.Laurel.Resolution.Synth.asType} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow \_}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}` -{docstring Strata.Laurel.synthIsType} +{docstring Strata.Laurel.Resolution.Synth.isType} $$`\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r \quad T_l \sim T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}` @@ -401,33 +401,33 @@ or {name Strata.Laurel.HighType.Unknown}`Unknown` type. `~` is the consistency r {name Strata.Laurel.isConsistent}`isConsistent` — symmetric, with the {name Strata.Laurel.HighType.Unknown}`Unknown` wildcard. -{docstring Strata.Laurel.synthRefEq} +{docstring Strata.Laurel.Resolution.Synth.refEq} $$`\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}` -{docstring Strata.Laurel.synthPureFieldUpdate} +{docstring Strata.Laurel.Resolution.Synth.pureFieldUpdate} ### Verification expressions $$`\frac{\Gamma, x : T \vdash \mathit{body} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Quantifier}\;\mathit{mode}\;\langle x, T\rangle\;\mathit{trig}\;\mathit{body} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Quantifier)}` -{docstring Strata.Laurel.synthQuantifier} +{docstring Strata.Laurel.Resolution.Synth.quantifier} $$`\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assigned}\;\mathit{name} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Assigned)}` -{docstring Strata.Laurel.synthAssigned} +{docstring Strata.Laurel.Resolution.Synth.assigned} $$`\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old)}` -{docstring Strata.Laurel.synthOld} +{docstring Strata.Laurel.Resolution.Synth.old} $$`\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}` -{docstring Strata.Laurel.synthFresh} +{docstring Strata.Laurel.Resolution.Synth.fresh} $$`\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy)}` -{docstring Strata.Laurel.synthProveBy} +{docstring Strata.Laurel.Resolution.Synth.proveBy} ### Self reference @@ -435,15 +435,15 @@ $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{some}\;T}{\Gamma \vdash \mat $$`\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{none}}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{Unknown}\;\;[\text{emits “‘this’ is not allowed outside instance methods”}]} \quad \text{([⇒] This-Outside)}` -{docstring Strata.Laurel.synthThis} +{docstring Strata.Laurel.Resolution.Synth.this} ### Untyped forms $$`\frac{}{\Gamma \vdash \mathsf{Abstract}\;/\;\mathsf{All}\;\ldots \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Abstract / All)}` -{docstring Strata.Laurel.synthAbstract} +{docstring Strata.Laurel.Resolution.Synth.abstract} -{docstring Strata.Laurel.synthAll} +{docstring Strata.Laurel.Resolution.Synth.all} ### ContractOf @@ -453,7 +453,7 @@ $$`\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma $$`\frac{\mathit{fn} \text{ is not a procedure reference}}{\Gamma \vdash \mathsf{ContractOf}\;\ldots\;\mathit{fn} \rightsquigarrow \text{error: “‘contractOf’ expected a procedure reference”}} \quad \text{([⇒] ContractOf-Error)}` -{docstring Strata.Laurel.synthContractOf} +{docstring Strata.Laurel.Resolution.Synth.contractOf} ### Holes @@ -461,11 +461,11 @@ $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T) \Rightarrow T} \qu $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-None)}` -{docstring Strata.Laurel.synthHole} +{docstring Strata.Laurel.Resolution.Synth.hole} $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapsto\;\; \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}` -{docstring Strata.Laurel.checkHoleNone} +{docstring Strata.Laurel.Resolution.Check.holeNone} ## Future structural changes @@ -504,7 +504,7 @@ just wasted work and a maintenance hazard. `InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that \[⇐\] Hole-None writes the expected type during resolution for holes in check-mode positions, the post-pass only needs to handle holes in synth-only positions (e.g. call -arguments resolved through `synthStmtExpr` instead of `checkStmtExpr`). As more constructs +arguments resolved through `Synth.resolveStmtExpr` instead of `Check.resolveStmtExpr`). As more constructs gain bespoke check rules, fewer holes will reach `InferHoleTypes`; eventually the pass can be deleted entirely. From 5826fffe3f98a17d850c93e25c1e45670cd1402d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 15:32:18 -0400 Subject: [PATCH 115/128] cleanup rules presentation --- Strata/Languages/Laurel/Resolution.lean | 307 ++++++++++++++++-------- docs/verso/LaurelDoc.lean | 15 +- 2 files changed, 216 insertions(+), 106 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 080142d15c..cb72db5599 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -683,41 +683,46 @@ def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : Resolv -- ### Literals -/-- Rule **Lit-Int**: `Γ ⊢ LiteralInt n ⇒ TInt`. -/ +/-- `Γ ⊢ LiteralInt n ⇒ TInt` -/ def Synth.litInt (v : Int) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralInt v, { val := .TInt, source := source }) -/-- Rule **Lit-Bool**: `Γ ⊢ LiteralBool b ⇒ TBool`. -/ +/-- `Γ ⊢ LiteralBool b ⇒ TBool` -/ def Synth.litBool (v : Bool) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralBool v, { val := .TBool, source := source }) -/-- Rule **Lit-String**: `Γ ⊢ LiteralString s ⇒ TString`. -/ +/-- `Γ ⊢ LiteralString s ⇒ TString` -/ def Synth.litString (v : String) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralString v, { val := .TString, source := source }) -/-- Rule **Lit-Decimal**: `Γ ⊢ LiteralDecimal d ⇒ TReal`. -/ +/-- `Γ ⊢ LiteralDecimal d ⇒ TReal` -/ def Synth.litDecimal (v : Decimal) (source : Option FileRange) : StmtExpr × HighTypeMd := (.LiteralDecimal v, { val := .TReal, source := source }) -- ### Variables -/-- Rule **Var-Local**: `Γ(x) = T ⊢ Var (.Local x) ⇒ T`. Resolves `ref` against - the lexical scope and reads its declared type. -/ +/-- `Γ(x) = T ∴ Γ ⊢ Var (.Local x) ⇒ T` + + Resolves `ref` against the lexical scope and reads its declared type. -/ def Synth.varLocal (ref : Identifier) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ref' ← resolveRef ref source let ty ← getVarType ref pure (.Var (.Local ref'), ty) -/-- Rule **Var-Declare**: extends the surrounding scope with `x : T` and - synthesizes `TVoid` (the declaration itself produces no value). -/ +/-- `x ∉ dom(Γ) ∴ Γ ⊢ Var (.Declare ⟨x, T⟩) ⇒ TVoid ⊣ Γ, x : T` + + `⊣ Γ, x : T` records that the surrounding scope is extended with the + new binding for the remainder of the enclosing scope. The declaration + itself produces no value, hence `TVoid`. -/ def Synth.varDeclare (param : Parameter) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) -/-- Rule **Var-Field**: `Γ ⊢ e ⇒ _, Γ(f) = T_f ⊢ Var (.Field e f) ⇒ T_f`. +/-- `Γ ⊢ e ⇒ _, Γ(f) = T_f ∴ Γ ⊢ Var (.Field e f) ⇒ T_f` + `f` is looked up against the type of `e` (or the enclosing instance type for `self.f`); the typing rule itself is path-agnostic. -/ def Synth.varField (exprMd : StmtExprMd) @@ -738,7 +743,15 @@ def Synth.varField (exprMd : StmtExprMd) -- ### Control flow -/-- Rules **If-NoElse** / **If-Synth**: `cond` is checked against `TBool`. +/-- When there is an else branch: + + `Γ ⊢ cond ⇐ TBool, Γ ⊢ thenBr ⇒ T_t, Γ ⊢ elseBr ⇒ T_e ∴ Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t ⊔ T_e` + + Otherwise: + + `Γ ⊢ cond ⇐ TBool, Γ ⊢ thenBr ⇐ TVoid ∴ Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid` + + `cond` is checked against `TBool`. With no else branch, the construct is a statement — `thenBr` is checked against `TVoid` and the result is `TVoid`, so `x : int := if c then 5` is rejected at the branch rather than slipping through to a downstream @@ -778,7 +791,13 @@ def Synth.ifThenElse (exprMd : StmtExprMd) try omega) | (apply Prod.Lex.right; decide) -/-- Rules **Block-Synth** / **Block-Synth-Empty**: each statement is resolved +/-- Cases on whether the statement list is empty. + + `Γ_0 = Γ, Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n), Γ_{n-1} ⊢ s_n ⇒ T ∴ Γ ⊢ Block [s_1; …; s_n] label ⇒ T` + + `Γ ⊢ Block [] label ⇒ TVoid` + + Each statement is resolved in the scope produced by its predecessor and may itself extend it (`Var (.Declare …)` does); non-last statements are synthesized but their types discarded (the lax rule, matching Java/Python/JS where `f(x);` is @@ -805,10 +824,11 @@ def Synth.block (exprMd : StmtExprMd) have := List.sizeOf_lt_of_mem ‹_ ∈ stmts› omega -/-- Rule **While**: `cond ⇐ TBool`, each invariant `⇐ TBool`, optional - `decreases` is resolved without a type check today (the intended target - is a numeric type), body is synthesized; the construct itself - synthesizes `TVoid`. -/ +/-- `Γ ⊢ cond ⇐ TBool, Γ ⊢ invs_i ⇐ TBool, Γ ⊢ dec ⇐ ?, Γ ⊢ body ⇒ _ ∴ Γ ⊢ While cond invs dec body ⇒ TVoid` + + `cond ⇐ TBool`, each invariant `⇐ TBool`, optional `decreases` is + resolved without a type check today (the intended target is a numeric + type), body is synthesized; the construct itself synthesizes `TVoid`. -/ def Synth.while (exprMd : StmtExprMd) (cond : StmtExprMd) (invs : List StmtExprMd) (dec : Option StmtExprMd) (body : StmtExprMd) @@ -831,12 +851,22 @@ def Synth.while (exprMd : StmtExprMd) try simp_all omega -/-- Rule **Exit**: `Γ ⊢ Exit target ⇒ TVoid`. -/ +/-- `Γ ⊢ Exit target ⇒ TVoid` -/ def Synth.exit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := (.Exit target, { val := .TVoid, source := source }) -/-- Rules **Return-None** / **Return-Some** / **Return-Void-Error** / - **Return-Multi-Error**: matches the optional return value against the +/-- Cases on whether the return value is `none` or `some e`, and on the + arity of the enclosing procedure's declared outputs. + + `Γ ⊢ Return none ⇒ TVoid` + + `Γ_proc.outputs = [T], Γ ⊢ e ⇐ T ∴ Γ ⊢ Return (some e) ⇒ TVoid` + + `Γ_proc.outputs = [] ∴ Γ ⊢ Return (some e) ↝ error: "void procedure cannot return a value"` + + `Γ_proc.outputs = [T_1; …; T_n] (n ≥ 2) ∴ Γ ⊢ Return (some e) ↝ error: "multi-output procedure cannot use 'return e'; assign to named outputs instead"` + + Matches the optional return value against the enclosing procedure's declared outputs. The expected output types are threaded through `ResolveState.expectedReturnTypes`, set from `proc.outputs` by `resolveProcedure` / `resolveInstanceProcedure` for @@ -887,13 +917,18 @@ def Synth.return (exprMd : StmtExprMd) (source : Option FileRange) simp_all omega -/-- Rules **Block-Check** / **Block-Check-Empty**: pushes `expected` into the - *last* statement rather than comparing the block's synthesized type at the - boundary. Errors fire at the offending subexpression, and `expected` - keeps propagating through nested `Block` / `IfThenElse` / `Hole` / - `Quantifier`. Empty blocks reduce to a subsumption check of `TVoid` - against `expected` — the same check `[⇐] Block-Empty` performs when - `T` admits `TVoid`. -/ +/-- Cases on whether the statement list is empty. + + `Γ_0 = Γ, Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n), Γ_{n-1} ⊢ s_n ⇐ T ∴ Γ ⊢ Block [s_1; …; s_n] label ⇐ T` + + `TVoid <: T ∴ Γ ⊢ Block [] label ⇐ T` + + Pushes `expected` into the *last* statement rather than comparing the + block's synthesized type at the boundary. Errors fire at the offending + subexpression, and `expected` keeps propagating through nested `Block` + / `IfThenElse` / `Hole` / `Quantifier`. Empty blocks reduce to a + subsumption check of `TVoid` against `expected` — the same check + `[⇐] Block-Empty` performs when `T` admits `TVoid`. -/ def Check.block (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (expected : HighTypeMd) (source : Option FileRange) @@ -920,12 +955,19 @@ def Check.block (exprMd : StmtExprMd) try simp_all omega -/-- Rules **If-Check** / **If-Check-NoElse**: pushes `expected` into both - branches (rather than going through If-Synth + Sub at the boundary). - Errors fire at the offending branch instead of the surrounding `if`. - Without an else branch, the construct can only succeed when `expected` - admits `TVoid` — the same subsumption check `[⇐] Block-Empty` performs - for an empty block. -/ +/-- When there is an else branch: + + `Γ ⊢ cond ⇐ TBool, Γ ⊢ thenBr ⇐ T, Γ ⊢ elseBr ⇐ T ∴ Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T` + + Otherwise: + + `Γ ⊢ cond ⇐ TBool, Γ ⊢ thenBr ⇐ T, TVoid <: T ∴ Γ ⊢ IfThenElse cond thenBr none ⇐ T` + + Pushes `expected` into both branches (rather than going through + If-Synth + Sub at the boundary). Errors fire at the offending branch + instead of the surrounding `if`. Without an else branch, the construct + can only succeed when `expected` admits `TVoid` — the same subsumption + check `[⇐] Block-Empty` performs for an empty block. -/ def Check.ifThenElse (exprMd : StmtExprMd) (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) @@ -947,8 +989,9 @@ def Check.ifThenElse (exprMd : StmtExprMd) -- ### Verification statements -/-- Rule **Assert**: `cond` is checked against `TBool`; the construct - synthesizes `TVoid`. -/ +/-- `Γ ⊢ cond ⇐ TBool ∴ Γ ⊢ Assert cond ⇒ TVoid` + + `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ def Synth.assert (exprMd : StmtExprMd) (condExpr : StmtExprMd) (summary : Option String) (source : Option FileRange) (h : exprMd.val = .Assert ⟨condExpr, summary⟩) : @@ -963,8 +1006,9 @@ def Synth.assert (exprMd : StmtExprMd) try simp_all omega -/-- Rule **Assume**: `cond` is checked against `TBool`; the construct - synthesizes `TVoid`. -/ +/-- `Γ ⊢ cond ⇐ TBool ∴ Γ ⊢ Assume cond ⇒ TVoid` + + `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ def Synth.assume (exprMd : StmtExprMd) (cond : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assume cond) : @@ -981,7 +1025,11 @@ def Synth.assume (exprMd : StmtExprMd) -- ### Assignment -/-- Rule **Assign**: each target's declared type `T_i` (from `Local`, +/-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇒ T_e, T_e <: ExpectedTy ∴ Γ ⊢ Assign targets e ⇒ TVoid` + + where `ExpectedTy = T_1` if `|targets| = 1`, else `MultiValuedExpr [T_1; …; T_n]`. + + Each target's declared type `T_i` (from `Local`, `Field`, or fresh `Declare`) is collapsed into a tuple `ExpectedTy` (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) and checked against the RHS's synthesized type. Both single- and @@ -1035,12 +1083,19 @@ def Synth.assign (exprMd : StmtExprMd) try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) omega -/-- Rule **Assign-Check**: an assignment in statement position (checked - against `TVoid`) discards its RHS value, so the synthesized type is not - compared against `expected`. This lets `b := 1` appear as the last - statement of a block in an else-less `if` (whose branch is checked - against `TVoid`) without firing a subsumption error against the RHS's - type. For non-`TVoid` expected types, falls back to subsumption. -/ +/-- Cases on whether `expected` is `TVoid` (statement position) or some + other type (expression position). + + `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇒ T_e, T_e <: ExpectedTy ∴ Γ ⊢ Assign targets e ⇐ TVoid` + + `Γ ⊢ Assign targets e ⇒ T_e, T_e <: T ∴ Γ ⊢ Assign targets e ⇐ T (T ≠ TVoid)` + + An assignment in statement position (checked against `TVoid`) discards + its RHS value, so the synthesized type is not compared against + `expected`. This lets `b := 1` appear as the last statement of a block + in an else-less `if` (whose branch is checked against `TVoid`) without + firing a subsumption error against the RHS's type. For non-`TVoid` + expected types, falls back to subsumption. -/ def Check.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) @@ -1086,11 +1141,17 @@ def Check.assign (exprMd : StmtExprMd) -- ### Calls -/-- Rules **Static-Call** / **Static-Call-Multi**: callee is resolved against - the expected kinds (parameter, static procedure, datatype constructor, - constant); each argument is synthesized and checked against the - corresponding parameter type. The result type is the (possibly - multi-valued) declared output type from `getCallInfo`. -/ +/-- Cases on the arity of the callee's declared outputs. + + `Γ(callee) = static-procedure with inputs Ts and outputs [T], Γ ⊢ args ⇒ Us, U_i <: T_i (pairwise) ∴ Γ ⊢ StaticCall callee args ⇒ T` + + `Γ(callee) = static-procedure with inputs Ts and outputs [T_1; …; T_n] (n ≠ 1), Γ ⊢ args ⇒ Us, U_i <: T_i (pairwise) ∴ Γ ⊢ StaticCall callee args ⇒ MultiValuedExpr [T_1; …; T_n]` + + Callee is resolved against the expected kinds (parameter, static + procedure, datatype constructor, constant); each argument is + synthesized and checked against the corresponding parameter type. The + result type is the (possibly multi-valued) declared output type from + `getCallInfo`. -/ def Synth.staticCall (exprMd : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) (h : exprMd.val = .StaticCall callee args) : @@ -1112,9 +1173,11 @@ def Synth.staticCall (exprMd : StmtExprMd) have := List.sizeOf_lt_of_mem ‹_ ∈ args› omega -/-- Rule **Instance-Call**: target is synthesized; callee resolves to an - instance or static procedure; arguments are checked pairwise against the - callee's parameter types after dropping `self`. -/ +/-- `Γ ⊢ target ⇒ _, Γ(callee) = instance-procedure with inputs [self; Ts] and outputs [T], Γ ⊢ args ⇒ Us, U_i <: T_i (pairwise; self dropped) ∴ Γ ⊢ InstanceCall target callee args ⇒ T` + + Target is synthesized; callee resolves to an instance or static + procedure; arguments are checked pairwise against the callee's + parameter types after dropping `self`. -/ def Synth.instanceCall (exprMd : StmtExprMd) (target : StmtExprMd) (callee : Identifier) (args : List StmtExprMd) (source : Option FileRange) @@ -1143,8 +1206,19 @@ def Synth.instanceCall (exprMd : StmtExprMd) -- ### Primitive operations -/-- Rules **Op-Bool** / **Op-Cmp** / **Op-Eq** / **Op-Arith** / **Op-Concat**: - each operator family has its own argument-type discipline and result +/-- Cases on the operator family. + + `Γ ⊢ args_i ⇐ TBool, op ∈ {And, Or, AndThen, OrElse, Not, Implies} ∴ Γ ⊢ PrimitiveOp op args ⇒ TBool` + + `Γ ⊢ args_i ⇐ Numeric, op ∈ {Lt, Leq, Gt, Geq} ∴ Γ ⊢ PrimitiveOp op args ⇒ TBool` + + `Γ ⊢ lhs ⇒ T_l, Γ ⊢ rhs ⇒ T_r, T_l ~ T_r, op ∈ {Eq, Neq} ∴ Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool` + + `Γ ⊢ args_i ⇐ Numeric, Γ ⊢ args.head ⇒ T, op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} ∴ Γ ⊢ PrimitiveOp op args ⇒ T` + + `Γ ⊢ args_i ⇐ TString, op = StrConcat ∴ Γ ⊢ PrimitiveOp op args ⇒ TString` + + Each operator family has its own argument-type discipline and result type. Arguments are synthesized first, then the per-family check fires: `⇐ TBool` for booleans, `Numeric` (consistent with `TInt`, `TReal`, or `TFloat64`) for arithmetic/comparison, consistency `~` for equality @@ -1205,9 +1279,15 @@ def Synth.primitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) -- ### Object forms -/-- Rules **New-Ok** / **New-Fallback**: when `ref` resolves to a composite or - datatype, the type is `UserDefined ref`; otherwise `Unknown` (suppresses - cascading errors after the kind diagnostic has already fired). -/ +/-- Cases on whether `ref` resolves to a composite/datatype. + + `Γ(ref) is a composite or datatype T ∴ Γ ⊢ New ref ⇒ UserDefined T` + + `Γ(ref) is not a composite or datatype ∴ Γ ⊢ New ref ⇒ Unknown` + + When `ref` resolves to a composite or datatype, the type is + `UserDefined ref`; otherwise `Unknown` (suppresses cascading errors + after the kind diagnostic has already fired). -/ def Synth.new (ref : Identifier) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let ref' ← resolveRef ref source @@ -1221,8 +1301,10 @@ def Synth.new (ref : Identifier) (source : Option FileRange) : else { val := HighType.Unknown, source := source } pure (.New ref', ty) -/-- Rule **AsType**: `target` is resolved but not checked against `T` — the - cast is the user's claim. The synthesized type is `T`. +/-- `Γ ⊢ target ⇒ _ ∴ Γ ⊢ AsType target T ⇒ T` + + `target` is resolved but not checked against `T` — the cast is the + user's claim. The synthesized type is `T`. `IsType` is the runtime test counterpart and synthesizes `TBool`. -/ def Synth.asType (exprMd : StmtExprMd) @@ -1239,7 +1321,9 @@ def Synth.asType (exprMd : StmtExprMd) simp [h] at hsz omega -/-- Rule **IsType**: `target` is resolved; the synthesized type is `TBool`. -/ +/-- `Γ ⊢ target ⇒ _ ∴ Γ ⊢ IsType target T ⇒ TBool` + + `target` is resolved; the synthesized type is `TBool`. -/ def Synth.isType (exprMd : StmtExprMd) (target : StmtExprMd) (ty : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .IsType target ty) : @@ -1254,10 +1338,12 @@ def Synth.isType (exprMd : StmtExprMd) simp [h] at hsz omega -/-- Rule **RefEq**: both operands must be reference types (`UserDefined` or - `Unknown`) — reference equality is meaningless on primitives. The - operands must also be mutually consistent (the symmetric `isConsistent`), - so `Cat === Dog` is rejected when `Cat` and `Dog` are unrelated +/-- `Γ ⊢ lhs ⇒ T_l, Γ ⊢ rhs ⇒ T_r, isReference T_l, isReference T_r, T_l ~ T_r ∴ Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool` + + Both operands must be reference types (`UserDefined` or `Unknown`) — + reference equality is meaningless on primitives. The operands must + also be mutually consistent (the symmetric `isConsistent`), so + `Cat === Dog` is rejected when `Cat` and `Dog` are unrelated user-defined types, while `Cat === Animal` is accepted when `Cat` extends `Animal` (the gradual `Unknown` wildcard makes either side flow freely against the other). -/ @@ -1287,10 +1373,12 @@ def Synth.refEq (exprMd : StmtExprMd) (expr : StmtExpr) simp [h] at hsz omega -/-- Rule **PureFieldUpdate**: `target` is synthesized, `f` resolved against - `T_t` (or the enclosing instance type), and `newVal` checked against the - field's declared type. The synthesized type is `T_t` — updating a field - on a pure type produces a new value of the same type. -/ +/-- `Γ ⊢ target ⇒ T_t, Γ(f) = T_f, Γ ⊢ newVal ⇐ T_f ∴ Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t` + + `target` is synthesized, `f` resolved against `T_t` (or the enclosing + instance type), and `newVal` checked against the field's declared + type. The synthesized type is `T_t` — updating a field on a pure type + produces a new value of the same type. -/ def Synth.pureFieldUpdate (exprMd : StmtExprMd) (target : StmtExprMd) (fieldName : Identifier) (newVal : StmtExprMd) (h : exprMd.val = .PureFieldUpdate target fieldName newVal) : @@ -1310,11 +1398,13 @@ def Synth.pureFieldUpdate (exprMd : StmtExprMd) -- ### Verification expressions -/-- Rule **Quantifier**: opens a fresh scope, binds `x : T` (in scope only - for the body and trigger), resolves the optional trigger, and checks - the body against `TBool` since a quantifier is a proposition. Without - that body check, `forall x: int :: x + 1` would be silently accepted. - The construct itself synthesizes `TBool`. -/ +/-- `Γ, x : T ⊢ body ⇐ TBool ∴ Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool` + + Opens a fresh scope, binds `x : T` (in scope only for the body and + trigger), resolves the optional trigger, and checks the body against + `TBool` since a quantifier is a proposition. Without that body check, + `forall x: int :: x + 1` would be silently accepted. The construct + itself synthesizes `TBool`. -/ def Synth.quantifier (exprMd : StmtExprMd) (mode : QuantifierMode) (param : Parameter) (trigger : Option StmtExprMd) (body : StmtExprMd) (source : Option FileRange) @@ -1336,8 +1426,9 @@ def Synth.quantifier (exprMd : StmtExprMd) try simp_all omega -/-- Rule **Assigned**: `name` is synthesized; the construct synthesizes - `TBool`. -/ +/-- `Γ ⊢ name ⇒ _ ∴ Γ ⊢ Assigned name ⇒ TBool` + + `name` is synthesized; the construct synthesizes `TBool`. -/ def Synth.assigned (exprMd : StmtExprMd) (name : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assigned name) : @@ -1351,7 +1442,7 @@ def Synth.assigned (exprMd : StmtExprMd) simp [h] at hsz omega -/-- Rule **Old**: `Γ ⊢ v ⇒ T ⊢ Old v ⇒ T`. -/ +/-- `Γ ⊢ v ⇒ T ∴ Γ ⊢ Old v ⇒ T` -/ def Synth.old (exprMd : StmtExprMd) (val : StmtExprMd) (h : exprMd.val = .Old val) : @@ -1365,10 +1456,11 @@ def Synth.old (exprMd : StmtExprMd) simp [h] at hsz omega -/-- Rule **Fresh**: `v` is synthesized and must have a reference type - (`UserDefined` or `Unknown`) — `Fresh` only makes sense on - heap-allocated references, so `fresh(5)` is rejected. The construct - itself synthesizes `TBool`. -/ +/-- `Γ ⊢ v ⇒ T, isReference T ∴ Γ ⊢ Fresh v ⇒ TBool` + + `v` is synthesized and must have a reference type (`UserDefined` or + `Unknown`) — `Fresh` only makes sense on heap-allocated references, so + `fresh(5)` is rejected. The construct itself synthesizes `TBool`. -/ def Synth.fresh (exprMd : StmtExprMd) (expr : StmtExpr) (val : StmtExprMd) (source : Option FileRange) (h_expr : expr = .Fresh val) @@ -1386,8 +1478,10 @@ def Synth.fresh (exprMd : StmtExprMd) (expr : StmtExpr) simp [h] at hsz omega -/-- Rule **ProveBy**: `v` and `proof` are both synthesized; the construct's - type is `v`'s type — `proof` is a hint for downstream verification. -/ +/-- `Γ ⊢ v ⇒ T, Γ ⊢ proof ⇒ _ ∴ Γ ⊢ ProveBy v proof ⇒ T` + + `v` and `proof` are both synthesized; the construct's type is `v`'s + type — `proof` is a hint for downstream verification. -/ def Synth.proveBy (exprMd : StmtExprMd) (val proof : StmtExprMd) (h : exprMd.val = .ProveBy val proof) : @@ -1405,14 +1499,21 @@ def Synth.proveBy (exprMd : StmtExprMd) -- ### Self reference -/-- Rules **This-Inside** / **This-Outside**: when `instanceTypeName` is set - (we're inside an instance method, populated on `ResolveState` by - `resolveInstanceProcedure` for the duration of an instance method body), - `This` synthesizes `UserDefined T`. With it, `this.field` and - instance-method dispatch synthesize real types instead of being - wildcarded through `Unknown`. Otherwise an error is emitted ("'this' - is not allowed outside instance methods") and the type collapses to - `Unknown` to suppress cascading errors. -/ +/-- Cases on whether `instanceTypeName` is set (i.e., we're inside an + instance method). + + `Γ.instanceTypeName = some T ∴ Γ ⊢ This ⇒ UserDefined T` + + `Γ.instanceTypeName = none ∴ Γ ⊢ This ⇒ Unknown` (emits "'this' is not allowed outside instance methods") + + When `instanceTypeName` is set (we're inside an instance method, + populated on `ResolveState` by `resolveInstanceProcedure` for the + duration of an instance method body), `This` synthesizes + `UserDefined T`. With it, `this.field` and instance-method dispatch + synthesize real types instead of being wildcarded through `Unknown`. + Otherwise an error is emitted ("'this' is not allowed outside instance + methods") and the type collapses to `Unknown` to suppress cascading + errors. -/ def Synth.this (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do let s ← get @@ -1430,17 +1531,25 @@ def Synth.this (source : Option FileRange) : -- ### Untyped forms -/-- Rule **Abstract**: synthesizes `Unknown`. -/ +/-- `Γ ⊢ Abstract ⇒ Unknown` -/ def Synth.abstract (source : Option FileRange) : StmtExpr × HighTypeMd := (.Abstract, { val := .Unknown, source := source }) -/-- Rule **All**: synthesizes `Unknown`. -/ +/-- `Γ ⊢ All ⇒ Unknown` -/ def Synth.all (source : Option FileRange) : StmtExpr × HighTypeMd := (.All, { val := .Unknown, source := source }) -- ### ContractOf -/-- Rules **ContractOf-Bool** / **ContractOf-Set** / **ContractOf-Error**: +/-- Cases on the contract type `ty` and on whether `fn` is a procedure + reference. + + `fn = Var (.Local id), Γ(id) ∈ {staticProcedure, instanceProcedure} ∴ Γ ⊢ ContractOf Precondition fn ⇒ TBool and Γ ⊢ ContractOf PostCondition fn ⇒ TBool` + + `fn = Var (.Local id), Γ(id) ∈ {staticProcedure, instanceProcedure} ∴ Γ ⊢ ContractOf Reads fn ⇒ TSet Unknown and Γ ⊢ ContractOf Modifies fn ⇒ TSet Unknown` + + `fn is not a procedure reference ∴ Γ ⊢ ContractOf _ fn ↝ error: "'contractOf' expected a procedure reference"` + `ContractOf ty fn` extracts a procedure's contract clause as a value: its preconditions (`Precondition`), postconditions (`PostCondition`), reads set (`Reads`), or modifies set (`Modifies`). `fn` must be a @@ -1492,8 +1601,14 @@ def Synth.contractOf (exprMd : StmtExprMd) -- ### Holes -/-- Rules **Hole-Some** / **Hole-None-Synth**: a typed hole synthesizes its - annotation; an untyped hole in synth position synthesizes `Unknown`. -/ +/-- Cases on whether the hole has a type annotation. + + `Γ ⊢ Hole d (some T) ⇒ T` + + `Γ ⊢ Hole d none ⇒ Unknown` + + A typed hole synthesizes its annotation; an untyped hole in synth + position synthesizes `Unknown`. -/ def Synth.hole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : ResolveM (StmtExpr × HighTypeMd) := do match type with @@ -1502,8 +1617,10 @@ def Synth.hole (det : Bool) (type : Option HighTypeMd) (source : Option FileRang pure (.Hole det ty', ty') | none => pure (.Hole det none, { val := .Unknown, source := source }) -/-- Rule **Hole-None-Check**: an untyped hole in check mode records the - expected type on the node so downstream passes don't have to infer it +/-- `Γ ⊢ Hole d none ⇐ T ↦ Γ ⊢ Hole d (some T)` + + An untyped hole in check mode records the expected type on the node + so downstream passes don't have to infer it again. The subsumption check is trivial (`Unknown <: T` always holds), so this rule never fails — it just preserves the type information available at the check-mode boundary instead of discarding it. diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index b9d1070f42..c3493faa12 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -205,8 +205,7 @@ Each construct is given as a derivation. `Γ` is the current lexical scope (see every premise and conclusion unless a rule explicitly extends it (written `Γ, x : T`). Each rule is tagged with `[⇒]` (synthesis) or `[⇐]` (checking) to make the -direction explicit. When a construct has both modes, the `-Synth` / `-Check` -suffix is dropped in favor of the prefix. +direction explicit. ### Index @@ -231,9 +230,6 @@ suffix is dropped in favor of the prefix. - *ContractOf* — \[⇒\] ContractOf-Bool, \[⇒\] ContractOf-Set, \[⇒\] ContractOf-Error - *Holes* — \[⇒\] Hole-Some, \[⇒\] Hole-None, \[⇐\] Hole-None -Each LaTeX rule below is followed by the docstring of the helper that implements it -(grouped when one helper covers multiple rules). - ### Subsumption $$`\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}` @@ -271,9 +267,6 @@ $$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \ma $$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` -`⊣ Γ, x : T` records that the surrounding `Γ` is extended with the new binding for the -remainder of the enclosing scope. - {docstring Strata.Laurel.Resolution.Synth.varDeclare} ### Control flow @@ -292,9 +285,9 @@ $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vda $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` -`Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i` says each statement is resolved in the scope produced by its -predecessor and may itself extend it (`Var (.Declare …)` does); `s_n` is typed in -`Γ_{n-1}`. Bindings introduced inside the block don't escape — `Γ` is what surrounds the +$`Γ_{i-1} ⊢ s_i ⇒ \_ ⊣ Γ_i` says each statement is resolved in the scope produced by its +predecessor and may itself extend it (`Var (.Declare …)` does); $`s_n` is typed in +$`Γ_{n-1}`. Bindings introduced inside the block don't escape — $`Γ` is what surrounds the block. $$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` From 1cde2a13378fabd240777abe497dd11022ea48a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:02:49 -0400 Subject: [PATCH 116/128] expand prose around Block typing rules --- docs/verso/LaurelDoc.lean | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index c3493faa12..ddbb57f6f6 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -285,19 +285,42 @@ $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vda $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` -$`Γ_{i-1} ⊢ s_i ⇒ \_ ⊣ Γ_i` says each statement is resolved in the scope produced by its -predecessor and may itself extend it (`Var (.Declare …)` does); $`s_n` is typed in -$`Γ_{n-1}`. Bindings introduced inside the block don't escape — $`Γ` is what surrounds the -block. +Reading the premise: $`\Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i` means $`s_i` +is resolved under the scope $`\Gamma_{i-1}` produced by its predecessor, synthesizes some +type (the `_` discards it — non-last statements are sequenced for effect, not value), and +produces a possibly extended scope $`\Gamma_i` that the next statement sees. In practice +only `Var (.Declare …)` actually extends the scope; every other construct leaves it +unchanged so $`\Gamma_i = \Gamma_{i-1}`. The last statement $`s_n` is typed in +$`\Gamma_{n-1}` and *its* synthesized type $`T` becomes the block's type. The block +opens a fresh nested scope, so declarations made inside don't leak out — once the block +ends, the surrounding $`\Gamma` is restored. + +Discarding the types of non-last statements matches Java/Python/JavaScript, where +`f(x);` is a normal statement even when `f` returns a value. The trade-off is that a +stray expression like `5;` is silently accepted; flagging that belongs to a lint, not +the type checker. $$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` +An empty block has no last statement to take a type from, so it defaults to `TVoid`. + {docstring Strata.Laurel.Resolution.Synth.block} $$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` +The check form differs from the synth form in exactly one place: the *last* statement is +checked against the block's expected type $`T` instead of synthesizing freely. Non-last +statements are still synthesized-and-discarded, just as in the synth rule. Pushing $`T` +into the tail (rather than synthesizing the whole block and applying \[⇐\] Sub at the +boundary) means a type mismatch is reported at the offending subexpression's source +location, and the expectation continues to propagate through nested `Block` / +`IfThenElse` / `Hole` / `Quantifier` constructs that have their own check rules. + $$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` +With no last statement to push the expectation into, the empty-block check falls back to +a single subsumption test: an empty block is acceptable wherever `TVoid` is. + {docstring Strata.Laurel.Resolution.Check.block} $$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` From cc16c3e5ef0de4cc1fe8b26530a5e24b2d5d36c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:03:54 -0400 Subject: [PATCH 117/128] remove synthesis rule for if-then-else Synth.resolveStmtExpr is now non-total: any constructor without a synthesis rule (currently only IfThenElse) hits a wildcard arm that emits a typeMismatch diagnostic and returns Unknown to suppress cascading errors at the use site. typeMismatch drops the trailing "got 'Unknown'" suffix when actual is Unknown, matching the "we couldn't synthesize a type" semantics. The deleted synth rule plus joinTypes / firstCommonAncestor are preserved on the leo/ifthenelse-synth-lub feature branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- Strata/Languages/Laurel/Laurel.lean | 37 -------------- Strata/Languages/Laurel/Resolution.lean | 67 ++++++------------------- docs/verso/LaurelDoc.lean | 8 +-- 3 files changed, 15 insertions(+), 97 deletions(-) diff --git a/Strata/Languages/Laurel/Laurel.lean b/Strata/Languages/Laurel/Laurel.lean index 05268233df..658cbada38 100644 --- a/Strata/Languages/Laurel/Laurel.lean +++ b/Strata/Languages/Laurel/Laurel.lean @@ -583,43 +583,6 @@ def isConsistent (ctx : TypeContext) (a b : HighTypeMd) : Bool := def isConsistentSubtype (ctx : TypeContext) (sub sup : HighTypeMd) : Bool := isConsistent ctx sub sup || isSubtype ctx sub sup -/-- BFS through `extendingMap` starting from `name` and stopping at the first - type that is also in `targetAncestors`. Used by `joinTypes` to find a - common ancestor between two composites; `visited` cuts off cycles. -/ -partial def TypeContext.firstCommonAncestor (ctx : TypeContext) - (name : String) (targetAncestors : Std.HashSet String) : Option String := - let rec go (frontier : List String) (visited : Std.HashSet String) : Option String := - match frontier with - | [] => none - | n :: rest => - if visited.contains n then go rest visited - else if targetAncestors.contains n then some n - else - let parents := (ctx.extendingMap.get? n).getD [] - go (rest ++ parents) (visited.insert n) - go [name] {} - -/-- Least upper bound for the if-then-else synthesis rule. When `a` and `b` - are subtype-related, returns the larger; for unrelated composites, walks - `extending` chains for the first common ancestor. When no common - supertype exists (e.g. unrelated primitives, or a value branch paired - with a `TVoid` `return`/`exit`), falls back to `a` — the enclosing - context's `checkSubtype` then surfaces any mismatch against the - then-branch's type, preserving the historical statement-form behavior. -/ -def joinTypes (ctx : TypeContext) (a b : HighTypeMd) : HighTypeMd := - if isConsistentSubtype ctx a b then b - else if isConsistentSubtype ctx b a then a - else - let a' := ctx.unfold a - let b' := ctx.unfold b - match a'.val, b'.val with - | .UserDefined aName, .UserDefined bName => - match ctx.firstCommonAncestor aName.text (ctx.ancestors bName.text) with - | some name => - { val := .UserDefined { text := name, source := none }, source := a.source } - | none => a - | _, _ => a - def HighType.isBool : HighType → Bool | TBool => true | _ => false diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index cb72db5599..b22672a7ad 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -454,13 +454,18 @@ private def formatType (ty : HighTypeMd) : String := /-- Emit a type mismatch diagnostic. With a `construct`, the message is "'' , got ''"; without, - ", got ''". -/ + ", got ''". When `actual` is `Unknown` the trailing + `got '…'` is dropped — "we couldn't synthesize a type" is the + statement, not "the type we got was Unknown". -/ private def typeMismatch (source : Option FileRange) (construct : Option StmtExpr) (problem : String) (actual : HighTypeMd) : ResolveM Unit := do let constructor := match construct with | some c => s!"'{c.constrName}' " | none => "" - let diag := diagnosticFromSource source s!"{constructor}{problem}, got '{formatType actual}'" + let suffix := match actual.val with + | .Unknown => "" + | _ => s!", got '{formatType actual}'" + let diag := diagnosticFromSource source s!"{constructor}{problem}{suffix}" modify fun s => { s with errors := s.errors.push diag } /-- Type-level subtype check: emits the standard "expected/got" diagnostic when @@ -534,7 +539,7 @@ inside the mutual block below. Helpers are grouped by section to mirror the - Literals — `Synth.litInt`, `Synth.litBool`, `Synth.litString`, `Synth.litDecimal` - Variables — `Synth.varLocal`, `Synth.varField`, `Synth.varDeclare` -- Control flow — `Synth.ifThenElse`, `Synth.block`, `Synth.while`, `Synth.exit`, +- Control flow — `Synth.block`, `Synth.while`, `Synth.exit`, `Synth.return`, `Check.block`, `Check.ifThenElse` - Verification statements — `Synth.assert`, `Synth.assume` - Assignment — `Synth.assign`, `Check.assign` @@ -580,8 +585,6 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy match h_node: exprMd with | AstNode.mk expr source => let (val', ty) ← match h_expr: expr with - | .IfThenElse cond thenBr elseBr => - Synth.ifThenElse exprMd cond thenBr elseBr (by rw [h_node]) | .Block stmts label => Synth.block exprMd stmts label (by rw [h_node]) | .While cond invs dec body => @@ -634,6 +637,12 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy | .Abstract => pure (Synth.abstract source) | .All => pure (Synth.all source) | .Hole det type => Synth.hole det type source + | _ => + let unknown : HighTypeMd := { val := .Unknown, source := source } + typeMismatch source (some expr) + "has no synthesis rule; use it in a position with a known expected type" + unknown + pure (expr, unknown) return ({ val := val', source := source }, ty) termination_by (exprMd, 2) decreasing_by all_goals first @@ -743,54 +752,6 @@ def Synth.varField (exprMd : StmtExprMd) -- ### Control flow -/-- When there is an else branch: - - `Γ ⊢ cond ⇐ TBool, Γ ⊢ thenBr ⇒ T_t, Γ ⊢ elseBr ⇒ T_e ∴ Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T_t ⊔ T_e` - - Otherwise: - - `Γ ⊢ cond ⇐ TBool, Γ ⊢ thenBr ⇐ TVoid ∴ Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid` - - `cond` is checked against `TBool`. - With no else branch, the construct is a statement — `thenBr` is checked - against `TVoid` and the result is `TVoid`, so `x : int := if c then 5` - is rejected at the branch rather than slipping through to a downstream - subsumption. - - With an else branch, the result type is the join (LUB) of the two - branches' synthesized types, so `if c then small else big` synthesizes - the common supertype rather than committing to one branch arbitrarily; - `if c then new Left else new Right` synthesizes the common ancestor. - When no common supertype exists (e.g. a value branch paired with a - `TVoid` `return`/`exit`), `joinTypes` falls back to the then-branch's - type and the enclosing context's check (`[⇐] Sub`, or a containing - `checkSubtype` like an assignment) surfaces any mismatch downstream - against the then-branch's type. -/ -def Synth.ifThenElse (exprMd : StmtExprMd) - (cond thenBr : StmtExprMd) (elseBr : Option StmtExprMd) - (h : exprMd.val = .IfThenElse cond thenBr elseBr) : - ResolveM (StmtExpr × HighTypeMd) := do - let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } - let voidTy : HighTypeMd := { val := .TVoid, source := exprMd.source } - match elseBr with - | none => - let thenBr' ← Check.resolveStmtExpr thenBr voidTy - pure (.IfThenElse cond' thenBr' none, voidTy) - | some e => - let (thenBr', thenTy) ← Synth.resolveStmtExpr thenBr - let (elseBr', elseTy) ← Synth.resolveStmtExpr e - let ctx := (← get).typeContext - pure (.IfThenElse cond' thenBr' (some elseBr'), joinTypes ctx thenTy elseTy) - termination_by (exprMd, 1) - decreasing_by - all_goals first - | (apply Prod.Lex.left - have hsz := exprMd.sizeOf_val_lt - simp [h] at hsz - try simp_all - try omega) - | (apply Prod.Lex.right; decide) - /-- Cases on whether the statement list is empty. `Γ_0 = Γ, Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n), Γ_{n-1} ⊢ s_n ⇒ T ∴ Γ ⊢ Block [s_1; …; s_n] label ⇒ T` diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index ddbb57f6f6..9e9e0d383b 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -212,7 +212,7 @@ direction explicit. - *Subsumption* — \[⇐\] Sub - *Literals* — \[⇒\] Lit-Int, \[⇒\] Lit-Bool, \[⇒\] Lit-String, \[⇒\] Lit-Decimal - *Variables* — \[⇒\] Var-Local, \[⇒\] Var-Field, \[⇒\] Var-Declare -- *Control flow* — \[⇒\] If-NoElse, \[⇒\] If, \[⇐\] If, \[⇐\] If-NoElse; +- *Control flow* — \[⇐\] If, \[⇐\] If-NoElse; \[⇒\] Block, \[⇒\] Block-Empty, \[⇐\] Block, \[⇐\] Block-Empty; \[⇒\] Exit; \[⇒\] Return-None, \[⇒\] Return-Some, \[⇒\] Return-Void-Error, \[⇒\] Return-Multi-Error; \[⇒\] While @@ -271,12 +271,6 @@ $$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.De ### Control flow -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow \mathsf{TVoid}}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-NoElse)}` - -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If)}` - -{docstring Strata.Laurel.Resolution.Synth.ifThenElse} - $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}` $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}` From fbccb1509d44957ef4f80d53c33a949943c0dd50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:06:32 -0400 Subject: [PATCH 118/128] remove synthesis rule for blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synth.block deleted; the dispatcher's wildcard arm now handles .Block the same way it handles .IfThenElse — emit a typeMismatch and return Unknown. Block prose in LaurelDoc.lean reframed around the check rule since it's now the only block typing rule. The deleted synth rule is preserved on the leo/block-synth feature branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- Strata/Languages/Laurel/Resolution.lean | 37 +------------------------ docs/verso/LaurelDoc.lean | 32 ++++++++------------- 2 files changed, 12 insertions(+), 57 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index b22672a7ad..6331bf7bfc 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -539,7 +539,7 @@ inside the mutual block below. Helpers are grouped by section to mirror the - Literals — `Synth.litInt`, `Synth.litBool`, `Synth.litString`, `Synth.litDecimal` - Variables — `Synth.varLocal`, `Synth.varField`, `Synth.varDeclare` -- Control flow — `Synth.block`, `Synth.while`, `Synth.exit`, +- Control flow — `Synth.while`, `Synth.exit`, `Synth.return`, `Check.block`, `Check.ifThenElse` - Verification statements — `Synth.assert`, `Synth.assume` - Assignment — `Synth.assign`, `Check.assign` @@ -585,8 +585,6 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy match h_node: exprMd with | AstNode.mk expr source => let (val', ty) ← match h_expr: expr with - | .Block stmts label => - Synth.block exprMd stmts label (by rw [h_node]) | .While cond invs dec body => Synth.while exprMd cond invs dec body (by rw [h_node]) | .Exit target => pure (Synth.exit target source) @@ -752,39 +750,6 @@ def Synth.varField (exprMd : StmtExprMd) -- ### Control flow -/-- Cases on whether the statement list is empty. - - `Γ_0 = Γ, Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n), Γ_{n-1} ⊢ s_n ⇒ T ∴ Γ ⊢ Block [s_1; …; s_n] label ⇒ T` - - `Γ ⊢ Block [] label ⇒ TVoid` - - Each statement is resolved - in the scope produced by its predecessor and may itself extend it - (`Var (.Declare …)` does); non-last statements are synthesized but their - types discarded (the lax rule, matching Java/Python/JS where `f(x);` is - normal even when `f` returns a value — trade-off: `5;` is silently - accepted, flagging it belongs to a lint). The last statement's type - becomes the block's type, or `TVoid` for an empty block. The block opens - a fresh nested scope, so bindings introduced inside don't escape. -/ -def Synth.block (exprMd : StmtExprMd) - (stmts : List StmtExprMd) (label : Option String) - (h : exprMd.val = .Block stmts label) : - ResolveM (StmtExpr × HighTypeMd) := do - withScope do - let results ← stmts.mapM Synth.resolveStmtExpr - let stmts' := results.map (·.1) - let lastTy := match results.getLast? with - | some (_, ty) => ty - | none => { val := .TVoid, source := exprMd.source } - pure (.Block stmts' label, lastTy) - termination_by (exprMd, 1) - decreasing_by - apply Prod.Lex.left - have hsz := exprMd.sizeOf_val_lt - simp [h] at hsz - have := List.sizeOf_lt_of_mem ‹_ ∈ stmts› - omega - /-- `Γ ⊢ cond ⇐ TBool, Γ ⊢ invs_i ⇐ TBool, Γ ⊢ dec ⇐ ?, Γ ⊢ body ⇒ _ ∴ Γ ⊢ While cond invs dec body ⇒ TVoid` `cond ⇐ TBool`, each invariant `⇐ TBool`, optional `decreases` is diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 9e9e0d383b..f358fcc258 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -213,7 +213,7 @@ direction explicit. - *Literals* — \[⇒\] Lit-Int, \[⇒\] Lit-Bool, \[⇒\] Lit-String, \[⇒\] Lit-Decimal - *Variables* — \[⇒\] Var-Local, \[⇒\] Var-Field, \[⇒\] Var-Declare - *Control flow* — \[⇐\] If, \[⇐\] If-NoElse; - \[⇒\] Block, \[⇒\] Block-Empty, \[⇐\] Block, \[⇐\] Block-Empty; \[⇒\] Exit; + \[⇐\] Block, \[⇐\] Block-Empty; \[⇒\] Exit; \[⇒\] Return-None, \[⇒\] Return-Some, \[⇒\] Return-Void-Error, \[⇒\] Return-Multi-Error; \[⇒\] While - *Verification statements* — \[⇒\] Assert, \[⇒\] Assume @@ -277,38 +277,28 @@ $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vda {docstring Strata.Laurel.Resolution.Check.ifThenElse} -$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block)}` +$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` Reading the premise: $`\Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i` means $`s_i` is resolved under the scope $`\Gamma_{i-1}` produced by its predecessor, synthesizes some type (the `_` discards it — non-last statements are sequenced for effect, not value), and produces a possibly extended scope $`\Gamma_i` that the next statement sees. In practice only `Var (.Declare …)` actually extends the scope; every other construct leaves it -unchanged so $`\Gamma_i = \Gamma_{i-1}`. The last statement $`s_n` is typed in -$`\Gamma_{n-1}` and *its* synthesized type $`T` becomes the block's type. The block -opens a fresh nested scope, so declarations made inside don't leak out — once the block -ends, the surrounding $`\Gamma` is restored. +unchanged so $`\Gamma_i = \Gamma_{i-1}`. The *last* statement $`s_n` is checked against +the block's expected type $`T` rather than synthesizing freely. The block opens a fresh +nested scope, so declarations made inside don't leak out — once the block ends, the +surrounding $`\Gamma` is restored. Discarding the types of non-last statements matches Java/Python/JavaScript, where `f(x);` is a normal statement even when `f` returns a value. The trade-off is that a stray expression like `5;` is silently accepted; flagging that belongs to a lint, not the type checker. -$$`\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Block-Empty)}` - -An empty block has no last statement to take a type from, so it defaults to `TVoid`. - -{docstring Strata.Laurel.Resolution.Synth.block} - -$$`\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \Rightarrow \_ \dashv \Gamma_i \;(1 \le i < n) \quad \Gamma_{n-1} \vdash s_n \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}` - -The check form differs from the synth form in exactly one place: the *last* statement is -checked against the block's expected type $`T` instead of synthesizing freely. Non-last -statements are still synthesized-and-discarded, just as in the synth rule. Pushing $`T` -into the tail (rather than synthesizing the whole block and applying \[⇐\] Sub at the -boundary) means a type mismatch is reported at the offending subexpression's source -location, and the expectation continues to propagate through nested `Block` / -`IfThenElse` / `Hole` / `Quantifier` constructs that have their own check rules. +Pushing $`T` into the tail (rather than synthesizing the whole block and applying +\[⇐\] Sub at the boundary) means a type mismatch is reported at the offending +subexpression's source location, and the expectation continues to propagate through +nested `Block` / `IfThenElse` / `Hole` / `Quantifier` constructs that have their own +check rules. $$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block-Empty)}` From 7d3cdf6c9ffb4b774a0dc9af9ae56f6cd1d4dd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:28:10 -0400 Subject: [PATCH 119/128] move statement-shaped constructs to check-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VarDeclare, While, Exit, Return, Assert, Assume — none of these have any reason to ever produce a synthesized type other than TVoid, so each gets a dedicated Check. rule that performs the construct's own work (Return's arity diagnostics, VarDeclare's scope extension, etc.) plus a TVoid-vs-expected subsumption check, replacing the corresponding Synth.. The synth dispatcher's wildcard arm now catches these too: any program that puts a VarDeclare/While/Exit/Return/Assert/Assume in synth position gets the standard "no synthesis rule" diagnostic. The deleted synth rules are preserved on leo/synth-control-flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- Strata/Languages/Laurel/Resolution.lean | 163 +++++++++++++----------- docs/verso/LaurelDoc.lean | 40 +++--- 2 files changed, 109 insertions(+), 94 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 6331bf7bfc..0758a4ad19 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -538,10 +538,10 @@ inside the mutual block below. Helpers are grouped by section to mirror the *Typing rules* index in `LaurelDoc.lean`: - Literals — `Synth.litInt`, `Synth.litBool`, `Synth.litString`, `Synth.litDecimal` -- Variables — `Synth.varLocal`, `Synth.varField`, `Synth.varDeclare` -- Control flow — `Synth.while`, `Synth.exit`, - `Synth.return`, `Check.block`, `Check.ifThenElse` -- Verification statements — `Synth.assert`, `Synth.assume` +- Variables — `Synth.varLocal`, `Synth.varField`, `Check.varDeclare` +- Control flow — `Check.while`, `Check.exit`, `Check.return`, + `Check.block`, `Check.ifThenElse` +- Verification statements — `Check.assert`, `Check.assume` - Assignment — `Synth.assign`, `Check.assign` - Calls — `Synth.staticCall`, `Synth.instanceCall` - Primitive operations — `Synth.primitiveOp` @@ -585,17 +585,11 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy match h_node: exprMd with | AstNode.mk expr source => let (val', ty) ← match h_expr: expr with - | .While cond invs dec body => - Synth.while exprMd cond invs dec body (by rw [h_node]) - | .Exit target => pure (Synth.exit target source) - | .Return val => - Synth.return exprMd source val (by rw [h_node]) | .LiteralInt v => pure (Synth.litInt v source) | .LiteralBool v => pure (Synth.litBool v source) | .LiteralString v => pure (Synth.litString v source) | .LiteralDecimal v => pure (Synth.litDecimal v source) | .Var (.Local ref) => Synth.varLocal ref source - | .Var (.Declare param) => Synth.varDeclare param source | .Var (.Field target fieldName) => Synth.varField exprMd target fieldName source (by rw [h_node]) | .Assign targets value => @@ -624,10 +618,6 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy Synth.old exprMd val (by rw [h_node]) | .Fresh val => Synth.fresh exprMd expr val source h_expr (by rw [h_node]) - | .Assert ⟨condExpr, summary⟩ => - Synth.assert exprMd condExpr summary source (by rw [h_node]) - | .Assume cond => - Synth.assume exprMd cond source (by rw [h_node]) | .ProveBy val proof => Synth.proveBy exprMd val proof (by rw [h_node]) | .ContractOf ty fn => @@ -676,6 +666,16 @@ def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : Resolv | .Assign targets value => Check.assign exprMd targets value expected source (by rw [h_node]) | .Hole det none => pure (Check.holeNone det expected source) + | .Var (.Declare param) => Check.varDeclare param expected source + | .While cond invs dec body => + Check.while exprMd cond invs dec body expected source (by rw [h_node]) + | .Exit target => pure (Check.exit target expected source) + | .Return val => + Check.return exprMd val expected source (by rw [h_node]) + | .Assert ⟨condExpr, summary⟩ => + Check.assert exprMd condExpr summary expected source (by rw [h_node]) + | .Assume cond => + Check.assume exprMd cond expected source (by rw [h_node]) | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← Synth.resolveStmtExpr exprMd @@ -717,17 +717,6 @@ def Synth.varLocal (ref : Identifier) (source : Option FileRange) : let ty ← getVarType ref pure (.Var (.Local ref'), ty) -/-- `x ∉ dom(Γ) ∴ Γ ⊢ Var (.Declare ⟨x, T⟩) ⇒ TVoid ⊣ Γ, x : T` - - `⊣ Γ, x : T` records that the surrounding scope is extended with the - new binding for the remainder of the enclosing scope. The declaration - itself produces no value, hence `TVoid`. -/ -def Synth.varDeclare (param : Parameter) (source : Option FileRange) : - ResolveM (StmtExpr × HighTypeMd) := do - let ty' ← resolveHighType param.type - let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (.Var (.Declare ⟨name', ty'⟩), { val := .TVoid, source := source }) - /-- `Γ ⊢ e ⇒ _, Γ(f) = T_f ∴ Γ ⊢ Var (.Field e f) ⇒ T_f` `f` is looked up against the type of `e` (or the enclosing instance type @@ -748,26 +737,43 @@ def Synth.varField (exprMd : StmtExprMd) try simp_all omega +/-- `x ∉ dom(Γ), TVoid <: T ∴ Γ ⊢ Var (.Declare ⟨x, T_x⟩) ⇐ T ⊣ Γ, x : T_x` + + `⊣ Γ, x : T_x` records that the surrounding scope is extended with the + new binding for the remainder of the enclosing scope. The declaration + itself produces no value, so `expected` must admit `TVoid`. -/ +def Check.varDeclare (param : Parameter) + (expected : HighTypeMd) (source : Option FileRange) : + ResolveM StmtExprMd := do + let ty' ← resolveHighType param.type + let name' ← defineNameCheckDup param.name (.var param.name ty') + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .Var (.Declare ⟨name', ty'⟩), source := source } + -- ### Control flow -/-- `Γ ⊢ cond ⇐ TBool, Γ ⊢ invs_i ⇐ TBool, Γ ⊢ dec ⇐ ?, Γ ⊢ body ⇒ _ ∴ Γ ⊢ While cond invs dec body ⇒ TVoid` +/-- `Γ ⊢ cond ⇐ TBool, Γ ⊢ invs_i ⇐ TBool, Γ ⊢ dec ⇐ ?, Γ ⊢ body ⇐ T, TVoid <: T ∴ Γ ⊢ While cond invs dec body ⇐ T` - `cond ⇐ TBool`, each invariant `⇐ TBool`, optional `decreases` is - resolved without a type check today (the intended target is a numeric - type), body is synthesized; the construct itself synthesizes `TVoid`. -/ -def Synth.while (exprMd : StmtExprMd) + `cond` is checked against `TBool`, each invariant against `TBool`, + optional `decreases` is currently resolved without a type check (the + intended target is a numeric type), and the body is checked against + the surrounding `expected` type. The construct itself produces no + value, so `expected` must admit `TVoid`. -/ +def Check.while (exprMd : StmtExprMd) (cond : StmtExprMd) (invs : List StmtExprMd) (dec : Option StmtExprMd) (body : StmtExprMd) + (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .While cond invs dec body) : - ResolveM (StmtExpr × HighTypeMd) := do + ResolveM StmtExprMd := do let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } let invs' ← invs.attach.mapM (fun a => have := a.property; do Check.resolveStmtExpr a.val { val := .TBool, source := a.val.source }) let dec' ← dec.attach.mapM (fun a => have := a.property; do let (e', _) ← Synth.resolveStmtExpr a.val; pure e') - let (body', _) ← Synth.resolveStmtExpr body - pure (.While cond' invs' dec' body', { val := .TVoid, source := exprMd.source }) - termination_by (exprMd, 1) + let body' ← Check.resolveStmtExpr body expected + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .While cond' invs' dec' body', source := source } + termination_by (exprMd, 0) decreasing_by all_goals apply Prod.Lex.left @@ -777,27 +783,30 @@ def Synth.while (exprMd : StmtExprMd) try simp_all omega -/-- `Γ ⊢ Exit target ⇒ TVoid` -/ -def Synth.exit (target : String) (source : Option FileRange) : StmtExpr × HighTypeMd := - (.Exit target, { val := .TVoid, source := source }) +/-- `TVoid <: T ∴ Γ ⊢ Exit target ⇐ T` -/ +def Check.exit (target : String) (expected : HighTypeMd) + (source : Option FileRange) : ResolveM StmtExprMd := do + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .Exit target, source := source } /-- Cases on whether the return value is `none` or `some e`, and on the arity of the enclosing procedure's declared outputs. - `Γ ⊢ Return none ⇒ TVoid` + `TVoid <: T ∴ Γ ⊢ Return none ⇐ T` - `Γ_proc.outputs = [T], Γ ⊢ e ⇐ T ∴ Γ ⊢ Return (some e) ⇒ TVoid` + `Γ_proc.outputs = [T_o], Γ ⊢ e ⇐ T_o, TVoid <: T ∴ Γ ⊢ Return (some e) ⇐ T` `Γ_proc.outputs = [] ∴ Γ ⊢ Return (some e) ↝ error: "void procedure cannot return a value"` `Γ_proc.outputs = [T_1; …; T_n] (n ≥ 2) ∴ Γ ⊢ Return (some e) ↝ error: "multi-output procedure cannot use 'return e'; assign to named outputs instead"` - Matches the optional return value against the - enclosing procedure's declared outputs. The expected output types are - threaded through `ResolveState.expectedReturnTypes`, set from - `proc.outputs` by `resolveProcedure` / `resolveInstanceProcedure` for - the duration of the body; `none` means "no enclosing procedure" — e.g. - resolving a constant initializer — and skips all `Return` checks. + The `Return` construct itself produces no value, so `expected` must + admit `TVoid`. The optional payload is matched against the enclosing + procedure's declared outputs (threaded through + `ResolveState.expectedReturnTypes`, set from `proc.outputs` by + `resolveProcedure` / `resolveInstanceProcedure` for the duration of + the body; `none` means "no enclosing procedure" — e.g. resolving a + constant initializer — and skips all `Return` checks). A bare `return;` is allowed in any context. In a single-output procedure it acts as a Dafny-style early exit — the output parameter retains @@ -807,34 +816,35 @@ def Synth.exit (target : String) (source : Option FileRange) : StmtExpr × HighT Multi-output procedures use named-output assignment (`r := …` on the declared output parameters); `return e` syntactically takes a single - `Option StmtExpr` and cannot carry multiple values, so it is flagged with - a diagnostic pointing users at the named-output convention. -/ -def Synth.return (exprMd : StmtExprMd) (source : Option FileRange) - (val : Option StmtExprMd) + `Option StmtExpr` and cannot carry multiple values, so it is flagged + with a diagnostic pointing users at the named-output convention. -/ +def Check.return (exprMd : StmtExprMd) + (val : Option StmtExprMd) (expected : HighTypeMd) + (source : Option FileRange) (h : exprMd.val = .Return val) : - ResolveM (StmtExpr × HighTypeMd) := do - let expected := (← get).expectedReturnTypes + ResolveM StmtExprMd := do + let expectedReturn := (← get).expectedReturnTypes let val' ← val.attach.mapM (fun a => have := a.property; do - match expected with + match expectedReturn with | some [singleOutput] => Check.resolveStmtExpr a.val singleOutput | _ => let (e', _) ← Synth.resolveStmtExpr a.val; pure e') - -- Arity/shape diagnostics independent of the value's own type. - match val, expected with + match val, expectedReturn with | none, some [] => pure () - | none, some [_] => pure () -- Dafny-style early exit - | none, some _ => pure () -- multi-output: bare return is fine + | none, some [_] => pure () + | none, some _ => pure () | some _, some [] => let diag := diagnosticFromSource source "void procedure cannot return a value" modify fun s => { s with errors := s.errors.push diag } - | some _, some [_] => pure () -- value already checked above + | some _, some [_] => pure () | some _, some _ => let diag := diagnosticFromSource source "multi-output procedure cannot use 'return e'; assign to named outputs instead" modify fun s => { s with errors := s.errors.push diag } - | _, none => pure () -- no enclosing procedure - pure (.Return val', { val := .TVoid, source := source }) - termination_by (exprMd, 1) + | _, none => pure () + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .Return val', source := source } + termination_by (exprMd, 0) decreasing_by all_goals apply Prod.Lex.left @@ -915,16 +925,19 @@ def Check.ifThenElse (exprMd : StmtExprMd) -- ### Verification statements -/-- `Γ ⊢ cond ⇐ TBool ∴ Γ ⊢ Assert cond ⇒ TVoid` +/-- `Γ ⊢ cond ⇐ TBool, TVoid <: T ∴ Γ ⊢ Assert cond ⇐ T` - `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ -def Synth.assert (exprMd : StmtExprMd) - (condExpr : StmtExprMd) (summary : Option String) (source : Option FileRange) + `cond` is checked against `TBool`; the construct produces no value, + so `expected` must admit `TVoid`. -/ +def Check.assert (exprMd : StmtExprMd) + (condExpr : StmtExprMd) (summary : Option String) + (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Assert ⟨condExpr, summary⟩) : - ResolveM (StmtExpr × HighTypeMd) := do + ResolveM StmtExprMd := do let cond' ← Check.resolveStmtExpr condExpr { val := .TBool, source := condExpr.source } - pure (.Assert { condition := cond', summary }, { val := .TVoid, source := source }) - termination_by (exprMd, 1) + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .Assert { condition := cond', summary }, source := source } + termination_by (exprMd, 0) decreasing_by apply Prod.Lex.left have hsz := exprMd.sizeOf_val_lt @@ -932,16 +945,18 @@ def Synth.assert (exprMd : StmtExprMd) try simp_all omega -/-- `Γ ⊢ cond ⇐ TBool ∴ Γ ⊢ Assume cond ⇒ TVoid` +/-- `Γ ⊢ cond ⇐ TBool, TVoid <: T ∴ Γ ⊢ Assume cond ⇐ T` - `cond` is checked against `TBool`; the construct synthesizes `TVoid`. -/ -def Synth.assume (exprMd : StmtExprMd) - (cond : StmtExprMd) (source : Option FileRange) + `cond` is checked against `TBool`; the construct produces no value, + so `expected` must admit `TVoid`. -/ +def Check.assume (exprMd : StmtExprMd) + (cond : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Assume cond) : - ResolveM (StmtExpr × HighTypeMd) := do + ResolveM StmtExprMd := do let cond' ← Check.resolveStmtExpr cond { val := .TBool, source := cond.source } - pure (.Assume cond', { val := .TVoid, source := source }) - termination_by (exprMd, 1) + checkSubtype source expected { val := .TVoid, source := source } + pure { val := .Assume cond', source := source } + termination_by (exprMd, 0) decreasing_by apply Prod.Lex.left have hsz := exprMd.sizeOf_val_lt diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index f358fcc258..ffa9cba9c4 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -211,12 +211,12 @@ direction explicit. - *Subsumption* — \[⇐\] Sub - *Literals* — \[⇒\] Lit-Int, \[⇒\] Lit-Bool, \[⇒\] Lit-String, \[⇒\] Lit-Decimal -- *Variables* — \[⇒\] Var-Local, \[⇒\] Var-Field, \[⇒\] Var-Declare +- *Variables* — \[⇒\] Var-Local, \[⇒\] Var-Field, \[⇐\] Var-Declare - *Control flow* — \[⇐\] If, \[⇐\] If-NoElse; - \[⇐\] Block, \[⇐\] Block-Empty; \[⇒\] Exit; - \[⇒\] Return-None, \[⇒\] Return-Some, \[⇒\] Return-Void-Error, - \[⇒\] Return-Multi-Error; \[⇒\] While -- *Verification statements* — \[⇒\] Assert, \[⇒\] Assume + \[⇐\] Block, \[⇐\] Block-Empty; \[⇐\] Exit; + \[⇐\] Return-None, \[⇐\] Return-Some, \[⇐\] Return-Void-Error, + \[⇐\] Return-Multi-Error; \[⇐\] While +- *Verification statements* — \[⇐\] Assert, \[⇐\] Assume - *Assignment* — \[⇒\] Assign - *Calls* — \[⇒\] Static-Call, \[⇒\] Static-Call-Multi, \[⇒\] Instance-Call - *Primitive operations* — \[⇒\] Op-Bool, \[⇒\] Op-Cmp, \[⇒\] Op-Eq, \[⇒\] Op-Arith, @@ -265,9 +265,9 @@ $$`\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \ma {docstring Strata.Laurel.Resolution.Synth.varField} -$$`\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T\rangle) \Rightarrow \mathsf{TVoid} \dashv \Gamma, x : T} \quad \text{([⇒] Var-Declare)}` +$$`\frac{x \notin \mathrm{dom}(\Gamma) \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, T_x\rangle) \Leftarrow T \dashv \Gamma, x : T_x} \quad \text{([⇐] Var-Declare)}` -{docstring Strata.Laurel.Resolution.Synth.varDeclare} +{docstring Strata.Laurel.Resolution.Check.varDeclare} ### Control flow @@ -307,33 +307,33 @@ a single subsumption test: an empty block is acceptable wherever `TVoid` is. {docstring Strata.Laurel.Resolution.Check.block} -$$`\frac{}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Exit)}` +$$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Exit}\;\mathit{target} \Leftarrow T} \quad \text{([⇐] Exit)}` -{docstring Strata.Laurel.Resolution.Synth.exit} +{docstring Strata.Laurel.Resolution.Check.exit} -$$`\frac{}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None)}` +$$`\frac{\mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] Return-None)}` -$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T] \quad \Gamma \vdash e \Leftarrow T}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-Some)}` +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_o] \quad \Gamma \vdash e \Leftarrow T_o \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \Leftarrow T} \quad \text{([⇐] Return-Some)}` -$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “void procedure cannot return a value”}} \quad \text{([⇒] Return-Void-Error)}` +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = []}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “void procedure cannot return a value”}} \quad \text{([⇐] Return-Void-Error)}` -$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}` +$$`\frac{\Gamma_{\mathit{proc}}.\mathit{outputs} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use ‘return e’; assign to named outputs instead”}} \quad \text{([⇐] Return-Multi-Error)}` -{docstring Strata.Laurel.Resolution.Synth.return} +{docstring Strata.Laurel.Resolution.Check.return} -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Rightarrow \_}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{dec} \Leftarrow {?} \quad \Gamma \vdash \mathit{body} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{dec}\;\mathit{body} \Leftarrow T} \quad \text{([⇐] While)}` -{docstring Strata.Laurel.Resolution.Synth.while} +{docstring Strata.Laurel.Resolution.Check.while} ### Verification statements -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Leftarrow T} \quad \text{([⇐] Assert)}` -{docstring Strata.Laurel.Resolution.Synth.assert} +{docstring Strata.Laurel.Resolution.Check.assert} -$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}` +$$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Leftarrow T} \quad \text{([⇐] Assume)}` -{docstring Strata.Laurel.Resolution.Synth.assume} +{docstring Strata.Laurel.Resolution.Check.assume} ### Assignment From be18c88ad45d1b78e3011cba0b7b6d0eb3bb6e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:41:53 -0400 Subject: [PATCH 120/128] move Old and ProveBy to check-only Both constructs are pass-through: Old v has the type of v, ProveBy v p has the type of v (proof is just a hint). Check.old / Check.proveBy push the surrounding expectation into the inner expression rather than synthing-then-subsuming. The proof in ProveBy still synthesizes since it has no type constraint of its own. The deleted synth rules are preserved on leo/synth-old-proveby. Co-Authored-By: Claude Opus 4.7 (1M context) --- Strata/Languages/Laurel/Resolution.lean | 49 ++++++++++++++----------- docs/verso/LaurelDoc.lean | 12 +++--- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 0758a4ad19..8811039d61 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -547,8 +547,8 @@ inside the mutual block below. Helpers are grouped by section to mirror the - Primitive operations — `Synth.primitiveOp` - Object forms — `Synth.new`, `Synth.asType`, `Synth.isType`, `Synth.refEq`, `Synth.pureFieldUpdate` -- Verification expressions — `Synth.quantifier`, `Synth.assigned`, `Synth.old`, - `Synth.fresh`, `Synth.proveBy` +- Verification expressions — `Synth.quantifier`, `Synth.assigned`, + `Synth.fresh`, `Check.old`, `Check.proveBy` - Self reference — `Synth.this` - Untyped forms — `Synth.abstract`, `Synth.all` - ContractOf — `Synth.contractOf` @@ -614,12 +614,8 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy Synth.quantifier exprMd mode param trigger body source (by rw [h_node]) | .Assigned name => Synth.assigned exprMd name source (by rw [h_node]) - | .Old val => - Synth.old exprMd val (by rw [h_node]) | .Fresh val => Synth.fresh exprMd expr val source h_expr (by rw [h_node]) - | .ProveBy val proof => - Synth.proveBy exprMd val proof (by rw [h_node]) | .ContractOf ty fn => Synth.contractOf exprMd ty fn source (by rw [h_node]) | .Abstract => pure (Synth.abstract source) @@ -676,6 +672,10 @@ def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : Resolv Check.assert exprMd condExpr summary expected source (by rw [h_node]) | .Assume cond => Check.assume exprMd cond expected source (by rw [h_node]) + | .Old val => + Check.old exprMd val expected source (by rw [h_node]) + | .ProveBy val proof => + Check.proveBy exprMd val proof expected source (by rw [h_node]) | _ => -- Subsumption fallback: synth then check `actual <: expected`. let (e', actual) ← Synth.resolveStmtExpr exprMd @@ -1383,14 +1383,17 @@ def Synth.assigned (exprMd : StmtExprMd) simp [h] at hsz omega -/-- `Γ ⊢ v ⇒ T ∴ Γ ⊢ Old v ⇒ T` -/ -def Synth.old (exprMd : StmtExprMd) - (val : StmtExprMd) +/-- `Γ ⊢ v ⇐ T ∴ Γ ⊢ Old v ⇐ T` + + `Old v` has the same type as `v`, so the surrounding expectation + propagates straight through. -/ +def Check.old (exprMd : StmtExprMd) + (val : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Old val) : - ResolveM (StmtExpr × HighTypeMd) := do - let (val', valTy) ← Synth.resolveStmtExpr val - pure (.Old val', valTy) - termination_by (exprMd, 1) + ResolveM StmtExprMd := do + let val' ← Check.resolveStmtExpr val expected + pure { val := .Old val', source := source } + termination_by (exprMd, 0) decreasing_by apply Prod.Lex.left have hsz := exprMd.sizeOf_val_lt @@ -1419,18 +1422,20 @@ def Synth.fresh (exprMd : StmtExprMd) (expr : StmtExpr) simp [h] at hsz omega -/-- `Γ ⊢ v ⇒ T, Γ ⊢ proof ⇒ _ ∴ Γ ⊢ ProveBy v proof ⇒ T` +/-- `Γ ⊢ v ⇐ T, Γ ⊢ proof ⇒ _ ∴ Γ ⊢ ProveBy v proof ⇐ T` - `v` and `proof` are both synthesized; the construct's type is `v`'s - type — `proof` is a hint for downstream verification. -/ -def Synth.proveBy (exprMd : StmtExprMd) - (val proof : StmtExprMd) + `ProveBy v proof` has the same type as `v` (the proof is just a hint + for downstream verification), so the surrounding expectation + propagates into `v`. The proof itself has no constraint on its type + and is still synthesized. -/ +def Check.proveBy (exprMd : StmtExprMd) + (val proof : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .ProveBy val proof) : - ResolveM (StmtExpr × HighTypeMd) := do - let (val', valTy) ← Synth.resolveStmtExpr val + ResolveM StmtExprMd := do + let val' ← Check.resolveStmtExpr val expected let (proof', _) ← Synth.resolveStmtExpr proof - pure (.ProveBy val' proof', valTy) - termination_by (exprMd, 1) + pure { val := .ProveBy val' proof', source := source } + termination_by (exprMd, 0) decreasing_by all_goals apply Prod.Lex.left diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index ffa9cba9c4..862e0f7348 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -223,8 +223,8 @@ direction explicit. \[⇒\] Op-Concat - *Object forms* — \[⇒\] New-Ok, \[⇒\] New-Fallback; \[⇒\] AsType; \[⇒\] IsType; \[⇒\] RefEq; \[⇒\] PureFieldUpdate -- *Verification expressions* — \[⇒\] Quantifier, \[⇒\] Assigned, \[⇒\] Old, - \[⇒\] Fresh, \[⇒\] ProveBy +- *Verification expressions* — \[⇒\] Quantifier, \[⇒\] Assigned, \[⇐\] Old, + \[⇒\] Fresh, \[⇐\] ProveBy - *Self reference* — \[⇒\] This-Inside, \[⇒\] This-Outside - *Untyped forms* — \[⇒\] Abstract / All - *ContractOf* — \[⇒\] ContractOf-Bool, \[⇒\] ContractOf-Set, \[⇒\] ContractOf-Error @@ -417,17 +417,17 @@ $$`\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assig {docstring Strata.Laurel.Resolution.Synth.assigned} -$$`\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old)}` +$$`\frac{\Gamma \vdash v \Leftarrow T}{\Gamma \vdash \mathsf{Old}\;v \Leftarrow T} \quad \text{([⇐] Old)}` -{docstring Strata.Laurel.Resolution.Synth.old} +{docstring Strata.Laurel.Resolution.Check.old} $$`\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}` {docstring Strata.Laurel.Resolution.Synth.fresh} -$$`\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy)}` +$$`\frac{\Gamma \vdash v \Leftarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Leftarrow T} \quad \text{([⇐] ProveBy)}` -{docstring Strata.Laurel.Resolution.Synth.proveBy} +{docstring Strata.Laurel.Resolution.Check.proveBy} ### Self reference From 4c23930cb5429f6096f008ec4052f3e016a1bb05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:44:42 -0400 Subject: [PATCH 121/128] push assignment target types into RHS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synth.assign and Check.assign now compute ExpectedTy from the targets' declared types and push it into the RHS via Check.resolveStmtExpr, instead of synthesizing the RHS and verifying ExpectedTy <: valueTy afterward. This means bidirectional rules in the RHS (Check.ifThenElse, Check.block, …) propagate the assignment's type into nested constructs: `var x: int := if c then a else b` checks each branch against int directly, with errors fired at the offending branch. Synth.assign returns ExpectedTy (rather than the RHS's synthesized type), since the RHS has been checked against ExpectedTy and any mismatch is already reported. Expression-position assignments like `x ++ (y := s)` see y's declared type, not s's actual type — matches the principle that a typed binding has its declared type. The TVoid-RHS skip in the previous code (which avoided spurious diagnostics when the RHS was a side-effecting statement like `return` or `while` that synthesized TVoid) is no longer needed: those constructs are check-only now and will be checked against ExpectedTy directly, surfacing a clear "expected 'int', got 'TVoid'" error if the user wrote something nonsensical like `x: int := while (…) {…}`. The previous synth-then-verify behavior is preserved on leo/synth-assign. Co-Authored-By: Claude Opus 4.7 (1M context) --- Strata/Languages/Laurel/Resolution.lean | 82 ++++++++++++------------- docs/verso/LaurelDoc.lean | 8 ++- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 8811039d61..a2986a5d83 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -966,23 +966,26 @@ def Check.assume (exprMd : StmtExprMd) -- ### Assignment -/-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇒ T_e, T_e <: ExpectedTy ∴ Γ ⊢ Assign targets e ⇒ TVoid` +/-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇐ ExpectedTy ∴ Γ ⊢ Assign targets e ⇒ ExpectedTy` where `ExpectedTy = T_1` if `|targets| = 1`, else `MultiValuedExpr [T_1; …; T_n]`. - Each target's declared type `T_i` (from `Local`, - `Field`, or fresh `Declare`) is collapsed into a tuple `ExpectedTy` - (single type if one target, otherwise `MultiValuedExpr [T_1; …; T_n]`) - and checked against the RHS's synthesized type. Both single- and - multi-target forms collapse into one tuple-vs-tuple check: when the RHS - is a `MultiValuedExpr`, both arity and per-position type mismatches - surface in a single diagnostic of shape *"expected '(int, int, int)', - got '(int, string)'"*. When the RHS is `TVoid` (a side-effecting - statement: `while`, `return`, …), all checks are skipped — there's no - value to assign. The construct synthesizes the RHS's type, so that - expression-position assignments like `x ++ (y := s)` see a string in - the second operand; statement-position uses are accommodated by - `Check.assign`, which accepts `TVoid` as the expected type. -/ + Each target's declared type `T_i` (from `Local`, `Field`, or fresh + `Declare`) is collapsed into a tuple `ExpectedTy` (single type if one + target, otherwise `MultiValuedExpr [T_1; …; T_n]`) and pushed into + the RHS via `Check.resolveStmtExpr`. This means the RHS's bidirectional + rules (e.g. `Check.ifThenElse`, `Check.block`) propagate `ExpectedTy` + inward: `var x: int := if c then a else b` checks each branch against + `int` directly, with errors fired at the offending branch. + + Multi-target forms produce a single tuple-vs-tuple check: when the + RHS is itself `MultiValuedExpr` (a multi-output procedure call), both + arity and per-position type mismatches surface in a single diagnostic + of shape *"expected '(int, int, int)', got '(int, string)'"*. + + The synthesized type is `ExpectedTy`, so expression-position + assignments like `x ++ (y := s)` see the target type in the second + operand. -/ def Synth.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) (h : exprMd.val = .Assign targets value) : @@ -1001,19 +1004,17 @@ def Synth.assign (exprMd : StmtExprMd) let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← Synth.resolveStmtExpr value let targetType (t : VariableMd) : ResolveM HighTypeMd := do match t.val with | .Local ref => getVarType ref | .Declare param => pure param.type | .Field _ fieldName => getVarType fieldName - if valueTy.val != HighType.TVoid then - let targetTys ← targets'.mapM targetType - let expectedTy : HighTypeMd := match targetTys with - | [single] => single - | _ => { val := .MultiValuedExpr targetTys, source := source } - checkSubtype source expectedTy valueTy - pure (.Assign targets' value', valueTy) + let targetTys ← targets'.mapM targetType + let expectedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + let value' ← Check.resolveStmtExpr value expectedTy + pure (.Assign targets' value', expectedTy) termination_by (exprMd, 1) decreasing_by all_goals @@ -1024,19 +1025,20 @@ def Synth.assign (exprMd : StmtExprMd) try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) omega -/-- Cases on whether `expected` is `TVoid` (statement position) or some - other type (expression position). +/-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇐ ExpectedTy, ExpectedTy <: T ∴ Γ ⊢ Assign targets e ⇐ T` - `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇒ T_e, T_e <: ExpectedTy ∴ Γ ⊢ Assign targets e ⇐ TVoid` + where `ExpectedTy = T_1` if `|targets| = 1`, else + `MultiValuedExpr [T_1; …; T_n]`. - `Γ ⊢ Assign targets e ⇒ T_e, T_e <: T ∴ Γ ⊢ Assign targets e ⇐ T (T ≠ TVoid)` - - An assignment in statement position (checked against `TVoid`) discards - its RHS value, so the synthesized type is not compared against - `expected`. This lets `b := 1` appear as the last statement of a block - in an else-less `if` (whose branch is checked against `TVoid`) without - firing a subsumption error against the RHS's type. For non-`TVoid` - expected types, falls back to subsumption. -/ + Like `Synth.assign`, the target tuple type is pushed into the RHS so + bidirectional rules in the RHS receive the assignment's type. The + outer subsumption `ExpectedTy <: T` accommodates use as a statement + (`T = TVoid`, no value to compare) or as an expression + (`T ≠ TVoid`, the result type must match). When `T = TVoid` the + subsumption is satisfied trivially since `_ <: TVoid` only when the + LHS is also `TVoid` — the assignment value is discarded in statement + position and we want no further check, so the subsumption is + skipped. -/ def Check.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) @@ -1055,20 +1057,18 @@ def Check.assign (exprMd : StmtExprMd) let ty' ← resolveHighType param.type let name' ← defineNameCheckDup param.name (.var param.name ty') pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let (value', valueTy) ← Synth.resolveStmtExpr value let targetType (t : VariableMd) : ResolveM HighTypeMd := do match t.val with | .Local ref => getVarType ref | .Declare param => pure param.type | .Field _ fieldName => getVarType fieldName - if valueTy.val != HighType.TVoid then - let targetTys ← targets'.mapM targetType - let assignedTy : HighTypeMd := match targetTys with - | [single] => single - | _ => { val := .MultiValuedExpr targetTys, source := source } - checkSubtype source assignedTy valueTy + let targetTys ← targets'.mapM targetType + let expectedTy : HighTypeMd := match targetTys with + | [single] => single + | _ => { val := .MultiValuedExpr targetTys, source := source } + let value' ← Check.resolveStmtExpr value expectedTy unless expected.val matches .TVoid do - checkSubtype source expected valueTy + checkSubtype source expected expectedTy pure { val := .Assign targets' value', source := source } termination_by (exprMd, 0) decreasing_by diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 862e0f7348..827bb2757d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -337,12 +337,16 @@ $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \mathsf{TVo ### Assignment -$$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Rightarrow T_e \quad T_e <: \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assign)}` +$$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Leftarrow \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathit{ExpectedTy}} \quad \text{([⇒] Assign)}` + +$$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Leftarrow \mathit{ExpectedTy} \quad \mathit{ExpectedTy} <: T}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Leftarrow T} \quad \text{([⇐] Assign)}` where `ExpectedTy = T_1` if `|targets| = 1` and `MultiValuedExpr [T_1; …; T_n]` otherwise. The target's declared type `T_i` comes from the variable's scope entry (for {name Strata.Laurel.Variable.Local}`Local` and {name Strata.Laurel.Variable.Field}`Field`) -or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. +or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. The +RHS receives `ExpectedTy` via `Check.resolveStmtExpr`, so bidirectional rules in the +RHS propagate the assignment's type into nested constructs. {docstring Strata.Laurel.Resolution.Synth.assign} From 971c39dde386881f5ffc1d6b22494d5e1175e632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:49:47 -0400 Subject: [PATCH 122/128] fix return type --- Strata/Languages/Laurel/Resolution.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index a2986a5d83..2bbf1c92ce 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -665,7 +665,7 @@ def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : Resolv | .Var (.Declare param) => Check.varDeclare param expected source | .While cond invs dec body => Check.while exprMd cond invs dec body expected source (by rw [h_node]) - | .Exit target => pure (Check.exit target expected source) + | .Exit target => Check.exit target expected source | .Return val => Check.return exprMd val expected source (by rw [h_node]) | .Assert ⟨condExpr, summary⟩ => From f3da746a495decdf6cd704cecf7c668a9d97eaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:54:13 -0400 Subject: [PATCH 123/128] =?UTF-8?q?add=20[=E2=87=90]=20Assign=20entry=20to?= =?UTF-8?q?=20typing-rule=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/verso/LaurelDoc.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 827bb2757d..86fef7e8e3 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -217,7 +217,7 @@ direction explicit. \[⇐\] Return-None, \[⇐\] Return-Some, \[⇐\] Return-Void-Error, \[⇐\] Return-Multi-Error; \[⇐\] While - *Verification statements* — \[⇐\] Assert, \[⇐\] Assume -- *Assignment* — \[⇒\] Assign +- *Assignment* — \[⇒\] Assign, \[⇐\] Assign - *Calls* — \[⇒\] Static-Call, \[⇒\] Static-Call-Multi, \[⇒\] Instance-Call - *Primitive operations* — \[⇒\] Op-Bool, \[⇒\] Op-Cmp, \[⇒\] Op-Eq, \[⇒\] Op-Arith, \[⇒\] Op-Concat From 5717a725d9c7ca1d34a03adc1d65099bda02b64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 16:56:53 -0400 Subject: [PATCH 124/128] remove synthesis rule for assign Synth.assign is gone; only Check.assign remains. Expression-position assignments (e.g. x ++ (y := s)) now hit the synth wildcard like every other migrated control-flow construct, producing the same "no synthesis rule" diagnostic. No test in the suite uses assignment in expression position; idiomatic Laurel only uses assign as a statement. The deleted synth rule is preserved on leo/synth-assign-expr-position. Co-Authored-By: Claude Opus 4.7 (1M context) --- Strata/Languages/Laurel/Resolution.lean | 81 +++---------------------- docs/verso/LaurelDoc.lean | 6 +- 2 files changed, 11 insertions(+), 76 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 2bbf1c92ce..2f2de9fabd 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -542,7 +542,7 @@ inside the mutual block below. Helpers are grouped by section to mirror the - Control flow — `Check.while`, `Check.exit`, `Check.return`, `Check.block`, `Check.ifThenElse` - Verification statements — `Check.assert`, `Check.assume` -- Assignment — `Synth.assign`, `Check.assign` +- Assignment — `Check.assign` - Calls — `Synth.staticCall`, `Synth.instanceCall` - Primitive operations — `Synth.primitiveOp` - Object forms — `Synth.new`, `Synth.asType`, `Synth.isType`, `Synth.refEq`, @@ -592,8 +592,6 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy | .Var (.Local ref) => Synth.varLocal ref source | .Var (.Field target fieldName) => Synth.varField exprMd target fieldName source (by rw [h_node]) - | .Assign targets value => - Synth.assign exprMd targets value source (by rw [h_node]) | .PureFieldUpdate target fieldName newVal => Synth.pureFieldUpdate exprMd target fieldName newVal (by rw [h_node]) | .StaticCall callee args => @@ -966,79 +964,20 @@ def Check.assume (exprMd : StmtExprMd) -- ### Assignment -/-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇐ ExpectedTy ∴ Γ ⊢ Assign targets e ⇒ ExpectedTy` - - where `ExpectedTy = T_1` if `|targets| = 1`, else `MultiValuedExpr [T_1; …; T_n]`. - - Each target's declared type `T_i` (from `Local`, `Field`, or fresh - `Declare`) is collapsed into a tuple `ExpectedTy` (single type if one - target, otherwise `MultiValuedExpr [T_1; …; T_n]`) and pushed into - the RHS via `Check.resolveStmtExpr`. This means the RHS's bidirectional - rules (e.g. `Check.ifThenElse`, `Check.block`) propagate `ExpectedTy` - inward: `var x: int := if c then a else b` checks each branch against - `int` directly, with errors fired at the offending branch. - - Multi-target forms produce a single tuple-vs-tuple check: when the - RHS is itself `MultiValuedExpr` (a multi-output procedure call), both - arity and per-position type mismatches surface in a single diagnostic - of shape *"expected '(int, int, int)', got '(int, string)'"*. - - The synthesized type is `ExpectedTy`, so expression-position - assignments like `x ++ (y := s)` see the target type in the second - operand. -/ -def Synth.assign (exprMd : StmtExprMd) - (targets : List VariableMd) (value : StmtExprMd) (source : Option FileRange) - (h : exprMd.val = .Assign targets value) : - ResolveM (StmtExpr × HighTypeMd) := do - let targets' ← targets.attach.mapM fun ⟨v, _⟩ => do - let ⟨vv, vs⟩ := v - match vv with - | .Local ref => - let ref' ← resolveRef ref source - pure (⟨.Local ref', vs⟩ : VariableMd) - | .Field target fieldName => - let (target', _) ← Synth.resolveStmtExpr target - let fieldName' ← resolveFieldRef target' fieldName source - pure (⟨.Field target' fieldName', vs⟩ : VariableMd) - | .Declare param => - let ty' ← resolveHighType param.type - let name' ← defineNameCheckDup param.name (.var param.name ty') - pure (⟨.Declare ⟨name', ty'⟩, vs⟩ : VariableMd) - let targetType (t : VariableMd) : ResolveM HighTypeMd := do - match t.val with - | .Local ref => getVarType ref - | .Declare param => pure param.type - | .Field _ fieldName => getVarType fieldName - let targetTys ← targets'.mapM targetType - let expectedTy : HighTypeMd := match targetTys with - | [single] => single - | _ => { val := .MultiValuedExpr targetTys, source := source } - let value' ← Check.resolveStmtExpr value expectedTy - pure (.Assign targets' value', expectedTy) - termination_by (exprMd, 1) - decreasing_by - all_goals - apply Prod.Lex.left - have hsz := exprMd.sizeOf_val_lt - simp [h] at hsz - try simp_all - try (have := List.sizeOf_lt_of_mem ‹_ ∈ targets›; simp_all) - omega - /-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇐ ExpectedTy, ExpectedTy <: T ∴ Γ ⊢ Assign targets e ⇐ T` where `ExpectedTy = T_1` if `|targets| = 1`, else `MultiValuedExpr [T_1; …; T_n]`. - Like `Synth.assign`, the target tuple type is pushed into the RHS so - bidirectional rules in the RHS receive the assignment's type. The - outer subsumption `ExpectedTy <: T` accommodates use as a statement - (`T = TVoid`, no value to compare) or as an expression - (`T ≠ TVoid`, the result type must match). When `T = TVoid` the - subsumption is satisfied trivially since `_ <: TVoid` only when the - LHS is also `TVoid` — the assignment value is discarded in statement - position and we want no further check, so the subsumption is - skipped. -/ + The target tuple type is pushed into the RHS via + `Check.resolveStmtExpr`, so bidirectional rules in the RHS receive + the assignment's type. The outer subsumption `ExpectedTy <: T` + accommodates use as a statement (`T = TVoid`, no value to compare) + or as an expression (`T ≠ TVoid`, the result type must match). When + `T = TVoid` the subsumption is satisfied trivially since `_ <: TVoid` + only when the LHS is also `TVoid` — the assignment value is + discarded in statement position and we want no further check, so the + subsumption is skipped. -/ def Check.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 86fef7e8e3..43e92692a6 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -217,7 +217,7 @@ direction explicit. \[⇐\] Return-None, \[⇐\] Return-Some, \[⇐\] Return-Void-Error, \[⇐\] Return-Multi-Error; \[⇐\] While - *Verification statements* — \[⇐\] Assert, \[⇐\] Assume -- *Assignment* — \[⇒\] Assign, \[⇐\] Assign +- *Assignment* — \[⇐\] Assign - *Calls* — \[⇒\] Static-Call, \[⇒\] Static-Call-Multi, \[⇒\] Instance-Call - *Primitive operations* — \[⇒\] Op-Bool, \[⇒\] Op-Cmp, \[⇒\] Op-Eq, \[⇒\] Op-Arith, \[⇒\] Op-Concat @@ -337,8 +337,6 @@ $$`\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \mathsf{TVo ### Assignment -$$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Leftarrow \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathit{ExpectedTy}} \quad \text{([⇒] Assign)}` - $$`\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Leftarrow \mathit{ExpectedTy} \quad \mathit{ExpectedTy} <: T}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Leftarrow T} \quad \text{([⇐] Assign)}` where `ExpectedTy = T_1` if `|targets| = 1` and `MultiValuedExpr [T_1; …; T_n]` otherwise. @@ -348,8 +346,6 @@ or from the {name Strata.Laurel.Variable.Declare}`Declare`-bound parameter type. RHS receives `ExpectedTy` via `Check.resolveStmtExpr`, so bidirectional rules in the RHS propagate the assignment's type into nested constructs. -{docstring Strata.Laurel.Resolution.Synth.assign} - {docstring Strata.Laurel.Resolution.Check.assign} ### Calls From 88e01cb65162e56d70ca45f780dc9005f328bbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Wed, 20 May 2026 17:13:15 -0400 Subject: [PATCH 125/128] better docstrings --- Strata/Languages/Laurel/Resolution.lean | 116 +++++++++++++----------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 2f2de9fabd..3f85e99a27 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -554,8 +554,10 @@ inside the mutual block below. Helpers are grouped by section to mirror the - ContractOf — `Synth.contractOf` - Holes — `Synth.hole`, `Check.holeNone` -The dispatch functions `Synth.resolveStmtExpr` and `Check.resolveStmtExpr` simply pattern-match -on the constructor and delegate to the corresponding helper. -/ +The dispatch functions `Synth.resolveStmtExpr` and `Check.resolveStmtExpr` +pattern-match on the constructor and delegate to the corresponding helper. +`Synth.resolveStmtExpr` is non-total: constructors without a synthesis rule +hit a wildcard arm that emits a diagnostic and returns `Unknown`. -/ namespace Resolution @@ -568,19 +570,19 @@ mutual -- ### Dispatch /-- Synth-mode resolution: resolve `e` and synthesize its `HighType`, - written `Γ ⊢ e ⇒ T`. Each constructor delegates to its rule's helper. - - Synthesis returns a type inferred from the expression itself; checking - (`Check.resolveStmtExpr`) verifies that the expression has a given expected - type. Each construct picks a mode based on whether its type is - determined locally (synth) or by context (check). Synth rules invoke - check on subexpressions whose expected type is known (e.g. - `cond ⇐ TBool` in `IfThenElse`); `Check.resolveStmtExpr` falls back to - `Synth.resolveStmtExpr` via subsumption (rule `[⇐] Sub`). The two functions - are mutually recursive, with termination on a lexicographic measure - `(exprMd, tag)` — tag `0` for check, `1` for synth — so that - subsumption (which calls synth on the *same* expression) can decrease - via `Prod.Lex.right`. -/ + written `Γ ⊢ e ⇒ T`. Each constructor with a synthesis rule delegates + to its rule's helper; constructors without one (statement-shaped + constructs like `IfThenElse`, `Block`, `While`, `Return`, `Assign`, + …) hit a wildcard arm that emits a `typeMismatch` diagnostic and + returns `Unknown` to suppress cascading errors. + + Synthesis returns a type inferred from the expression itself; + checking (`Check.resolveStmtExpr`) verifies that the expression has + a given expected type. The two functions are mutually recursive, + with termination on a lexicographic measure `(exprMd, tag)` — tag + `2` for synth, `3` for check, helpers smaller — so that subsumption + (which calls synth on the *same* expression) can decrease via + `Prod.Lex.right`. -/ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTypeMd) := do match h_node: exprMd with | AstNode.mk expr source => @@ -634,21 +636,27 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy /-- Check-mode resolution (rule **Sub** at the boundary): resolve `e` and verify its type is a consistent subtype of `expected`, written - `Γ ⊢ e ⇐ T`. Bidirectional rules for individual constructs (`Block`, - `IfThenElse`, `Assign`, `Hole`) push `expected` into subexpressions - rather than bouncing through synthesis, which keeps error messages - localized and lets the expected type propagate through nested control - flow. Everything else falls back to subsumption — synthesize, then - verify `isConsistentSubtype actual expected`. + `Γ ⊢ e ⇐ T`. Bidirectional rules for individual constructs push + `expected` into subexpressions rather than bouncing through + synthesis, which keeps error messages localized and lets the + expected type propagate through nested control flow. Constructs + with a dedicated `Check.` rule: + + - bindings — `Var (.Declare …)`, `Assign` + - control flow — `Block`, `IfThenElse`, `While`, `Exit`, `Return` + - verification — `Assert`, `Assume`, `Old`, `ProveBy` + - holes — untyped `Hole` + + Everything else falls back to subsumption — synthesize, then verify + `isConsistentSubtype actual expected`. The right principle for new call sites is: when the position has a known expected type (`TBool` for conditions, numeric for `decreases`, the declared output for a constant initializer or a functional body), - use `Check.resolveStmtExpr`. When it doesn't, use `resolveStmtExpr` (a thin - wrapper that calls `Synth.resolveStmtExpr` and discards the synthesized type, - used at sites where typing is not enforced — verification annotations, - modifies/reads clauses). `Synth.resolveStmtExpr` itself is mostly an internal - interface used by other rules. -/ + use `Check.resolveStmtExpr`. When it doesn't, use `resolveStmtExpr` + (a thin wrapper that calls `Synth.resolveStmtExpr` and discards the + synthesized type, used at sites where typing is not enforced — + verification annotations, modifies/reads clauses). -/ def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : ResolveM StmtExprMd := do match h_node: exprMd with | AstNode.mk expr source => @@ -964,20 +972,20 @@ def Check.assume (exprMd : StmtExprMd) -- ### Assignment -/-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇐ ExpectedTy, ExpectedTy <: T ∴ Γ ⊢ Assign targets e ⇐ T` +/-- `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇐ ExpectedTy, ExpectedTy <: T ∴ Γ ⊢ Assign targets e ⇐ T` (T ≠ TVoid) + + `Γ ⊢ targets_i ⇒ T_i, Γ ⊢ e ⇐ ExpectedTy ∴ Γ ⊢ Assign targets e ⇐ TVoid` where `ExpectedTy = T_1` if `|targets| = 1`, else `MultiValuedExpr [T_1; …; T_n]`. The target tuple type is pushed into the RHS via `Check.resolveStmtExpr`, so bidirectional rules in the RHS receive - the assignment's type. The outer subsumption `ExpectedTy <: T` - accommodates use as a statement (`T = TVoid`, no value to compare) - or as an expression (`T ≠ TVoid`, the result type must match). When - `T = TVoid` the subsumption is satisfied trivially since `_ <: TVoid` - only when the LHS is also `TVoid` — the assignment value is - discarded in statement position and we want no further check, so the - subsumption is skipped. -/ + the assignment's type. When `T ≠ TVoid` (expression position) the + outer subsumption `ExpectedTy <: T` is enforced. When `T = TVoid` + (statement position) the subsumption is skipped: the assignment's + value is discarded as a statement, so there is nothing to compare + against `expected`. -/ def Check.assign (exprMd : StmtExprMd) (targets : List VariableMd) (value : StmtExprMd) (expected : HighTypeMd) (source : Option FileRange) @@ -1086,30 +1094,32 @@ def Synth.instanceCall (exprMd : StmtExprMd) -- ### Primitive operations -/-- Cases on the operator family. +/-- Cases on the operator family. All operands are synthesized first; + then a per-family verification fires using `checkSubtype` (a post-synth + subtype test, not bidirectional check resolution). - `Γ ⊢ args_i ⇐ TBool, op ∈ {And, Or, AndThen, OrElse, Not, Implies} ∴ Γ ⊢ PrimitiveOp op args ⇒ TBool` + `Γ ⊢ args_i ⇒ U_i, U_i <: TBool, op ∈ {And, Or, AndThen, OrElse, Not, Implies} ∴ Γ ⊢ PrimitiveOp op args ⇒ TBool` - `Γ ⊢ args_i ⇐ Numeric, op ∈ {Lt, Leq, Gt, Geq} ∴ Γ ⊢ PrimitiveOp op args ⇒ TBool` + `Γ ⊢ args_i ⇒ U_i, Numeric U_i, op ∈ {Lt, Leq, Gt, Geq} ∴ Γ ⊢ PrimitiveOp op args ⇒ TBool` `Γ ⊢ lhs ⇒ T_l, Γ ⊢ rhs ⇒ T_r, T_l ~ T_r, op ∈ {Eq, Neq} ∴ Γ ⊢ PrimitiveOp op [lhs; rhs] ⇒ TBool` - `Γ ⊢ args_i ⇐ Numeric, Γ ⊢ args.head ⇒ T, op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} ∴ Γ ⊢ PrimitiveOp op args ⇒ T` - - `Γ ⊢ args_i ⇐ TString, op = StrConcat ∴ Γ ⊢ PrimitiveOp op args ⇒ TString` - - Each operator family has its own argument-type discipline and result - type. Arguments are synthesized first, then the per-family check fires: - `⇐ TBool` for booleans, `Numeric` (consistent with `TInt`, `TReal`, or - `TFloat64`) for arithmetic/comparison, consistency `~` for equality - (symmetric — no subtype direction is privileged), `⇐ TString` for - concatenation. The result type is `TBool` for - booleans/comparisons/equality, the head argument's type for arithmetic - ("result is the type of the first argument" handles `int + int → int`, - `real + real → real`, etc. without unification — known relaxation: - `int + real` passes since each operand individually passes `Numeric`; - a proper fix needs numeric promotion or unification), `TString` for - concatenation. -/ + `Γ ⊢ args_i ⇒ U_i, Numeric U_i, Γ ⊢ args.head ⇒ T, op ∈ {Neg, Add, Sub, Mul, Div, Mod, DivT, ModT} ∴ Γ ⊢ PrimitiveOp op args ⇒ T` + + `Γ ⊢ args_i ⇒ U_i, U_i <: TString, op = StrConcat ∴ Γ ⊢ PrimitiveOp op args ⇒ TString` + + `Numeric T` is the predicate "T unfolds to TInt / TReal / TFloat64 + (or Unknown via the gradual escape hatch)" — not a single type, so it + cannot serve as an `expected` for `Check.resolveStmtExpr`. `~` is + symmetric consistency under the gradual relation, so equality has no + privileged operand direction. + + The result type is `TBool` for booleans/comparisons/equality, the + head argument's type for arithmetic ("result is the type of the + first argument" handles `int + int → int`, `real + real → real`, + etc. without unification — known relaxation: `int + real` passes + since each operand individually passes `Numeric`; a proper fix needs + numeric promotion or unification), `TString` for concatenation. -/ def Synth.primitiveOp (exprMd : StmtExprMd) (expr : StmtExpr) (op : Operation) (args : List StmtExprMd) (source : Option FileRange) (h_expr : expr = .PrimitiveOp op args) From f0016e6a99dff032eeb46bce80f556f8d6cf93cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Thu, 21 May 2026 09:45:29 -0400 Subject: [PATCH 126/128] move holes to check-only, roll back InferHoleTypes Synth.hole is gone; both flavors of Hole are now check-only: - Check.holeSome validates a user annotation T_h against the surrounding expected T via subsumption (T_h <: T) and preserves the annotation on the node. - Check.holeNone records the surrounding expected type as the hole's annotation, same as before. A Hole reaching synth mode hits the dispatcher's wildcard arm and produces a "no synthesis rule" diagnostic. The Hole-None-Check pre-annotation guarantee from earlier is now extended: every hole reachable in a check-mode position carries a type after resolution. InferHoleTypes.lean is restored to the pre-branch version (parent of 40e15722). The consistency-check logic that flagged disagreement between resolution-time and inference-time types is gone, along with the typeContext threading that supported it. The deleted Synth.hole rule is preserved on leo/synth-hole. Co-Authored-By: Claude Opus 4.7 (1M context) --- Strata/Languages/Laurel/InferHoleTypes.lean | 21 +------- Strata/Languages/Laurel/Resolution.lean | 55 ++++++++------------- docs/verso/LaurelDoc.lean | 18 +++---- 3 files changed, 31 insertions(+), 63 deletions(-) diff --git a/Strata/Languages/Laurel/InferHoleTypes.lean b/Strata/Languages/Laurel/InferHoleTypes.lean index 248d90716d..d56ad86881 100644 --- a/Strata/Languages/Laurel/InferHoleTypes.lean +++ b/Strata/Languages/Laurel/InferHoleTypes.lean @@ -51,8 +51,6 @@ inductive InferHoleTypesStats where structure InferHoleState where model : SemanticModel - /-- Type-relation tables used by the consistency check on pre-annotated holes. -/ - typeContext : TypeContext currentOutputType : HighTypeMd statistics : Statistics := {} diagnostics : List DiagnosticModel := [] @@ -89,7 +87,7 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol match expr with | AstNode.mk val source => match val with - | .Hole det existingTy => + | .Hole det _ => if expectedType.val == .Unknown then modify fun s => { s with statistics := s.statistics.increment s!"{InferHoleTypesStats.holesLeftUnknown}" @@ -97,18 +95,6 @@ private def inferExpr (expr : StmtExprMd) (expectedType : HighTypeMd) : InferHol } return expr else - -- If the hole already carried a type (from resolution's Hole-None-Check - -- rule, or from a user-written `?: T`), flag a conflict when the two - -- types disagree under consistency (gradual ~). - match existingTy with - | some prior => - let ctx := (← get).typeContext - unless isConsistent ctx prior expectedType do - modify fun s => { s with - diagnostics := s.diagnostics ++ [diagnosticFromSource source - s!"hole annotated with '{formatHighTypeVal prior.val}' but context expects '{formatHighTypeVal expectedType.val}'"] - } - | none => pure () modify fun s => { s with statistics := s.statistics.increment s!"{InferHoleTypesStats.holesAnnotated}" } return ⟨.Hole det (some expectedType), source⟩ | .PrimitiveOp op args => @@ -186,10 +172,7 @@ private def inferProcedure (proc : Procedure) : InferHoleM Procedure := do Annotate every `.Hole` in the program with a type inferred from context. -/ def inferHoleTypes (model : SemanticModel) (program : Program) : Program × List DiagnosticModel × Statistics := - let initState : InferHoleState := { - model := model, - typeContext := TypeContext.ofTypes program.types, - currentOutputType := { val := .Unknown, source := none } } + let initState : InferHoleState := { model := model, currentOutputType := { val := .Unknown, source := none }} let (procs, finalState) := (program.staticProcedures.mapM inferProcedure).run initState ({ program with staticProcedures := procs }, finalState.diagnostics, finalState.statistics) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 3f85e99a27..77ea08c5e6 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -119,10 +119,11 @@ A few open structural questions worth recording — see the *Type checking* sect Resolution already synthesizes those types and discards them. Caching per-node types on `SemanticModel` (or directly on the AST) would let the later passes look them up instead of recomputing. -- *Shrink or remove `InferHoleTypes`.* `Hole-None-Check` already records expected types - during resolution for holes in check-mode positions. Holes in synth-only positions still - need the post-pass, but as more constructs gain bespoke check rules, fewer holes need - it; eventually the pass can go away. +- *Shrink or remove `InferHoleTypes`.* Holes are check-only now: `Hole-Some` validates + user annotations against the surrounding type, and `Hole-None` records the expected + type for untyped holes. Every hole reachable in a check-mode position already carries + a type after resolution; the inference pass is left handling whatever residue remains + and can plausibly be deleted entirely. -/ namespace Strata.Laurel @@ -552,7 +553,7 @@ inside the mutual block below. Helpers are grouped by section to mirror the - Self reference — `Synth.this` - Untyped forms — `Synth.abstract`, `Synth.all` - ContractOf — `Synth.contractOf` -- Holes — `Synth.hole`, `Check.holeNone` +- Holes — `Check.holeSome`, `Check.holeNone` The dispatch functions `Synth.resolveStmtExpr` and `Check.resolveStmtExpr` pattern-match on the constructor and delegate to the corresponding helper. @@ -620,7 +621,6 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy Synth.contractOf exprMd ty fn source (by rw [h_node]) | .Abstract => pure (Synth.abstract source) | .All => pure (Synth.all source) - | .Hole det type => Synth.hole det type source | _ => let unknown : HighTypeMd := { val := .Unknown, source := source } typeMismatch source (some expr) @@ -645,7 +645,7 @@ def Synth.resolveStmtExpr (exprMd : StmtExprMd) : ResolveM (StmtExprMd × HighTy - bindings — `Var (.Declare …)`, `Assign` - control flow — `Block`, `IfThenElse`, `While`, `Exit`, `Return` - verification — `Assert`, `Assume`, `Old`, `ProveBy` - - holes — untyped `Hole` + - holes — `Hole` (typed and untyped) Everything else falls back to subsumption — synthesize, then verify `isConsistentSubtype actual expected`. @@ -668,6 +668,7 @@ def Check.resolveStmtExpr (exprMd : StmtExprMd) (expected : HighTypeMd) : Resolv | .Assign targets value => Check.assign exprMd targets value expected source (by rw [h_node]) | .Hole det none => pure (Check.holeNone det expected source) + | .Hole det (some ty) => Check.holeSome det ty expected source | .Var (.Declare param) => Check.varDeclare param expected source | .While cond invs dec body => Check.while exprMd cond invs dec body expected source (by rw [h_node]) @@ -1496,38 +1497,24 @@ def Synth.contractOf (exprMd : StmtExprMd) -- ### Holes -/-- Cases on whether the hole has a type annotation. +/-- `T_h <: T ∴ Γ ⊢ Hole d (some T_h) ⇐ T` - `Γ ⊢ Hole d (some T) ⇒ T` - - `Γ ⊢ Hole d none ⇒ Unknown` - - A typed hole synthesizes its annotation; an untyped hole in synth - position synthesizes `Unknown`. -/ -def Synth.hole (det : Bool) (type : Option HighTypeMd) (source : Option FileRange) : - ResolveM (StmtExpr × HighTypeMd) := do - match type with - | some ty => - let ty' ← resolveHighType ty - pure (.Hole det ty', ty') - | none => pure (.Hole det none, { val := .Unknown, source := source }) + A typed hole carries the user's annotation `T_h`. The annotation is + resolved and verified against the surrounding `expected` type via + subsumption; the resolved annotation is preserved on the node so + downstream passes (hole elimination) can generate correctly typed + uninterpreted functions. -/ +def Check.holeSome (det : Bool) (ty : HighTypeMd) (expected : HighTypeMd) + (source : Option FileRange) : ResolveM StmtExprMd := do + let ty' ← resolveHighType ty + checkSubtype source expected ty' + pure { val := .Hole det (some ty'), source := source } /-- `Γ ⊢ Hole d none ⇐ T ↦ Γ ⊢ Hole d (some T)` An untyped hole in check mode records the expected type on the node - so downstream passes don't have to infer it - again. The subsumption check is trivial (`Unknown <: T` always holds), - so this rule never fails — it just preserves the type information - available at the check-mode boundary instead of discarding it. - - A separate `InferHoleTypes` pass still runs after resolution to - annotate holes that ended up in synth-only positions. When that pass - encounters a hole whose type was already set (by `[⇐] Hole-None` or by - a user-written `?: T`), it checks the resolution-time and - inference-time types for consistency under `~`; a disagreement fires - the diagnostic *"hole annotated with 'T_resolution' but context - expects 'T_inference'"*, surfacing what would otherwise be a silent - overwrite. -/ + so downstream passes (hole elimination) don't have to infer it + again. -/ def Check.holeNone (det : Bool) (expected : HighTypeMd) (source : Option FileRange) : StmtExprMd := { val := .Hole det (some expected), source := source } diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index 43e92692a6..f668685da4 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -228,7 +228,7 @@ direction explicit. - *Self reference* — \[⇒\] This-Inside, \[⇒\] This-Outside - *Untyped forms* — \[⇒\] Abstract / All - *ContractOf* — \[⇒\] ContractOf-Bool, \[⇒\] ContractOf-Set, \[⇒\] ContractOf-Error -- *Holes* — \[⇒\] Hole-Some, \[⇒\] Hole-None, \[⇐\] Hole-None +- *Holes* — \[⇐\] Hole-Some, \[⇐\] Hole-None ### Subsumption @@ -457,11 +457,9 @@ $$`\frac{\mathit{fn} \text{ is not a procedure reference}}{\Gamma \vdash \mathsf ### Holes -$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T) \Rightarrow T} \quad \text{([⇒] Hole-Some)}` +$$`\frac{T_h <: T}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T_h) \Leftarrow T} \quad \text{([⇐] Hole-Some)}` -$$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-None)}` - -{docstring Strata.Laurel.Resolution.Synth.hole} +{docstring Strata.Laurel.Resolution.Check.holeSome} $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapsto\;\; \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}` @@ -502,11 +500,11 @@ just wasted work and a maintenance hazard. ### Shrink or remove `InferHoleTypes` `InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that -\[⇐\] Hole-None writes the expected type during resolution for holes in check-mode -positions, the post-pass only needs to handle holes in synth-only positions (e.g. call -arguments resolved through `Synth.resolveStmtExpr` instead of `Check.resolveStmtExpr`). As more constructs -gain bespoke check rules, fewer holes will reach `InferHoleTypes`; eventually the pass -can be deleted entirely. +holes are check-only — \[⇐\] Hole-Some validates user annotations against context, and +\[⇐\] Hole-None records the expected type for untyped holes — every hole reachable in a +check-mode position already carries a type after resolution. `InferHoleTypes` is left +with whatever residue (in principle nothing, since synth-position holes are now flagged +as errors at resolution time and don't reach the inference pass). # Translation Pipeline From 778faf7d8977dbabb52945d700dc380e275756cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Thu, 21 May 2026 10:08:17 -0400 Subject: [PATCH 127/128] remove future structural changes entirely --- Strata/Languages/Laurel/Resolution.lean | 20 ------------ docs/verso/LaurelDoc.lean | 41 ------------------------- 2 files changed, 61 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 77ea08c5e6..51fdfaeaaa 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -104,26 +104,6 @@ Each of these nodes carries a `uniqueId : Option Nat` field (defaulting to `none`). Phase 1 fills in unique values; Phase 2 then builds a map from reference IDs to `ResolvedNode` values describing the definition each reference resolves to. - -## Future structural changes - -A few open structural questions worth recording — see the *Type checking* section of -`LaurelDoc.lean` for context. - -- *Rename to `NameTypeResolution`.* This pass resolves names and type-checks expressions in - one walk. The current name only mentions half of what it does. `NameTypeResolution.lean` - (or similar) would advertise both responsibilities. -- *Eliminate `LaurelTypes.computeExprType` by caching types.* Five later passes - (`LaurelToCoreTranslator`, `ModifiesClauses`, `LiftImperativeExpressions`, - `HeapParameterization`, `TypeHierarchy`) re-derive `StmtExpr` types after resolution. - Resolution already synthesizes those types and discards them. Caching per-node types on - `SemanticModel` (or directly on the AST) would let the later passes look them up instead - of recomputing. -- *Shrink or remove `InferHoleTypes`.* Holes are check-only now: `Hole-Some` validates - user annotations against the surrounding type, and `Hole-None` records the expected - type for untyped holes. Every hole reachable in a check-mode position already carries - a type after resolution; the inference pass is left handling whatever residue remains - and can plausibly be deleted entirely. -/ namespace Strata.Laurel diff --git a/docs/verso/LaurelDoc.lean b/docs/verso/LaurelDoc.lean index f668685da4..71c6595c6d 100644 --- a/docs/verso/LaurelDoc.lean +++ b/docs/verso/LaurelDoc.lean @@ -465,47 +465,6 @@ $$`\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \;\;\mapst {docstring Strata.Laurel.Resolution.Check.holeNone} -## Future structural changes - -The current pipeline has resolution and several downstream passes that recompute or -re-derive type information that resolution already synthesized. A few cleanups worth -considering: - -### Rename `Resolution.lean` → `NameTypeResolution.lean` - -The pass resolves names *and* type-checks expressions in one walk; the file name only -advertises the first half. A rename (e.g. `NameTypeResolution.lean` or -`ResolutionAndTyping.lean`) would describe what the pass actually does. The -`SemanticModel` and `ResolvedNode` types could keep their names — they're about resolved -references, not typing. - -### Eliminate `LaurelTypes.computeExprType` by caching types - -`LaurelTypes.lean` exports `computeExprType : SemanticModel → StmtExprMd → HighTypeMd`, -which five later passes call (`LaurelToCoreTranslator`, `ModifiesClauses`, -`LiftImperativeExpressions`, `HeapParameterization`, `TypeHierarchy`) to ask "what's the -type of this expression?" after resolution. Resolution already synthesizes the same types -during its walk, then discards them. Two ways to remove the duplication: - -- *Cache types on the AST.* Add a `HighTypeMd` field to `StmtExpr` (or a parallel - `Std.HashMap Nat HighTypeMd` keyed by node-id, attached to `SemanticModel`), populate it - during resolution, and have later passes read it. `computeExprType` becomes a lookup, - not a re-traversal. -- *Make the cache opt-in.* Same idea, but only enable the type-cache for passes that need - it. Less invasive but partially defeats the point. - -The duplication isn't a correctness issue today (both paths produce consistent results), -just wasted work and a maintenance hazard. - -### Shrink or remove `InferHoleTypes` - -`InferHoleTypes` walks the post-resolution AST a second time to annotate holes. Now that -holes are check-only — \[⇐\] Hole-Some validates user annotations against context, and -\[⇐\] Hole-None records the expected type for untyped holes — every hole reachable in a -check-mode position already carries a type after resolution. `InferHoleTypes` is left -with whatever residue (in principle nothing, since synth-position holes are now flagged -as errors at resolution time and don't reach the inference pass). - # Translation Pipeline Laurel programs are verified by translating them to Strata Core and then invoking the Core From b649e94d26d42b9d6085205cd928a0c9605401c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20LEESCO?= Date: Thu, 21 May 2026 13:27:58 -0400 Subject: [PATCH 128/128] update rules --- Strata/Languages/Laurel/Resolution.lean | 102 +++++++++++++++--------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/Strata/Languages/Laurel/Resolution.lean b/Strata/Languages/Laurel/Resolution.lean index 51fdfaeaaa..0e002ec048 100644 --- a/Strata/Languages/Laurel/Resolution.lean +++ b/Strata/Languages/Laurel/Resolution.lean @@ -770,26 +770,30 @@ def Check.while (exprMd : StmtExprMd) try simp_all omega -/-- `TVoid <: T ∴ Γ ⊢ Exit target ⇐ T` -/ -def Check.exit (target : String) (expected : HighTypeMd) +/-- `Γ ⊢ Exit target ⇐ T` + + `exit` is a control-flow jump out of a labeled block; it doesn't + deliver a value to the enclosing block, so no subsumption against + `expected` is required. -/ +def Check.exit (target : String) (_expected : HighTypeMd) (source : Option FileRange) : ResolveM StmtExprMd := do - checkSubtype source expected { val := .TVoid, source := source } pure { val := .Exit target, source := source } /-- Cases on whether the return value is `none` or `some e`, and on the arity of the enclosing procedure's declared outputs. - `TVoid <: T ∴ Γ ⊢ Return none ⇐ T` + `Γ ⊢ Return none ⇐ T` - `Γ_proc.outputs = [T_o], Γ ⊢ e ⇐ T_o, TVoid <: T ∴ Γ ⊢ Return (some e) ⇐ T` + `Γ_proc.outputs = [T_o], Γ ⊢ e ⇐ T_o ∴ Γ ⊢ Return (some e) ⇐ T` `Γ_proc.outputs = [] ∴ Γ ⊢ Return (some e) ↝ error: "void procedure cannot return a value"` `Γ_proc.outputs = [T_1; …; T_n] (n ≥ 2) ∴ Γ ⊢ Return (some e) ↝ error: "multi-output procedure cannot use 'return e'; assign to named outputs instead"` - The `Return` construct itself produces no value, so `expected` must - admit `TVoid`. The optional payload is matched against the enclosing - procedure's declared outputs (threaded through + `return` is a control-flow jump out of the procedure; it doesn't + deliver a value to the enclosing block, so no subsumption against the + surrounding `expected` is required. The optional payload is matched + against the enclosing procedure's declared outputs (threaded through `ResolveState.expectedReturnTypes`, set from `proc.outputs` by `resolveProcedure` / `resolveInstanceProcedure` for the duration of the body; `none` means "no enclosing procedure" — e.g. resolving a @@ -829,7 +833,10 @@ def Check.return (exprMd : StmtExprMd) "multi-output procedure cannot use 'return e'; assign to named outputs instead" modify fun s => { s with errors := s.errors.push diag } | _, none => pure () - checkSubtype source expected { val := .TVoid, source := source } + -- `return` is a control-flow jump; it doesn't deliver a value to the + -- enclosing block, so no TVoid-vs-expected subsumption is required. + -- The return value (if any) was already checked against the declared + -- output above via `expectedReturnTypes`. pure { val := .Return val', source := source } termination_by (exprMd, 0) decreasing_by @@ -842,24 +849,37 @@ def Check.return (exprMd : StmtExprMd) /-- Cases on whether the statement list is empty. - `Γ_0 = Γ, Γ_{i-1} ⊢ s_i ⇒ _ ⊣ Γ_i (1 ≤ i < n), Γ_{n-1} ⊢ s_n ⇐ T ∴ Γ ⊢ Block [s_1; …; s_n] label ⇐ T` + `Γ_0 = Γ, Γ_{i-1} ⊢ s_i ⇐ Unknown ⊣ Γ_i (1 ≤ i < n), Γ_{n-1} ⊢ s_n ⇐ T ∴ Γ ⊢ Block [s_1; …; s_n] label ⇐ T` `TVoid <: T ∴ Γ ⊢ Block [] label ⇐ T` Pushes `expected` into the *last* statement rather than comparing the block's synthesized type at the boundary. Errors fire at the offending subexpression, and `expected` keeps propagating through nested `Block` - / `IfThenElse` / `Hole` / `Quantifier`. Empty blocks reduce to a - subsumption check of `TVoid` against `expected` — the same check - `[⇐] Block-Empty` performs when `T` admits `TVoid`. -/ + / `IfThenElse` / `Hole` / `Quantifier`. + + Non-last statements are checked against `Unknown` so their type is + accepted regardless: this matches the Java/Python/JavaScript discipline + where `f(x);` is a valid statement even when `f` returns a value (the + value is discarded). Routing through check mode (rather than synth) + means that constructs without a synth rule — control-flow constructs + in particular — are still resolved correctly, with their own + bidirectional rules pushing `Unknown` into their subexpressions. The + trade-off is that a stray expression like `5;` is silently accepted; + flagging that belongs to a lint, not the type checker. + + Empty blocks reduce to a subsumption check of `TVoid` against + `expected` — the same check `[⇐] Block-Empty` performs when `T` + admits `TVoid`. -/ def Check.block (exprMd : StmtExprMd) (stmts : List StmtExprMd) (label : Option String) (expected : HighTypeMd) (source : Option FileRange) (h : exprMd.val = .Block stmts label) : ResolveM StmtExprMd := do withScope do + let unknownTy : HighTypeMd := { val := .Unknown, source := source } let init' ← stmts.dropLast.attach.mapM (fun ⟨s, hMem⟩ => do have : s ∈ stmts := List.dropLast_subset stmts hMem - let (s', _) ← Synth.resolveStmtExpr s; pure s') + Check.resolveStmtExpr s unknownTy) match _lastResult: stmts.getLast? with | none => checkSubtype source expected { val := .TVoid, source := source } @@ -1515,21 +1535,38 @@ def resolveParameter (param : Parameter) : ResolveM Parameter := do let name' ← defineNameCheckDup param.name (.parameter ⟨param.name, ty'⟩) return ⟨name', ty'⟩ -/-- Resolve a procedure body. Returns the resolved body and its type. -/ -def resolveBody (body : Body) : ResolveM (Body × HighTypeMd) := do +/-- Resolve a procedure body, checking its impl block (if any) against + `expected`. The expected type comes from the procedure's declared + output: a single output `T` for functional procedures, `TVoid` + otherwise. Bodies without an impl block (`Abstract`, `External`) ignore + `expected`. -/ +def resolveBody (body : Body) (expected : HighTypeMd) : ResolveM Body := do match body with | .Transparent b => - let (b', ty) ← Synth.resolveStmtExpr b - return (.Transparent b', ty) + let b' ← Check.resolveStmtExpr b expected + return .Transparent b' | .Opaque posts impl mods => let posts' ← posts.mapM (·.mapM resolveStmtExpr) - let impl' ← impl.mapM resolveStmtExpr + let impl' ← impl.mapM (Check.resolveStmtExpr · expected) let mods' ← mods.mapM resolveStmtExpr - return (.Opaque posts' impl' mods', { val := .TVoid, source := none }) + return .Opaque posts' impl' mods' | .Abstract posts => let posts' ← posts.mapM (·.mapM resolveStmtExpr) - return (.Abstract posts', { val := .TVoid, source := none }) - | .External => return (.External, { val := .TVoid, source := none }) + return .Abstract posts' + | .External => return .External + +/-- Compute the expected body type for a procedure. Functional + procedures with a single output `T` expect `T` — the body's last + statement is the result and must produce a `T`. Non-functional + procedures expect `Unknown`: their body is statement-typed and the + last statement (if any) is discarded — outputs are observed via + `return e` or named-output assignment, validated independently + inside `Check.return` via `expectedReturnTypes`. -/ +private def procedureBodyType (isFunctional : Bool) (outputs : List Parameter) + (source : Option FileRange) : HighTypeMd := + match isFunctional, outputs with + | true, [singleOutput] => singleOutput.type + | _, _ => { val := .Unknown, source := source } /-- Resolve a procedure: resolve its name, then resolve params, contracts, and body in a new scope. -/ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do @@ -1541,20 +1578,13 @@ def resolveProcedure (proc : Procedure) : ResolveM Procedure := do let dec' ← proc.decreases.mapM resolveStmtExpr let savedReturns := (← get).expectedReturnTypes modify fun s => { s with expectedReturnTypes := some (outputs'.map (·.type)) } - let (body', bodyTy) ← resolveBody proc.body + let bodyExpected := procedureBodyType proc.isFunctional outputs' proc.name.source + let body' ← resolveBody proc.body bodyExpected modify fun s => { s with expectedReturnTypes := savedReturns } if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" modify fun s => { s with errors := s.errors.push diag } - -- Check body type matches declared output type for functional procedures with transparent bodies - if proc.isFunctional && body'.isTransparent then - match proc.outputs with - | [singleOutput] => - -- Only check when body produces a value (not void from return/while/assign) - if bodyTy.val != HighType.TVoid then - checkSubtype proc.name.source singleOutput.type bodyTy - | _ => pure () let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr return { name := procName', inputs := inputs', outputs := outputs', isFunctional := proc.isFunctional, @@ -1585,19 +1615,13 @@ def resolveInstanceProcedure (typeName : Identifier) (proc : Procedure) : Resolv let dec' ← proc.decreases.mapM resolveStmtExpr let savedReturns := (← get).expectedReturnTypes modify fun s => { s with expectedReturnTypes := some (outputs'.map (·.type)) } - let (body', bodyTy) ← resolveBody proc.body + let bodyExpected := procedureBodyType proc.isFunctional outputs' proc.name.source + let body' ← resolveBody proc.body bodyExpected modify fun s => { s with expectedReturnTypes := savedReturns } if !proc.isFunctional && body'.isTransparent then let diag := diagnosticFromSource proc.name.source s!"transparent procedures are not yet supported. Add 'opaque' to make the procedure opaque" modify fun s => { s with errors := s.errors.push diag } - -- Check body type matches declared output type for functional procedures with transparent bodies - if proc.isFunctional && body'.isTransparent then - match proc.outputs with - | [singleOutput] => - if bodyTy.val != HighType.TVoid then - checkSubtype proc.name.source singleOutput.type bodyTy - | _ => pure () let invokeOn' ← proc.invokeOn.mapM resolveStmtExpr modify fun s => { s with instanceTypeName := savedInstType } return { name := procName', inputs := inputs', outputs := outputs',