diff --git a/Strata/DDM/AST.lean b/Strata/DDM/AST.lean index 901dbeb9..df18115e 100644 --- a/Strata/DDM/AST.lean +++ b/Strata/DDM/AST.lean @@ -90,6 +90,9 @@ inductive TypeExprF (α : Type) where | ident (ann : α) (name : QualifiedIdent) (args : Array (TypeExprF α)) /-- A bound type variable at the given deBruijn index in the context. -/ | bvar (ann : α) (index : Nat) + /-- A polymorphic type variable (universally quantified). + Used for polymorphic function type parameters -/ +| tvar (ann : α) (name : String) /-- A reference to a global variable along with any arguments to ensure it is well-typed. -/ | fvar (ann : α) (fvar : FreeVarIndex) (args : Array (TypeExprF α)) /-- A function type. -/ @@ -101,6 +104,7 @@ namespace TypeExprF def ann {α} : TypeExprF α → α | .ident ann _ _ => ann | .bvar ann _ => ann +| .tvar ann _ => ann | .fvar ann _ _ => ann | .arrow ann _ _ => ann @@ -112,6 +116,7 @@ protected def incIndices {α} (tp : TypeExprF α) (count : Nat) : TypeExprF α : | .ident n i args => .ident n i (args.attach.map fun ⟨e, _⟩ => e.incIndices count) | .fvar n f args => .fvar n f (args.attach.map fun ⟨e, _⟩ => e.incIndices count) | .bvar n idx => .bvar n (idx + count) + | .tvar n name => .tvar n name -- tvar doesn't use indices | .arrow n a r => .arrow n (a.incIndices count) (r.incIndices count) /-- Return true if type expression has a bound variable. -/ @@ -119,6 +124,7 @@ protected def hasUnboundVar {α} (bindingCount : Nat := 0) : TypeExprF α → Bo | .ident _ _ args => args.attach.any (fun ⟨e, _⟩ => e.hasUnboundVar bindingCount) | .fvar _ _ args => args.attach.any (fun ⟨e, _⟩ => e.hasUnboundVar bindingCount) | .bvar _ idx => idx ≥ bindingCount +| .tvar _ _ => true | .arrow _ a r => a.hasUnboundVar bindingCount || r.hasUnboundVar bindingCount termination_by e => e @@ -135,6 +141,7 @@ protected def instTypeM {m α} [Monad m] (d : TypeExprF α) (bindings : α → N | .ident n i a => .ident n i <$> a.attach.mapM (fun ⟨e, _⟩ => e.instTypeM bindings) | .bvar n idx => bindings n idx + | .tvar n name => pure (.tvar n name) | .fvar n idx a => .fvar n idx <$> a.attach.mapM (fun ⟨e, _⟩ => e.instTypeM bindings) | .arrow n a b => .arrow n <$> a.instTypeM bindings <*> b.instTypeM bindings termination_by d @@ -472,6 +479,15 @@ def scopeDatatypeIndex (metadata : Metadata) : Option (Nat × Nat) := | some #[.catbvar nameIdx, .catbvar typeParamsIdx] => some (nameIdx, typeParamsIdx) | some _ => panic! s!"Unexpected argument count to scopeDatatype" +/-- Returns the typeArgs index if @[scopeTypeVars] is present. + Used for polymorphic function declarations where type parameters should be + added as .tvar bindings. -/ +def scopeTypeVarsIndex (metadata : Metadata) : Option Nat := + match metadata[q`StrataDDL.scopeTypeVars]? with + | none => none + | some #[.catbvar typeArgsIdx] => some typeArgsIdx + | some _ => panic! s!"Unexpected argument count to scopeTypeVars" + /-- Returns the index of the value in the binding for the given variable of the scope to use. -/ private def resultIndex (metadata : Metadata) : Option Nat := match metadata[MetadataAttr.scopeName]? with @@ -510,8 +526,12 @@ generate types. inductive PreType where /-- A dialect defined type. -/ | ident (ann : SourceRange) (name : QualifiedIdent) (args : Array PreType) - /-- A bound type variable at the given deBruijn index in the context. -/ + /-- A bound type variable at the given deBruijn index in the context. + Used for type alias parameters -/ | bvar (ann : SourceRange) (index : Nat) + /-- A polymorphic type variable (universally quantified). + Used for polymorphic function type parameters -/ +| tvar (ann : SourceRange) (name : String) /-- A reference to a global variable along with any arguments to ensure it is well-typed. -/ | fvar (ann : SourceRange) (fvar : FreeVarIndex) (args : Array PreType) /-- A function type. -/ @@ -526,6 +546,7 @@ namespace PreType def ann : PreType → SourceRange | .ident ann _ _ => ann | .bvar ann _ => ann +| .tvar ann _ => ann | .fvar ann _ _ => ann | .arrow ann _ _ => ann | .funMacro ann _ _ => ann @@ -533,6 +554,7 @@ def ann : PreType → SourceRange def ofType : TypeExprF SourceRange → PreType | .ident loc name args => .ident loc name (args.map fun a => .ofType a) | .bvar loc idx => .bvar loc idx +| .tvar loc name => .tvar loc name | .fvar loc idx args => .fvar loc idx (args.map fun a => .ofType a) | .arrow loc a r => .arrow loc (.ofType a) (.ofType r) termination_by tp => tp @@ -696,6 +718,18 @@ def argScopeDatatypeLevel (argDecls : ArgDecls) (level : Fin argDecls.size) : Op else panic! s!"scopeDatatype name index {nameIdx} out of bounds ({level.val})" +/-- Returns the typeArgs level if @[scopeTypeVars] is present. + This is used for polymorphic function declarations where type parameters like `` + should be added as .tvar bindings when parsing parameter types and return type. -/ +def argScopeTypeVarsLevel (argDecls : ArgDecls) (level : Fin argDecls.size) : Option (Fin level.val) := + match argDecls[level].metadata.scopeTypeVarsIndex with + | none => none + | some typeArgsIdx => + if h : typeArgsIdx < level.val then + some ⟨level.val - (typeArgsIdx + 1), by omega⟩ + else + panic! s!"scopeTypeVars typeArgs index {typeArgsIdx} out of bounds ({level.val})" + end ArgDecls /-- @@ -752,10 +786,10 @@ structure ConstructorInfo where /-- Build a TypeExpr reference to the datatype with type parameters, using -`.fvar` for the datatype's GlobalContext index. +`.fvar` for the datatype's GlobalContext index and `.tvar` for type parameters. -/ def mkDatatypeTypeRef (ann : SourceRange) (datatypeIndex : FreeVarIndex) (typeParams : Array String) : TypeExpr := - let typeArgs := typeParams.mapIdx fun i _ => TypeExprF.bvar ann i + let typeArgs := typeParams.map fun name => TypeExprF.tvar ann name TypeExprF.fvar ann datatypeIndex typeArgs /-- diff --git a/Strata/DDM/BuiltinDialects/StrataDDL.lean b/Strata/DDM/BuiltinDialects/StrataDDL.lean index 417a84af..8c0a7437 100644 --- a/Strata/DDM/BuiltinDialects/StrataDDL.lean +++ b/Strata/DDM/BuiltinDialects/StrataDDL.lean @@ -171,6 +171,8 @@ def StrataDDL : Dialect := BuiltinM.create! "StrataDDL" #[initDialect] do used for recursive datatype definitions where the datatype name must be visible when parsing constructor field types (e.g., `tail: List` in `Cons(head: int, tail: List)`) -/ declareMetadata { name := "scopeDatatype", args := #[.mk "name" .ident, .mk "typeParams" .ident] } + -- Metadata for bringing type variable names into scope as .tvar bindings. + declareMetadata { name := "scopeTypeVars", args := #[.mk "typeArgs" .ident] } end Strata end diff --git a/Strata/DDM/Elab/Core.lean b/Strata/DDM/Elab/Core.lean index e67e56b7..2091862b 100644 --- a/Strata/DDM/Elab/Core.lean +++ b/Strata/DDM/Elab/Core.lean @@ -54,6 +54,7 @@ partial def expandMacros (m : DialectMap) (f : PreType) (args : Nat → Option A | .arrow loc a b => .arrow loc <$> expandMacros m a args <*> expandMacros m b args | .fvar loc i a => .fvar loc i <$> a.mapM fun e => expandMacros m e args | .bvar loc idx => pure (.bvar loc idx) + | .tvar loc name => pure (.tvar loc name) | .funMacro loc i r => do let r ← expandMacros m r args match args i with @@ -77,7 +78,7 @@ the head is in a normal form. -/ partial def hnf (tctx : TypingContext) (e : TypeExpr) : TypeExpr := match e with - | .arrow .. | .ident .. => e + | .arrow .. | .ident .. | .tvar .. => e | .fvar _ idx args => let gctx := tctx.globalContext match gctx.kindOf! idx with @@ -94,6 +95,7 @@ partial def hnf (tctx : TypingContext) (e : TypeExpr) : TypeExpr := assert! d.isGround hnf (tctx.drop (idx + 1)) d | .type _ _ none => e + | .tvar _ _ => e | _ => panic! "Expected a type" /-- @@ -176,6 +178,9 @@ def resolveTypeBinding (tctx : TypingContext) (loc : SourceRange) (name : String if let .type loc [] _ := k then let info : TypeInfo := { inputCtx := tctx, loc := loc, typeExpr := .bvar loc idx, isInferred := false } return .node (.ofTypeInfo info) #[] + else if let .tvar loc tvarName := k then + let info : TypeInfo := { inputCtx := tctx, loc := loc, typeExpr := .tvar loc tvarName, isInferred := false } + return .node (.ofTypeInfo info) #[] else logErrorMF loc mf!"Expected a type instead of {k}" return default @@ -326,7 +331,7 @@ N.B. This expects that macros have already been expanded in e. -/ partial def headExpandTypeAlias (gctx : GlobalContext) (e : TypeExpr) : TypeExpr := match e with - | .arrow .. | .ident .. | .bvar .. => e + | .arrow .. | .ident .. | .bvar .. | .tvar .. => e | .fvar _ idx args => match gctx.kindOf! idx with | .expr _ => panic! "Type free variable bound to expression." @@ -347,6 +352,10 @@ partial def checkExpressionType (tctx : TypingContext) (itype rtype : TypeExpr) let itype := tctx.hnf itype let rtype := tctx.hnf rtype match itype, rtype with + -- tvar matches anything (dialect responsible for typechecking) + | .tvar _ n1, .tvar _ n2 => return n1 == n2 + | .tvar _ _, _ => return true + | _, .tvar _ _ => return true | .ident _ iq ia, .ident _ rq ra => if p : iq = rq ∧ ia.size = ra.size then do for i in Fin.range ia.size do @@ -468,9 +477,12 @@ partial def unifyTypes if !(← checkExpressionType tctx inferredType info.typeExpr) then logErrorMF exprLoc mf!"Expression has type {withBindings tctx.bindings (mformat inferredType)} when {withBindings tctx.bindings (mformat info.typeExpr)} expected." pure args + | .tvar _ _ => + -- tvar nodes are passed through without attempting unification + pure args | .arrow _ ea er => match inferredType with - | .ident .. | .bvar .. | .fvar .. => + | .ident .. | .bvar .. | .fvar .. | .tvar .. => logErrorMF exprLoc mf!"Expected {expectedType} when {inferredType} found" pure args | .arrow _ ia ir => @@ -752,6 +764,12 @@ def translateBindingKind (tree : Tree) : ElabM BindingKind := do else logErrorMF nameLoc mf!"Unexpected arguments to type variable {name}." return default + else if let .tvar loc tvarName := k then + if tpArgs.isEmpty then + return .expr (.tvar loc tvarName) + else + logErrorMF nameLoc mf!"Unexpected arguments to type variable {name}." + return default else logErrorMF nameLoc mf!"Expected a type instead of {k}" return default @@ -795,12 +813,15 @@ def evalBindingSpec let (bindings, success) ← runChecked <| elabArgIndex initSize args b.argsIndex fun argLoc b => match b.kind with | .expr tp => - pure (b.ident, tp) + pure (some (b.ident, tp)) + | .tvar .. => + pure none | .type .. | .cat _ => do logError argLoc "Expecting expressions in variable binding" - pure default + pure none if !success then return default + let bindings := bindings.filterMap id let typeTree := args[b.typeIndex.toLevel] let kind ← match typeTree.info with @@ -827,6 +848,8 @@ def evalBindingSpec match b.kind with | .type _ [] _ => pure () + | .tvar _ _ => + pure () | .type .. | .expr _ | .cat _ => do logError argLoc s!"{b.ident} must be have type Type instead of {repr b.kind}." return b.ident @@ -955,6 +978,33 @@ def unwrapTree (tree : Tree) (unwrap : Bool) : Arg := | .ofBytesInfo info => .bytes info.loc info.val | _ => tree.arg -- Fallback for non-unwrappable types +/-- +Extract type variable names from a TypeArgs tree (or Option TypeArgs). +TypeArgs is an operation `type_args` with one child that is `CommaSepBy Ident`. +Option TypeArgs wraps this in an OptionInfo node. +-/ +def extractTypeVarNames (tree : Tree) : Array String := + let innerTree := + match tree.info with + | .ofOptionInfo _ => + match tree.children[0]? with + | none => none + | some t => some t + | _ => some tree + match innerTree with + | none => #[] + | some typeArgsTree => + -- typeArgsTree should be the type_args operation + -- Its first child should be CommaSepBy Ident + match typeArgsTree.children[0]? with + | none => #[] + | some commaSepTree => + -- commaSepTree has CommaSepInfo with children that are Ident trees + commaSepTree.children.filterMap fun identTree => + match identTree.info with + | .ofIdentInfo info => some info.val + | _ => none + mutual partial def elabOperation (tctx : TypingContext) (stx : Syntax) : ElabM Tree := do @@ -1017,21 +1067,43 @@ partial def runSyntaxElaborator | .ofIdentInfo info => info.val | _ => panic! "Expected identifier for datatype name" let baseCtx := typeParamsT.resultContext + -- Extract type parameter names from the bindings + let typeParamNames := baseCtx.bindings.toArray.filterMap fun b => + match b.kind with + | .type _ [] _ => some b.ident + | _ => none -- Add the datatype name to the GlobalContext as a type let gctx := baseCtx.globalContext let gctx := if datatypeName ∈ gctx then gctx - else gctx.push datatypeName (GlobalKind.type [] none) - -- Create a new typing context with the updated GlobalContext - pure (baseCtx.withGlobalContext gctx) + else gctx.push datatypeName (GlobalKind.type typeParamNames.toList none) + -- Add .tvar bindings for type parameters + let loc := typeParamsT.info.loc + let tctx := typeParamNames.foldl (init := baseCtx.withGlobalContext gctx) fun ctx name => + ctx.push { ident := name, kind := .tvar loc name } + pure tctx | _, _ => continue | none => - match ae.contextLevel with - | some idx => - match trees[idx] with - | some t => pure t.resultContext + -- Check for typeVarsScope (polymorphic function type parameters) + match ae.typeVarsScope with + | some typeArgsLevel => + match trees[typeArgsLevel] with + | some typeArgsTree => + -- Extract identifiers from TypeArgs and add as .tvar bindings + let baseCtx := typeArgsTree.resultContext + let typeVarNames := extractTypeVarNames typeArgsTree + let tctx := typeVarNames.foldl (init := baseCtx) fun ctx name => + let loc := typeArgsTree.info.loc + ctx.push { ident := name, kind := .tvar loc name } + pure tctx | none => continue - | none => pure tctx0 + | none => + match ae.contextLevel with + | some idx => + match trees[idx] with + | some t => pure t.resultContext + | none => continue + | none => pure tctx0 let astx := args[ae.syntaxLevel] let expectedKind := argDecls[argLevel].kind match expectedKind with @@ -1209,6 +1281,9 @@ partial def elabExpr (tctx : TypingContext) (stx : Syntax) : ElabM Tree := do | .type _ _params _ => logErrorMF loc mf!"{name} is a type when an expression is required." return default + | .tvar _ _ => + logErrorMF loc mf!"{name} is a type variable when an expression is required." + return default | .cat c => logErrorMF loc mf!"{name} has category {c} when an expression is required." return default diff --git a/Strata/DDM/Elab/DialectM.lean b/Strata/DDM/Elab/DialectM.lean index a10d4d11..bd572b3a 100644 --- a/Strata/DDM/Elab/DialectM.lean +++ b/Strata/DDM/Elab/DialectM.lean @@ -29,6 +29,7 @@ private def foldBoundTypeVars {α} (tp : PreType) (init : α) (f : α → Nat | .ident _ _ a => a.attach.foldl (init := init) fun r ⟨e, _⟩ => e.foldBoundTypeVars r f | .fvar _ _ a => a.attach.foldl (init := init) fun r ⟨e, _⟩ => e.foldBoundTypeVars r f | .bvar _ i => f init i + | .tvar _ _ => init | .arrow _ a r => r.foldBoundTypeVars (a.foldBoundTypeVars init f) f | .funMacro _ _ r => r.foldBoundTypeVars init f diff --git a/Strata/DDM/Elab/SyntaxElab.lean b/Strata/DDM/Elab/SyntaxElab.lean index 0f73a6cf..8d09f916 100644 --- a/Strata/DDM/Elab/SyntaxElab.lean +++ b/Strata/DDM/Elab/SyntaxElab.lean @@ -27,6 +27,9 @@ structure ArgElaborator where -- Datatype scope: (nameLevel, typeParamsLevel) for recursive datatype definitions -- When set, the datatype name is added to the typing context as a type datatypeScope : Option (Fin argLevel × Fin argLevel) := .none + -- Type vars scope: level of TypeArgs argument for polymorphic function declarations + -- When set, identifiers from TypeArgs are added as .tvar bindings + typeVarsScope : Option (Fin argLevel) := .none -- Whether to unwrap this argument unwrap : Bool := false deriving Inhabited, Repr @@ -65,6 +68,7 @@ private def push (as : ArgElaborators) argLevel := argLevel.val contextLevel := argDecls.argScopeLevel argLevel datatypeScope := argDecls.argScopeDatatypeLevel argLevel + typeVarsScope := argDecls.argScopeTypeVarsLevel argLevel } have scp : sc < sc + 1 := by grind { as with argElaborators := as.argElaborators.push ⟨newElab, scp⟩ } @@ -81,6 +85,7 @@ private def pushWithUnwrap argLevel := argLevel.val contextLevel := argDecls.argScopeLevel argLevel datatypeScope := argDecls.argScopeDatatypeLevel argLevel + typeVarsScope := argDecls.argScopeTypeVarsLevel argLevel unwrap := unwrap } have scp : sc < sc + 1 := by grind diff --git a/Strata/DDM/Elab/Tree.lean b/Strata/DDM/Elab/Tree.lean index be2a17e8..12f4782a 100644 --- a/Strata/DDM/Elab/Tree.lean +++ b/Strata/DDM/Elab/Tree.lean @@ -32,6 +32,11 @@ over the parameters. Variable belongs to the particular category below. -/ | cat (k : SyntaxCat) +/-- +Variable is a polymorphic type variable (for function type parameters). +These are passed through to the dialect's typechecker for inference. +-/ +| tvar (ann : SourceRange) (name : String) deriving Inhabited, Repr namespace BindingKind @@ -48,6 +53,7 @@ def ofCat (c : SyntaxCat) : BindingKind := def categoryOf : BindingKind → SyntaxCat | .expr tp => .atom tp.ann q`Init.Expr | .type loc _ _ => .atom loc q`Init.Type +| .tvar loc _ => .atom loc q`Init.Type | .cat c => c instance : ToStrataFormat BindingKind where @@ -55,6 +61,7 @@ instance : ToStrataFormat BindingKind where match bk with | .expr tp => mformat tp | .type _ params _ => mformat (params.foldr (init := f!"Type") (fun a f => f!"({a} : Type) -> {f}")) + | .tvar _ name => mformat f!"tvar({name})" | .cat c => mformat c end BindingKind diff --git a/Strata/DDM/Format.lean b/Strata/DDM/Format.lean index 03b761c7..47012bca 100644 --- a/Strata/DDM/Format.lean +++ b/Strata/DDM/Format.lean @@ -275,6 +275,7 @@ private protected def mformat : TypeExprF α → StrataFormat | .ident _ tp a => a.attach.foldl (init := mformat tp) fun m ⟨e, _⟩ => mf!"{m} {e.mformat.ensurePrec (appPrec + 1)}".setPrec appPrec | .bvar _ idx => .bvar idx +| .tvar _ name => mf!"tvar!{name}" | .fvar _ idx a => a.attach.foldl (init := .fvar idx) fun m ⟨e, _⟩ => mf!"{m} {e.mformat.ensurePrec (appPrec + 1)}".setPrec appPrec | .arrow _ a r => mf!"{a.mformat.ensurePrec (arrowPrec+1)} -> {r.mformat.ensurePrec arrowPrec}" @@ -289,6 +290,7 @@ namespace PreType private protected def mformat : PreType → StrataFormat | .ident _ tp a => a.attach.foldl (init := mformat tp) (fun m ⟨e, _⟩ => mf!"{m} {e.mformat}") | .bvar _ idx => .bvar idx +| .tvar _ name => mf!"tvar!{name}" | .fvar _ idx a => a.attach.foldl (init := .fvar idx) (fun m ⟨e, _⟩ => mf!"{m} {e.mformat}") | .arrow _ a r => mf!"{a.mformat} -> {r.mformat}" | .funMacro _ idx r => mf!"fnOf({StrataFormat.bvar idx}, {r.mformat})" diff --git a/Strata/DDM/Integration/Lean/ToExpr.lean b/Strata/DDM/Integration/Lean/ToExpr.lean index 02d74f65..981c15e9 100644 --- a/Strata/DDM/Integration/Lean/ToExpr.lean +++ b/Strata/DDM/Integration/Lean/ToExpr.lean @@ -131,6 +131,8 @@ private protected def toExpr {α} [ToExpr α] : TypeExprF α → Lean.Expr astAnnExpr! ident ann (toExpr nm) ae | .bvar ann idx => astAnnExpr! bvar ann (toExpr idx) +| .tvar ann name => + astAnnExpr! tvar ann (toExpr name) | .fvar ann idx a => let ae := arrayToExpr levelZero (TypeExprF.typeExpr (toTypeExpr α)) (a.map (·.toExpr)) astAnnExpr! fvar ann (toExpr idx) ae @@ -223,6 +225,7 @@ private protected def toExpr : PreType → Lean.Expr let args := arrayToExpr .zero PreType.typeExpr (a.map (·.toExpr)) astExpr! ident (toExpr loc) (toExpr nm) args | .bvar loc idx => astExpr! bvar (toExpr loc) (toExpr idx) +| .tvar loc name => astExpr! tvar (toExpr loc) (toExpr name) | .fvar loc idx a => let args := arrayToExpr .zero PreType.typeExpr (a.map (·.toExpr)) astExpr! fvar (toExpr loc) (toExpr idx) args diff --git a/Strata/DDM/Ion.lean b/Strata/DDM/Ion.lean index 160367da..459db1df 100644 --- a/Strata/DDM/Ion.lean +++ b/Strata/DDM/Ion.lean @@ -385,6 +385,9 @@ private protected def toIon {α} [ToIon α] (refs : SymbolIdCache) (tpe : TypeEx -- A bound type variable with the given index. | .bvar ann vidx => return Ion.sexp #[ionSymbol! "bvar", ← toIon ann, .int vidx] + -- A polymorphic type variable with the given name. + | .tvar ann name => + return Ion.sexp #[ionSymbol! "tvar", ← toIon ann, .string name] | .fvar ann idx a => do let s : Array (Ion SymbolId) := #[ionSymbol! "fvar", ← toIon ann, .int idx] let s ← a.attach.mapM_off (init := s) fun ⟨e, _⟩ => @@ -416,6 +419,11 @@ private protected def fromIon {α} [FromIon α] (v : Ion SymbolId) : FromIonM (T return .bvar (← FromIon.fromIon args[1]) (← .asNat "Type expression bvar" args[2]) + | "tvar" => + let ⟨p⟩ ← .checkArgCount "Type expression tvar" args 3 + return .tvar + (← FromIon.fromIon args[1]) + (← .asString "Type expression tvar name" args[2]) | "fvar" => let ⟨p⟩ ← .checkArgMin "Type expression free variable" args 3 let ann ← FromIon.fromIon args[1] @@ -931,6 +939,9 @@ private protected def toIon (refs : SymbolIdCache) (tpe : PreType) : InternM (Io -- A bound type variable with the given index. | .bvar loc vidx => return Ion.sexp #[ionSymbol! "bvar", ← toIon loc, .int vidx] + -- A polymorphic type variable with the given name. + | .tvar loc name => + return Ion.sexp #[ionSymbol! "tvar", ← toIon loc, .string name] | .fvar loc idx a => do let s : Array (Ion SymbolId) := #[ionSymbol! "fvar", ← toIon loc, .int idx] let s ← a.attach.mapM_off (init := s) fun ⟨e, _⟩ => e.toIon refs @@ -958,6 +969,11 @@ private protected def fromIon (v : Ion SymbolId) : FromIonM PreType := do return PreType.bvar (← fromIon args[1]) (← .asNat "TypeExpr bvar" args[2]) + | "tvar" => + let ⟨p⟩ ← .checkArgCount "PreType tvar" args 3 + return PreType.tvar + (← fromIon args[1]) + (← .asString "PreType tvar name" args[2]) | "fvar" => let ⟨p⟩ ← .checkArgMin "fvar" args 3 let ann ← fromIon args[1] diff --git a/Strata/Languages/Boogie/DDMTransform/Parse.lean b/Strata/Languages/Boogie/DDMTransform/Parse.lean index d4e8494e..55525b3c 100644 --- a/Strata/Languages/Boogie/DDMTransform/Parse.lean +++ b/Strata/Languages/Boogie/DDMTransform/Parse.lean @@ -265,8 +265,8 @@ op command_constdecl (name : Ident, @[declareFn(name, b, r)] op command_fndecl (name : Ident, typeArgs : Option TypeArgs, - @[scope(typeArgs)] b : Bindings, - @[scope (typeArgs)] r : Type) : Command => + @[scopeTypeVars(typeArgs)] b : Bindings, + @[scopeTypeVars(typeArgs)] r : Type) : Command => "function " name typeArgs b ":" r ";\n"; category Inline; @@ -275,8 +275,8 @@ op inline () : Inline => "inline"; @[declareFn(name, b, r)] op command_fndef (name : Ident, typeArgs : Option TypeArgs, - @[scope (typeArgs)] b : Bindings, - @[scope (typeArgs)] r : Type, + @[scopeTypeVars(typeArgs)] b : Bindings, + @[scopeTypeVars(typeArgs)] r : Type, @[scope(b)] c : r, // Prefer adding the inline attribute here so // that the order of the arguments in the fndecl and fndef diff --git a/Strata/Languages/Boogie/DDMTransform/Translate.lean b/Strata/Languages/Boogie/DDMTransform/Translate.lean index 0c563dbb..174ccd7f 100644 --- a/Strata/Languages/Boogie/DDMTransform/Translate.lean +++ b/Strata/Languages/Boogie/DDMTransform/Translate.lean @@ -272,7 +272,12 @@ partial def translateLMonoTy (bindings : TransBindings) (arg : Arg) : assert! i < bindings.boundTypeVars.size let var := bindings.boundTypeVars[bindings.boundTypeVars.size - (i+1)]! return (.ftvar var) - | _ => TransM.error s!"translateLMonoTy not yet implemented {repr tp}" + | .tvar _ name => + return (.ftvar name) + | .arrow _ argTp resTp => + let argTy ← translateLMonoTy bindings (.type argTp) + let resTy ← translateLMonoTy bindings (.type resTp) + return (.arrow argTy resTy) partial def translateLMonoTys (bindings : TransBindings) (args : Array Arg) : TransM (Array LMonoTy) := diff --git a/Strata/Languages/C_Simp/DDMTransform/Translate.lean b/Strata/Languages/C_Simp/DDMTransform/Translate.lean index b7df8b3b..5e8db806 100644 --- a/Strata/Languages/C_Simp/DDMTransform/Translate.lean +++ b/Strata/Languages/C_Simp/DDMTransform/Translate.lean @@ -151,6 +151,8 @@ partial def translateLMonoTy (bindings : TransBindings) (arg : Arg) : assert! i < bindings.boundTypeVars.size let var := bindings.boundTypeVars[bindings.boundTypeVars.size - (i+1)]! return (.ftvar var) + | .tvar _ name => + return (.ftvar name) | _ => TransM.error s!"translateLMonoTy not yet implemented {repr tp}" partial def translateLMonoTys (bindings : TransBindings) (args : Array Arg) : diff --git a/Strata/Languages/Python/BoogiePrelude.lean b/Strata/Languages/Python/BoogiePrelude.lean index 7eb7cd3d..abc2ed51 100644 --- a/Strata/Languages/Python/BoogiePrelude.lean +++ b/Strata/Languages/Python/BoogiePrelude.lean @@ -46,38 +46,13 @@ datatype Error () { // ///////////////////////////////////////////////////////////////////////////////////// // Regular Expressions -type Except (err : Type, ok : Type); - -// FIXME: -// Once DDM support polymorphic functions (and not just type declarations), -// we will be able to define the following generic functions and axioms. For now, -// we manually define appropriate instantiations. -// Also: when ADT support is lifted up to Boogie, all these -// constructors, testers, destructors, and axioms will be auto-generated. -// How will the DDM keep track of them? - -// // Constructors -// function Except_mkOK(err : Type, ok : Type, val : ok) : Except err ok; -// function Except_mkErr(err : Type, ok : Type, val : err) : Except err ok; -// // Testers -// function Except_isOK(err : Type, ok : Type, x : Except err ok) : bool; -// function Except_isErr(err : Type, ok : Type, x : Except err ok) : bool; -// // Destructors -// function Except_getOK(err : Type, ok : Type, x : Except err ok) : ok; -// function Except_getErr(err : Type, ok : Type, x : Except err ok) : err; -// // Axioms -// // Testers of Constructors -// axiom [Except_isOK_mkOK]: (forall x : ok :: Except_isOK(Except_mkOK x)); -// axiom [Except_isErr_mkErr]: (forall x : err :: Except_isErr(Except_mkErr x)); -// // Destructors of Constructors -// axiom [Except_getOK_mkOK]: (forall x : ok :: Except_getOK(Except_mkOK x) == x); -// axiom [Except_getErr_mkErr]: (forall x : err :: Except_isErr(Except_mkErr x)); - -datatype ExceptErrorRegex () { - ExceptErrorRegex_mkOK(ExceptErrorRegex_getOK: regex), - ExceptErrorRegex_mkErr(ExceptErrorRegex_getErr: Error) +datatype Except (err : Type, ok : Type) { + Except_mkOK(Except_getOK: ok), + Except_mkErr(Except_getErr: err) }; +type ExceptErrorRegex := Except Error regex; + // NOTE: `re.match` returns a `Re.Match` object, but for now, we are interested // only in match/nomatch, which is why we return `bool` here. function PyReMatchRegex(pattern : regex, str : string, flags : int) : bool; diff --git a/StrataTest/Languages/B3/DDMFormatTests.lean b/StrataTest/Languages/B3/DDMFormatTests.lean index e28b3942..be25b503 100644 --- a/StrataTest/Languages/B3/DDMFormatTests.lean +++ b/StrataTest/Languages/B3/DDMFormatTests.lean @@ -119,6 +119,7 @@ mutual partial def typeExprFUnitToSourceRange : TypeExprF Unit → TypeExprF SourceRange | .ident () tp a => .ident default tp (a.map typeExprFUnitToSourceRange) | .bvar () idx => .bvar default idx + | .tvar () name => .tvar default name | .fvar () idx a => .fvar default idx (a.map typeExprFUnitToSourceRange) | .arrow () a r => .arrow default (typeExprFUnitToSourceRange a) (typeExprFUnitToSourceRange r) diff --git a/StrataTest/Languages/Boogie/PolymorphicDatatypeTest.lean b/StrataTest/Languages/Boogie/PolymorphicDatatypeTest.lean new file mode 100644 index 00000000..6c2651f7 --- /dev/null +++ b/StrataTest/Languages/Boogie/PolymorphicDatatypeTest.lean @@ -0,0 +1,319 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +import Strata.Languages.Boogie.Verifier + +/-! +# Polymorphic Datatype Integration Tests + +Tests polymorphic datatype declarations in Boogie syntax, including function +generation (constructor, accessor, etc) and SMT verification for concrete +instantiations. +-/ + +namespace Strata.PolymorphicDatatypeTest + +--------------------------------------------------------------------- +-- Test 1: Option Datatype Declaration +--------------------------------------------------------------------- + +def optionDeclPgm : Program := +#strata +program Boogie; + +datatype Option (a : Type) { None(), Some(value: a) }; + +#end + +/-- info: ok: type: +Option +Type Arguments: +[a] +Constructors: +[Name: None Args: [] Tester: Option..isNone , Name: Some Args: [(value, a)] Tester: Option..isSome ]-/ +#guard_msgs in +#eval Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram optionDeclPgm)).fst + +--------------------------------------------------------------------- +-- Test 2: Option Used with Concrete Type (int) +--------------------------------------------------------------------- + +def optionIntPgm : Program := +#strata +program Boogie; + +datatype Option (a : Type) { None(), Some(value: a) }; + +procedure TestOptionInt() returns () +spec { + ensures true; +} +{ + var x : Option int; + var y : Option int; + var v : int; + + x := None(); + y := Some(42); + v := value(y); + assert [valIs42]: v == 42; +}; +#end + +/-- info: ok: type: +Option +Type Arguments: +[a] +Constructors: +[Name: None Args: [] Tester: Option..isNone , Name: Some Args: [(value, a)] Tester: Option..isSome ] + +(procedure TestOptionInt : () → ()) +modifies: [] +preconditions: +postconditions: (TestOptionInt_ensures_0, #true) +body: init (x : (Option int)) := (init_x_0 : (Option int)) +init (y : (Option int)) := (init_y_1 : (Option int)) +init (v : int) := (init_v_2 : int) +x := (~None : (Option int)) +y := ((~Some : (arrow int (Option int))) #42) +v := ((~value : (arrow (Option int) int)) (y : (Option int))) +assert [valIs42] ((v : int) == #42)-/ +#guard_msgs in +#eval Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram optionIntPgm)).fst + +--------------------------------------------------------------------- +-- Test 3: List Used with Concrete Type (int) +--------------------------------------------------------------------- + +def listIntPgm : Program := +#strata +program Boogie; + +datatype List (a : Type) { Nil(), Cons(head: a, tail: List a) }; + +procedure TestListInt() returns () +spec { + ensures true; +} +{ + var xs : List int; + var h : int; + + xs := Cons(1, Cons(2, Nil())); + h := head(xs); + assert [headIs1]: h == 1; +}; +#end + +/-- info: ok: type: +List +Type Arguments: +[a] +Constructors: +[Name: Nil Args: [] Tester: List..isNil , Name: Cons Args: [(head, a), (tail, (List a))] Tester: List..isCons ] + +(procedure TestListInt : () → ()) +modifies: [] +preconditions: +postconditions: (TestListInt_ensures_0, #true) +body: init (xs : (List int)) := (init_xs_0 : (List int)) +init (h : int) := (init_h_1 : int) +xs := (((~Cons : (arrow int (arrow (List int) (List int)))) #1) (((~Cons : (arrow int (arrow (List int) (List int)))) #2) (~Nil : (List int)))) +h := ((~head : (arrow (List int) int)) (xs : (List int))) +assert [headIs1] ((h : int) == #1)-/ +#guard_msgs in +#eval Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram listIntPgm)).fst + +--------------------------------------------------------------------- +-- Test 4: Type with Multiple Parameters (Either) +--------------------------------------------------------------------- + +def eitherUsePgm : Program := +#strata +program Boogie; + +datatype Either (a : Type, b : Type) { Left(l: a), Right(r: b) }; + +procedure TestEither() returns () +spec { + ensures true; +} +{ + var x : Either int bool; + var y : Either int bool; + + x := Left(42); + y := Right(true); + + assert [xIsLeft]: Either..isLeft(x); + assert [yIsRight]: Either..isRight(y); + assert [lValue]: l(x) == 42; +}; +#end + +/-- info: ok: type: +Either +Type Arguments: +[a, b] +Constructors: +[Name: Left Args: [(l, a)] Tester: Either..isLeft , Name: Right Args: [(r, b)] Tester: Either..isRight ] + +(procedure TestEither : () → ()) +modifies: [] +preconditions: +postconditions: (TestEither_ensures_0, #true) +body: init (x : (Either int bool)) := (init_x_0 : (Either int bool)) +init (y : (Either int bool)) := (init_y_1 : (Either int bool)) +x := ((~Left : (arrow int (Either int bool))) #42) +y := ((~Right : (arrow bool (Either int bool))) #true) +assert [xIsLeft] ((~Either..isLeft : (arrow (Either int bool) bool)) (x : (Either int bool))) +assert [yIsRight] ((~Either..isRight : (arrow (Either int bool) bool)) (y : (Either int bool))) +assert [lValue] (((~l : (arrow (Either int bool) int)) (x : (Either int bool))) == #42)-/ +#guard_msgs in +#eval Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram eitherUsePgm)).fst + +--------------------------------------------------------------------- +-- Test 9: Nested Polymorphic Types (Option of List) +--------------------------------------------------------------------- + +def nestedPolyPgm : Program := +#strata +program Boogie; + +datatype Option (a : Type) { None(), Some(value: a) }; +datatype List (a : Type) { Nil(), Cons(head: a, tail: List a) }; + +procedure TestNestedPoly() returns () +spec { + ensures true; +} +{ + var x : Option (List int); + + x := Some(Cons(1, Nil())); + assert [isSome]: Option..isSome(x); +}; +#end + +/-- info: ok: type: +Option +Type Arguments: +[a] +Constructors: +[Name: None Args: [] Tester: Option..isNone , Name: Some Args: [(value, a)] Tester: Option..isSome ] + +type: +List +Type Arguments: +[a] +Constructors: +[Name: Nil Args: [] Tester: List..isNil , Name: Cons Args: [(head, a), (tail, (List a))] Tester: List..isCons ] + +(procedure TestNestedPoly : () → ()) +modifies: [] +preconditions: +postconditions: (TestNestedPoly_ensures_0, #true) +body: init (x : (Option (List int))) := (init_x_0 : (Option (List int))) +x := ((~Some : (arrow (List int) (Option (List int)))) (((~Cons : (arrow int (arrow (List int) (List int)))) #1) (~Nil : (List int)))) +assert [isSome] ((~Option..isSome : (arrow (Option (List int)) bool)) (x : (Option (List int))))-/ +#guard_msgs in +#eval Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram nestedPolyPgm)).fst + +--------------------------------------------------------------------- +-- Test 6: Polymorphic List Destructor with Havoc (SMT verification) +--------------------------------------------------------------------- + +def polyListHavocPgm : Program := +#strata +program Boogie; + +datatype List (a : Type) { Nil(), Cons(head: a, tail: List a) }; + +procedure TestPolyListHavoc() returns () +spec { + ensures true; +} +{ + var xs : List int; + var h : int; + + xs := Nil(); + havoc xs; + + assume xs == Cons(100, Nil()); + + h := head(xs); + + assert [headIs100]: h == 100; +}; +#end + +/-- info: true -/ +#guard_msgs in +#eval TransM.run Inhabited.default (translateProgram polyListHavocPgm) |>.snd |>.isEmpty + +/-- +info: +Obligation: headIs100 +Property: assert +Result: ✅ pass + +Obligation: TestPolyListHavoc_ensures_0 +Property: assert +Result: ✅ pass +-/ +#guard_msgs in +#eval verify "cvc5" polyListHavocPgm Inhabited.default Options.quiet + +--------------------------------------------------------------------- +-- Test 7: Multiple Instantiations with SMT Verification +--------------------------------------------------------------------- + +/-- Test SMT verification with List int and List bool in same procedure -/ +def multiInstSMTPgm : Program := +#strata +program Boogie; + +datatype List (a : Type) { Nil(), Cons(head: a, tail: List a) }; + +procedure TestMultiInstSMT() returns () +spec { + ensures true; +} +{ + var xs : List int; + var ys : List bool; + + xs := Nil(); + ys := Nil(); + havoc xs; + havoc ys; + + assume List..isCons(xs); + assume List..isCons(ys); + + assert [bothCons]: List..isCons(xs) == List..isCons(ys); +}; +#end + +/-- info: true -/ +#guard_msgs in +#eval TransM.run Inhabited.default (translateProgram multiInstSMTPgm) |>.snd |>.isEmpty + +/-- +info: +Obligation: bothCons +Property: assert +Result: ✅ pass + +Obligation: TestMultiInstSMT_ensures_0 +Property: assert +Result: ✅ pass +-/ +#guard_msgs in +#eval verify "cvc5" multiInstSMTPgm Inhabited.default Options.quiet + +end Strata.PolymorphicDatatypeTest diff --git a/StrataTest/Languages/Boogie/PolymorphicFunctionTest.lean b/StrataTest/Languages/Boogie/PolymorphicFunctionTest.lean new file mode 100644 index 00000000..dcd8836e --- /dev/null +++ b/StrataTest/Languages/Boogie/PolymorphicFunctionTest.lean @@ -0,0 +1,189 @@ +/- + Copyright Strata Contributors + + SPDX-License-Identifier: Apache-2.0 OR MIT +-/ + +import Strata.Languages.Boogie.Verifier + +/-! +# Polymorphic Function Integration Tests + +Tests polymorphic function declarations in Boogie syntax, including parsing, +typechecking, and type inference. +-/ + +namespace Strata.PolymorphicFunctionTest + +--------------------------------------------------------------------- +-- Test 1: Single Type Parameter Function Declaration +--------------------------------------------------------------------- + +def singleTypeParamDeclPgm : Program := +#strata +program Boogie; + +function identity(x : a) : a; + +#end + +/-- +info: ok: func identity : ∀[$__ty0]. ((x : $__ty0)) → $__ty0; +-/ +#guard_msgs in +#eval Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram singleTypeParamDeclPgm)).fst + +--------------------------------------------------------------------- +-- Test 2: Single Type Parameter Function Concrete Instantiation +--------------------------------------------------------------------- + +def singleTypeParamIntPgm : Program := +#strata +program Boogie; + +function identity(x : a) : a; + +procedure TestIdentityInt() returns () +spec { + ensures true; +} +{ + var x : int; + var y : int; + x := 42; + y := identity(x); +}; +#end + +/-- info: ok: func identity : ∀[$__ty0]. ((x : $__ty0)) → $__ty0; +(procedure TestIdentityInt : () → ()) +modifies: [] +preconditions: +postconditions: (TestIdentityInt_ensures_0, #true) +body: init (x : int) := (init_x_0 : int) +init (y : int) := (init_y_1 : int) +x := #42 +y := ((~identity : (arrow int int)) (x : int))-/ +#guard_msgs in +#eval (Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram singleTypeParamIntPgm)).fst) + +--------------------------------------------------------------------- +-- Test 3: Multiple Type Parameter Function Used in Expression +--------------------------------------------------------------------- + +def multiTypeParamUsePgm : Program := +#strata +program Boogie; + +function makePair(x : a, y : b) : Map a b; + +procedure TestMakePair() returns () +spec { + ensures true; +} +{ + var m : Map int bool; + m := makePair(42, true); +}; +#end + +/-- info: ok: func makePair : ∀[$__ty0, $__ty1]. ((x : $__ty0) (y : $__ty1)) → (Map $__ty0 $__ty1); +(procedure TestMakePair : () → ()) +modifies: [] +preconditions: +postconditions: (TestMakePair_ensures_0, #true) +body: init (m : (Map int bool)) := (init_m_0 : (Map int bool)) +m := (((~makePair : (arrow int (arrow bool (Map int bool)))) #42) #true)-/ +#guard_msgs in +#eval (Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram multiTypeParamUsePgm)).fst) + +--------------------------------------------------------------------- +-- Test 4: Polymorphic Function with Arrow Types Used in Expression +--------------------------------------------------------------------- + +def arrowTypeParamUsePgm : Program := +#strata +program Boogie; + +function apply(f : a -> b, x : a) : b; +function intToBool(x : int) : bool; + +procedure TestApply() returns () +spec { + ensures true; +} +{ + var result : bool; + result := apply(intToBool, 42); +}; +#end + +/-- info: ok: func apply : ∀[$__ty0, $__ty1]. ((f : (arrow $__ty0 $__ty1)) (x : $__ty0)) → $__ty1; +func intToBool : ((x : int)) → bool; +(procedure TestApply : () → ()) +modifies: [] +preconditions: +postconditions: (TestApply_ensures_0, #true) +body: init (result : bool) := (init_result_0 : bool) +result := (((~apply : (arrow (arrow int bool) (arrow int bool))) (~intToBool : (arrow int bool))) #42)-/ +#guard_msgs in +#eval (Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram arrowTypeParamUsePgm)).fst) + +--------------------------------------------------------------------- +-- Test 5: Different Instantiations in a Single Term +--------------------------------------------------------------------- + +def differentInstantiationsPgm : Program := +#strata +program Boogie; + +function identity(x : a) : a; +function makePair(x : a, y : b) : Map a b; + +procedure TestDifferentInstantiations() returns () +spec { + ensures true; +} +{ + var m : Map int bool; + m := makePair(identity(42), identity(true)); +}; +#end + +/-- info: ok: func identity : ∀[$__ty0]. ((x : $__ty0)) → $__ty0; +func makePair : ∀[$__ty1, $__ty2]. ((x : $__ty1) (y : $__ty2)) → (Map $__ty1 $__ty2); +(procedure TestDifferentInstantiations : () → ()) +modifies: [] +preconditions: +postconditions: (TestDifferentInstantiations_ensures_0, #true) +body: init (m : (Map int bool)) := (init_m_0 : (Map int bool)) +m := (((~makePair : (arrow int (arrow bool (Map int bool)))) ((~identity : (arrow int int)) #42)) ((~identity : (arrow bool bool)) #true))-/ +#guard_msgs in +#eval (Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram differentInstantiationsPgm)).fst) + +--------------------------------------------------------------------- +-- Test 6: Negative Test - Type Unification Failure (eq with different types) +--------------------------------------------------------------------- + +def eqTypeMismatchPgm : Program := +#strata +program Boogie; + +function eq(x : a, y : a) : bool; + +procedure TestEqTypeMismatch() returns () +spec { + ensures true; +} +{ + var result : bool; + result := eq(42, true); +}; +#end + +/-- info: error: (0, 0) Impossible to unify (arrow int bool) with (arrow bool $__ty6). +First mismatch: int with bool.-/ +#guard_msgs in +#eval (Boogie.typeCheck Options.quiet (TransM.run Inhabited.default (translateProgram eqTypeMismatchPgm)).fst) + +end Strata.PolymorphicFunctionTest diff --git a/docs/verso/DDMDoc.lean b/docs/verso/DDMDoc.lean index 477cf933..494c00b2 100644 --- a/docs/verso/DDMDoc.lean +++ b/docs/verso/DDMDoc.lean @@ -397,6 +397,28 @@ category Statement; op varStatement (dl : DeclList) : Statement => "var " dl ";"; ``` +### Polymorphic Type Variables + +The `@[scopeTypeVars]` annotation allows polymorphic function declarations +where type parameters (like ``) +need to be in scope when parsing parameter types and return types. +For example, function declarations in Strata.Boogie are defined as +the following: + +``` +category TypeArgs; +op type_args (args : CommaSepBy Ident) : TypeArgs => "<" args ">"; + +@[declareFn(name, b, r)] +op command_fndecl (name : Ident, + typeArgs : Option TypeArgs, + @[scopeTypeVars(typeArgs)] b : Bindings, + @[scopeTypeVars(typeArgs)] r : Type) : Command => + "function " name typeArgs b ":" r ";"; +``` + +This allows parsing declarations like `function identity(x: a): a`. + ## The `Init` dialect %%% tag := "init"