diff --git a/primer-service/exe-server/Main.hs b/primer-service/exe-server/Main.hs index 14a251de3..ca52de50c 100644 --- a/primer-service/exe-server/Main.hs +++ b/primer-service/exe-server/Main.hs @@ -376,7 +376,9 @@ instance ConvertLogMessage SomeException LogMsg where instance ConvertLogMessage PrimerErr LogMsg where convert (DatabaseErr e) = LogMsg e convert (UnknownDef e) = LogMsg $ show e + convert (UnknownTypeDef e) = LogMsg $ show e convert (UnexpectedPrimDef e) = LogMsg $ show e + convert (UnexpectedPrimTypeDef e) = LogMsg $ show e convert (AddDefError m n e) = LogMsg $ show (m, n, e) convert (AddTypeDefError tc vcs e) = LogMsg $ show (tc, vcs, e) convert (ActionOptionsNoID e) = LogMsg $ show e diff --git a/primer-service/src/Primer/OpenAPI.hs b/primer-service/src/Primer/OpenAPI.hs index 91db986b5..39f9bc2ff 100644 --- a/primer-service/src/Primer/OpenAPI.hs +++ b/primer-service/src/Primer/OpenAPI.hs @@ -10,6 +10,12 @@ module Primer.OpenAPI ( import Foreword import Data.Aeson ( + FromJSON, + GFromJSON, + GToEncoding, + GToJSON, + ToJSON, + Zero, toJSON, ) import Data.OpenApi ( @@ -27,6 +33,7 @@ import Data.OpenApi.Internal.Schema ( rename, timeSchema, ) +import Data.Text qualified as T import Data.Time ( UTCTime (..), fromGregorian, @@ -42,10 +49,11 @@ import Primer.API ( Module, NewSessionReq, NodeBody, - NodeSelection (..), Prog, - Selection (..), + Selection, Tree, + TypeDef, + ValCon, ) import Primer.API qualified as API import Primer.API.NodeFlavor ( @@ -56,8 +64,8 @@ import Primer.API.NodeFlavor ( ) import Primer.API.RecordPair (RecordPair) import Primer.Action.Available qualified as Available -import Primer.App (NodeType) -import Primer.App.Base (Level) +import Primer.App (DefSelection, NodeSelection, NodeType, TypeDefSelection) +import Primer.App.Base (Level, TypeDefConsFieldSelection (..), TypeDefConsSelection (..), TypeDefNodeSelection) import Primer.Core ( GlobalName, GlobalNameKind (ADefName, ATyCon, AValCon), @@ -65,16 +73,21 @@ import Primer.Core ( LVarName, ModuleName, PrimCon, + TyVarName, ) import Primer.Database ( LastModified, Session, SessionName, ) -import Primer.JSON (CustomJSON, PrimerJSON) +import Primer.JSON (CustomJSON (CustomJSON), PrimerJSON) import Primer.Name (Name) import Servant.API (FromHttpApiData (parseQueryParam), ToHttpApiData (toQueryParam)) +newtype PrimerJSONNamed (s :: Symbol) a = PrimerJSONNamed a +deriving via PrimerJSON a instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (PrimerJSONNamed s a) +deriving via PrimerJSON a instance (Generic a, GToJSON Zero (Rep a), GToEncoding Zero (Rep a)) => ToJSON (PrimerJSONNamed s a) + -- $orphanInstances -- -- We define some OpenApi orphan instances in primer-service, to avoid @@ -89,6 +102,12 @@ instance where declareNamedSchema _ = genericDeclareNamedSchema (fromAesonOptions (aesonOptions @os)) (Proxy @a) +instance + (Typeable a, Generic a, GToSchema (Rep a), KnownSymbol s) => + ToSchema (PrimerJSONNamed s a) + where + declareNamedSchema _ = rename (Just $ T.pack $ symbolVal $ Proxy @s) <$> declareNamedSchema (Proxy @(PrimerJSON a)) + instance ToSchema SessionName deriving via PrimerJSON Session instance ToSchema Session @@ -120,12 +139,12 @@ deriving via Text instance (ToSchema Name) -- at the openapi level, so api consumers do not have to deal with -- three identical types. Note that our openapi interface is a -- simplified view, so this collapse is in the correct spirit. -instance ToSchema (GlobalName 'ADefName) where - declareNamedSchema _ = rename (Just "GlobalName") <$> declareNamedSchema (Proxy @(PrimerJSON (GlobalName 'ADefName))) +deriving via PrimerJSONNamed "GlobalName" (GlobalName 'ADefName) instance ToSchema (GlobalName 'ADefName) deriving via GlobalName 'ADefName instance ToSchema (GlobalName 'ATyCon) deriving via GlobalName 'ADefName instance ToSchema (GlobalName 'AValCon) deriving via Name instance (ToSchema LVarName) +deriving via Name instance (ToSchema TyVarName) deriving via PrimerJSON (RecordPair a b) instance (ToSchema a, ToSchema b) => ToSchema (RecordPair a b) deriving via PrimerJSON Tree instance ToSchema Tree deriving via PrimerJSON API.Name instance ToSchema API.Name @@ -135,6 +154,8 @@ deriving via PrimerJSON NodeFlavorTextBody instance ToSchema NodeFlavorTextBody deriving via PrimerJSON NodeFlavorPrimBody instance ToSchema NodeFlavorPrimBody deriving via PrimerJSON NodeFlavorBoxBody instance ToSchema NodeFlavorBoxBody deriving via PrimerJSON NodeFlavorNoBody instance ToSchema NodeFlavorNoBody +deriving via PrimerJSON TypeDef instance ToSchema TypeDef +deriving via PrimerJSON ValCon instance ToSchema ValCon deriving via PrimerJSON Def instance ToSchema Def deriving via NonEmpty Name instance ToSchema ModuleName deriving via PrimerJSON Module instance ToSchema Module @@ -146,8 +167,13 @@ deriving via PrimerJSON Available.FreeInput instance ToSchema Available.FreeInpu deriving via PrimerJSON Available.Options instance ToSchema Available.Options deriving via PrimerJSON Available.Action instance ToSchema Available.Action deriving via PrimerJSON ApplyActionBody instance ToSchema ApplyActionBody -deriving via PrimerJSON Selection instance ToSchema Selection -deriving via PrimerJSON NodeSelection instance ToSchema NodeSelection +deriving via PrimerJSONNamed "Selection" Selection instance ToSchema Selection +deriving via PrimerJSONNamed "TypeDefSelection" (TypeDefSelection ID) instance ToSchema (TypeDefSelection ID) +deriving via PrimerJSONNamed "TypeDefNodeSelection" (TypeDefNodeSelection ID) instance ToSchema (TypeDefNodeSelection ID) +deriving via PrimerJSONNamed "TypeDefConsSelection" (TypeDefConsSelection ID) instance ToSchema (TypeDefConsSelection ID) +deriving via PrimerJSONNamed "TypeDefConsFieldSelection" (TypeDefConsFieldSelection ID) instance ToSchema (TypeDefConsFieldSelection ID) +deriving via PrimerJSONNamed "DefSelection" (DefSelection ID) instance ToSchema (DefSelection ID) +deriving via PrimerJSONNamed "NodeSelection" (NodeSelection ID) instance ToSchema (NodeSelection ID) deriving via PrimerJSON NodeType instance ToSchema NodeType deriving via PrimerJSON Level instance ToSchema Level deriving via PrimerJSON NewSessionReq instance ToSchema NewSessionReq diff --git a/primer-service/src/Primer/Server.hs b/primer-service/src/Primer/Server.hs index 1ab766ec6..97a751460 100644 --- a/primer-service/src/Primer/Server.hs +++ b/primer-service/src/Primer/Server.hs @@ -405,6 +405,8 @@ serve ss q v port origins logger = do DatabaseErr msg -> err500{errBody = encode msg} UnknownDef d -> err404{errBody = "Unknown definition: " <> encode (globalNamePretty d)} UnexpectedPrimDef d -> err400{errBody = "Unexpected primitive definition: " <> encode (globalNamePretty d)} + UnknownTypeDef d -> err404{errBody = "Unknown type definition: " <> encode (globalNamePretty d)} + UnexpectedPrimTypeDef d -> err400{errBody = "Unexpected primitive type definition: " <> encode (globalNamePretty d)} AddDefError m md pe -> err400{errBody = "Error while adding definition (" <> s <> "): " <> show pe} where s = encode $ case md of diff --git a/primer-service/test/Tests/OpenAPI.hs b/primer-service/test/Tests/OpenAPI.hs index a00773e92..80a639f25 100644 --- a/primer-service/test/Tests/OpenAPI.hs +++ b/primer-service/test/Tests/OpenAPI.hs @@ -30,10 +30,11 @@ import Primer.API ( Module (Module), NewSessionReq (..), NodeBody (BoxBody, NoBody, PrimBody, TextBody), - NodeSelection (..), Prog (Prog), - Selection (..), + Selection, Tree, + TypeDef (..), + ValCon (..), viewTreeExpr, viewTreeType, ) @@ -45,7 +46,17 @@ import Primer.API.NodeFlavor ( ) import Primer.API.RecordPair (RecordPair (RecordPair)) import Primer.Action.Available qualified as Available -import Primer.App (Level, NodeType) +import Primer.App ( + DefSelection (..), + Level, + NodeSelection (..), + NodeType, + Selection' (..), + TypeDefConsFieldSelection (TypeDefConsFieldSelection), + TypeDefConsSelection (..), + TypeDefSelection (..), + ) +import Primer.App.Base (TypeDefNodeSelection (..)) import Primer.Core (GVarName, ID (ID), ModuleName, PrimCon (PrimChar, PrimInt)) import Primer.Database ( LastModified (..), @@ -64,6 +75,7 @@ import Primer.Gen.Core.Raw ( genModuleName, genName, genTyConName, + genTyVarName, genType, genValConName, ) @@ -207,6 +219,18 @@ tasty_NodeFlavorNoBody = testToJSON $ G.enumBounded @_ @NodeFlavorNoBody genDef :: ExprGen Def genDef = Def <$> genGVarName <*> genExprTree <*> G.maybe genTypeTree +genTypeDef :: ExprGen TypeDef +genTypeDef = + TypeDef + <$> genTyConName + <*> G.list (R.linear 0 3) genTyVarName + <*> G.list (R.linear 0 3) genName + <*> G.maybe + ( G.list + (R.linear 0 3) + (ValCon <$> genValConName <*> G.list (R.linear 0 3) genTypeTree) + ) + tasty_Def :: Property tasty_Def = testToJSON $ evalExprGen 0 genDef @@ -215,7 +239,7 @@ genModule = Module <$> genModuleName <*> G.bool - <*> G.list (R.linear 0 3) genTyConName + <*> G.list (R.linear 0 3) genTypeDef <*> G.list (R.linear 0 3) genDef tasty_Module :: Property @@ -224,11 +248,33 @@ tasty_Module = testToJSON $ evalExprGen 0 genModule genNodeType :: ExprGen NodeType genNodeType = G.enumBounded -genNodeSelection :: ExprGen NodeSelection +genNodeSelection :: ExprGen (NodeSelection ID) genNodeSelection = NodeSelection <$> genNodeType <*> genID +genDefSelection :: ExprGen (DefSelection ID) +genDefSelection = DefSelection <$> genGVarName <*> G.maybe genNodeSelection + +genTypeDefSelection :: ExprGen (TypeDefSelection ID) +genTypeDefSelection = + TypeDefSelection + <$> genTyConName + <*> G.maybe + ( G.choice + [ TypeDefParamNodeSelection <$> genTyVarName + , TypeDefConsNodeSelection + <$> ( TypeDefConsSelection + <$> genValConName + <*> G.maybe (TypeDefConsFieldSelection <$> G.integral (R.linear 0 3) <*> genID) + ) + ] + ) + genSelection :: ExprGen Selection -genSelection = Selection <$> genGVarName <*> G.maybe genNodeSelection +genSelection = + G.choice + [ SelectionDef <$> genDefSelection + , SelectionTypeDef <$> genTypeDefSelection + ] genProg :: Gen Prog genProg = evalExprGen 0 $ Prog <$> G.list (R.linear 0 3) genModule <*> G.maybe genSelection <*> G.bool <*> G.bool @@ -307,7 +353,7 @@ instance Arbitrary ApplyActionBody where arbitrary = ApplyActionBody <$> arbitrary <*> arbitrary instance Arbitrary Selection where arbitrary = hedgehog $ evalExprGen 0 genSelection -instance Arbitrary NodeSelection where +instance Arbitrary (NodeSelection ID) where arbitrary = hedgehog $ evalExprGen 0 genNodeSelection instance Arbitrary a => Arbitrary (NonEmpty a) where arbitrary = (:|) <$> arbitrary <*> arbitrary diff --git a/primer-service/test/outputs/OpenAPI/openapi.json b/primer-service/test/outputs/OpenAPI/openapi.json index c4bafb2c5..40f9ec429 100644 --- a/primer-service/test/outputs/OpenAPI/openapi.json +++ b/primer-service/test/outputs/OpenAPI/openapi.json @@ -100,6 +100,20 @@ ], "type": "object" }, + "DefSelection": { + "properties": { + "def": { + "$ref": "#/components/schemas/GlobalName" + }, + "node": { + "$ref": "#/components/schemas/NodeSelection" + } + }, + "required": [ + "def" + ], + "type": "object" + }, "EvalFullResp": { "oneOf": [ { @@ -187,7 +201,11 @@ "MakeTVar", "MakeForall", "RenameForall", - "RenameDef" + "RenameDef", + "RenameType", + "RenameCon", + "RenameTypeParam", + "AddCon" ], "type": "string" }, @@ -224,7 +242,7 @@ }, "types": { "items": { - "$ref": "#/components/schemas/GlobalName" + "$ref": "#/components/schemas/TypeDef" }, "type": "array" } @@ -284,7 +302,8 @@ "RaiseType", "DeleteType", "DuplicateDef", - "DeleteDef" + "DeleteDef", + "AddConField" ], "type": "string" }, @@ -413,7 +432,7 @@ }, "NodeSelection": { "properties": { - "id": { + "meta": { "maximum": 9223372036854775807, "minimum": -9223372036854775808, "type": "integer" @@ -424,7 +443,7 @@ }, "required": [ "nodeType", - "id" + "meta" ], "type": "object" }, @@ -656,18 +675,44 @@ "type": "object" }, "Selection": { - "properties": { - "def": { - "$ref": "#/components/schemas/GlobalName" + "oneOf": [ + { + "properties": { + "contents": { + "$ref": "#/components/schemas/DefSelection" + }, + "tag": { + "enum": [ + "SelectionDef" + ], + "type": "string" + } + }, + "required": [ + "tag", + "contents" + ], + "type": "object" }, - "node": { - "$ref": "#/components/schemas/NodeSelection" + { + "properties": { + "contents": { + "$ref": "#/components/schemas/TypeDefSelection" + }, + "tag": { + "enum": [ + "SelectionTypeDef" + ], + "type": "string" + } + }, + "required": [ + "tag", + "contents" + ], + "type": "object" } - }, - "required": [ - "def" - ], - "type": "object" + ] }, "Session": { "properties": { @@ -716,10 +761,146 @@ ], "type": "object" }, + "TypeDef": { + "properties": { + "constructors": { + "items": { + "$ref": "#/components/schemas/ValCon" + }, + "type": "array" + }, + "name": { + "$ref": "#/components/schemas/GlobalName" + }, + "nameHints": { + "items": { + "type": "string" + }, + "type": "array" + }, + "params": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "name", + "params", + "nameHints" + ], + "type": "object" + }, + "TypeDefConsFieldSelection": { + "properties": { + "index": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "meta": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "index", + "meta" + ], + "type": "object" + }, + "TypeDefConsSelection": { + "properties": { + "con": { + "$ref": "#/components/schemas/GlobalName" + }, + "field": { + "$ref": "#/components/schemas/TypeDefConsFieldSelection" + } + }, + "required": [ + "con" + ], + "type": "object" + }, + "TypeDefNodeSelection": { + "oneOf": [ + { + "properties": { + "contents": { + "type": "string" + }, + "tag": { + "enum": [ + "TypeDefParamNodeSelection" + ], + "type": "string" + } + }, + "required": [ + "tag", + "contents" + ], + "type": "object" + }, + { + "properties": { + "contents": { + "$ref": "#/components/schemas/TypeDefConsSelection" + }, + "tag": { + "enum": [ + "TypeDefConsNodeSelection" + ], + "type": "string" + } + }, + "required": [ + "tag", + "contents" + ], + "type": "object" + } + ] + }, + "TypeDefSelection": { + "properties": { + "def": { + "$ref": "#/components/schemas/GlobalName" + }, + "node": { + "$ref": "#/components/schemas/TypeDefNodeSelection" + } + }, + "required": [ + "def" + ], + "type": "object" + }, "UUID": { "example": "00000000-0000-0000-0000-000000000000", "format": "uuid", "type": "string" + }, + "ValCon": { + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/Tree" + }, + "type": "array" + }, + "name": { + "$ref": "#/components/schemas/GlobalName" + } + }, + "required": [ + "name", + "fields" + ], + "type": "object" } } }, @@ -1078,7 +1259,11 @@ "MakeTVar", "MakeForall", "RenameForall", - "RenameDef" + "RenameDef", + "RenameType", + "RenameCon", + "RenameTypeParam", + "AddCon" ], "type": "string" } diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index 76f968907..481d24b3c 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -37,6 +37,8 @@ module Primer.API ( viewProg, Prog (Prog), Module (Module), + TypeDef (TypeDef), + ValCon (ValCon), Def (Def), getProgram, getProgram', @@ -63,10 +65,7 @@ module Primer.API ( viewTreeType, viewTreeExpr, getApp, - Selection (..), - viewSelection, - NodeSelection (..), - viewNodeSelection, + Selection, undoAvailable, redoAvailable, Name (..), @@ -103,6 +102,7 @@ import Primer.Action (ActionError, ProgAction, toProgActionInput, toProgActionNo import Primer.Action.Available qualified as Available import Primer.App ( App, + DefSelection (..), EditAppM, Editable, EvalFullReq (..), @@ -110,10 +110,14 @@ import Primer.App ( EvalResp (..), Level, MutationRequest, + NodeSelection (..), NodeType (..), ProgError, QueryAppM, Question (GenerateName), + Selection' (..), + TypeDefConsSelection (..), + TypeDefSelection (..), appProg, handleEvalFullRequest, handleEvalRequest, @@ -123,6 +127,7 @@ import Primer.App ( newApp, progAllDefs, progAllTypeDefs, + progAllTypeDefsMeta, progCxt, progImports, progLog, @@ -134,6 +139,7 @@ import Primer.App ( unlog, ) import Primer.App qualified as App +import Primer.App.Base (TypeDefNodeSelection (..)) import Primer.Core ( Bind' (..), CaseBranch' (..), @@ -152,6 +158,7 @@ import Primer.Core ( TyVarName, Type, Type' (..), + TypeMeta, ValConName, getID, unLocalName, @@ -210,10 +217,11 @@ import Primer.Log ( PureLog, runPureLog, ) -import Primer.Module (moduleDefsQualified, moduleName, moduleTypesQualified) +import Primer.Module (moduleDefsQualified, moduleName, moduleTypesQualifiedMeta) import Primer.Name qualified as Name import Primer.Primitives (primDefType) -import Primer.TypeDef (ASTTypeDef (ASTTypeDef), ValCon (ValCon)) +import Primer.TypeDef (ASTTypeDef (..), forgetTypeDefMetadata, typeDefNameHints, typeDefParameters) +import Primer.TypeDef qualified as TypeDef import StmContainers.Map qualified as StmMap -- | The API environment. @@ -255,10 +263,12 @@ runPrimerM = runReaderT . unPrimerM data PrimerErr = DatabaseErr Text | UnknownDef GVarName + | UnknownTypeDef TyConName | UnexpectedPrimDef GVarName + | UnexpectedPrimTypeDef TyConName | AddDefError ModuleName (Maybe Text) ProgError | AddTypeDefError TyConName [ValConName] ProgError - | ActionOptionsNoID (Maybe (NodeType, ID)) + | ActionOptionsNoID Selection | ToProgActionError Available.Action ActionError | ApplyActionError [ProgAction] ProgError | UndoError ProgError @@ -630,7 +640,7 @@ data Prog = Prog data Module = Module { modname :: ModuleName , editable :: Bool - , types :: [TyConName] + , types :: [TypeDef] , -- We don't use Map Name Def as it is rather redundant since each -- Def carries a name field, and it is difficult to enforce that -- "the keys of this object match the name field of the @@ -641,6 +651,25 @@ data Module = Module deriving (ToJSON, FromJSON) via PrimerJSON Module deriving anyclass (NFData) +data TypeDef = TypeDef + { name :: TyConName + , params :: [TyVarName] + , nameHints :: [Name.Name] + , constructors :: Maybe [ValCon] + -- ^ a `Nothing` here indicates a primitive type (whereas `Just []` is `Void`) + } + deriving stock (Generic, Show, Read) + deriving (ToJSON, FromJSON) via PrimerJSON TypeDef + deriving anyclass (NFData) + +data ValCon = ValCon + { name :: ValConName + , fields :: [Tree] + } + deriving stock (Generic, Show, Read) + deriving (ToJSON, FromJSON) via PrimerJSON ValCon + deriving anyclass (NFData) + -- | This type is the api's view of a 'Primer.Core.Def' -- (this is expected to evolve as we flesh out the API) data Def = Def @@ -657,7 +686,7 @@ viewProg :: App.Prog -> Prog viewProg p = Prog { modules = map (viewModule True) (progModules p) <> map (viewModule False) (progImports p) - , selection = viewSelection <$> progSelection p + , selection = getID <<$>> progSelection p , undoAvailable = not $ null $ unlog $ progLog p , redoAvailable = not $ null $ unlog $ redoLog p } @@ -666,7 +695,24 @@ viewProg p = Module { modname = moduleName m , editable = e - , types = fst <$> Map.assocs (moduleTypesQualified m) + , types = + ( \(name, d) -> + TypeDef + { name + , params = fst <$> typeDefParameters d + , nameHints = typeDefNameHints d + , constructors = case d of + TypeDef.TypeDefPrim _ -> Nothing + TypeDef.TypeDefAST t -> + Just $ + astTypeDefConstructors t <&> \(TypeDef.ValCon nameCon argsCon) -> + ValCon + { name = nameCon + , fields = viewTreeType' . over _typeMeta (show . view _id) <$> argsCon + } + } + ) + <$> Map.assocs (moduleTypesQualifiedMeta m) , defs = ( \(name, d) -> Def @@ -681,7 +727,7 @@ viewProg p = flip evalState (0 :: Int) . traverseOf _typeMeta \() -> do n <- get put $ n + 1 - pure $ "primtype_" <> show d' <> "_" <> show n + pure $ "primtype_" <> Name.unName (Core.baseName name) <> "_" <> show n } ) <$> Map.assocs (moduleDefsQualified m) @@ -1026,7 +1072,7 @@ createTypeDef :: createTypeDef = curry3 $ logAPI (noError CreateTypeDef) \(sid, tyconName, valcons) -> - edit sid (App.Edit [App.AddTypeDef tyconName $ ASTTypeDef [] (map (`ValCon` []) valcons) []]) + edit sid (App.Edit [App.AddTypeDef tyconName $ ASTTypeDef [] (map (`TypeDef.ValCon` []) valcons) []]) >>= either (throwM . AddTypeDefError tyconName valcons) (pure . viewProg) availableActions :: @@ -1038,15 +1084,23 @@ availableActions :: availableActions = curry3 $ logAPI (noError AvailableActions) $ \(sid, level, selection) -> do prog <- getProgram sid let allDefs = progAllDefs prog - allTypeDefs = progAllTypeDefs prog - (editable, ASTDef{astDefType = type_, astDefExpr = expr}) <- findASTDef allDefs selection.def - case selection.node of - Nothing -> - pure $ Available.forDef (snd <$> allDefs) level editable selection.def - Just NodeSelection{..} -> do - pure $ case nodeType of - SigNode -> Available.forSig level editable type_ id - BodyNode -> Available.forBody (snd <$> allTypeDefs) level editable expr id + allTypeDefs = progAllTypeDefsMeta prog + case selection of + SelectionDef sel -> do + (editable, ASTDef{astDefType = type_, astDefExpr = expr}) <- findASTDef allDefs sel.def + pure $ case sel.node of + Nothing -> Available.forDef (snd <$> allDefs) level editable sel.def + Just NodeSelection{..} -> case nodeType of + SigNode -> Available.forSig level editable type_ meta + BodyNode -> Available.forBody (forgetTypeDefMetadata . snd <$> allTypeDefs) level editable expr meta + SelectionTypeDef sel -> do + (editable, def) <- findASTTypeDef allTypeDefs sel.def + pure $ case sel.node of + Nothing -> Available.forTypeDef level editable + Just (TypeDefParamNodeSelection _) -> Available.forTypeDefParamNode level editable + Just (TypeDefConsNodeSelection s) -> case s.field of + Nothing -> Available.forTypeDefConsNode level editable + Just field -> Available.forTypeDefConsFieldNode level editable def s.con field.index field.meta actionOptions :: (MonadIO m, MonadThrow m, MonadAPILog l m) => @@ -1060,17 +1114,9 @@ actionOptions = curry4 $ logAPI (noError ActionOptions) $ \(sid, level, selectio let prog = appProg app allDefs = progAllDefs prog allTypeDefs = progAllTypeDefs prog - nodeSel = selection.node <&> \s -> (s.nodeType, s.id) - def' <- snd <$> findASTDef allDefs selection.def - maybe (throwM $ ActionOptionsNoID nodeSel) pure $ - Available.options - (snd <$> allTypeDefs) - (snd <$> allDefs) - (progCxt prog) - level - def' - nodeSel - action + def <- snd <$> findASTTypeOrTermDef prog selection + maybe (throwM $ ActionOptionsNoID selection) pure $ + Available.options (snd <$> allTypeDefs) (snd <$> allDefs) (progCxt prog) level def selection action findASTDef :: MonadThrow m => Map GVarName (Editable, Def.Def) -> GVarName -> m (Editable, ASTDef) findASTDef allDefs def = case allDefs Map.!? def of @@ -1078,6 +1124,19 @@ findASTDef allDefs def = case allDefs Map.!? def of Just (_, Def.DefPrim _) -> throwM $ UnexpectedPrimDef def Just (editable, Def.DefAST d) -> pure (editable, d) +findASTTypeDef :: MonadThrow m => Map TyConName (Editable, TypeDef.TypeDef a) -> TyConName -> m (Editable, ASTTypeDef a) +findASTTypeDef allTypeDefs def = case allTypeDefs Map.!? def of + Nothing -> throwM $ UnknownTypeDef def + Just (_, TypeDef.TypeDefPrim _) -> throwM $ UnexpectedPrimTypeDef def + Just (editable, TypeDef.TypeDefAST d) -> pure (editable, d) + +findASTTypeOrTermDef :: MonadThrow f => App.Prog -> Selection -> f (Editable, Either (ASTTypeDef TypeMeta) ASTDef) +findASTTypeOrTermDef prog = \case + App.SelectionTypeDef sel -> + Left <<$>> findASTTypeDef (progAllTypeDefsMeta prog) sel.def + App.SelectionDef sel -> + Right <<$>> findASTDef (progAllDefs prog) sel.def + applyActionNoInput :: (MonadIO m, MonadThrow m, MonadAPILog l m) => SessionId -> @@ -1086,15 +1145,10 @@ applyActionNoInput :: PrimerM m Prog applyActionNoInput = curry3 $ logAPI (noError ApplyActionNoInput) $ \(sid, selection, action) -> do prog <- getProgram sid - def <- snd <$> findASTDef (progAllDefs prog) selection.def + def <- snd <$> findASTTypeOrTermDef prog selection actions <- either (throwM . ToProgActionError (Available.NoInput action)) pure $ - toProgActionNoInput - (snd <$> progAllDefs prog) - def - selection.def - (selection.node <&> \s -> (s.nodeType, s.id)) - action + toProgActionNoInput (snd <$> progAllDefs prog) def selection action applyActions sid actions applyActionInput :: @@ -1105,15 +1159,10 @@ applyActionInput :: PrimerM m Prog applyActionInput = curry3 $ logAPI (noError ApplyActionInput) $ \(sid, body, action) -> do prog <- getProgram sid - def <- snd <$> findASTDef (progAllDefs prog) body.selection.def + def <- snd <$> findASTTypeOrTermDef prog body.selection actions <- either (throwM . ToProgActionError (Available.Input action)) pure $ - toProgActionInput - def - body.selection.def - (body.selection.node <&> \s -> (s.nodeType, s.id)) - body.option - action + toProgActionInput def body.selection body.option action applyActions sid actions data ApplyActionBody = ApplyActionBody @@ -1149,25 +1198,4 @@ redo = >>= either (throwM . RedoError) (pure . viewProg) -- | 'App.Selection' without any node metadata. -data Selection = Selection - { def :: GVarName - , node :: Maybe NodeSelection - } - deriving stock (Eq, Show, Read, Generic) - deriving (FromJSON, ToJSON) via PrimerJSON Selection - deriving anyclass (NFData) - -viewSelection :: App.Selection -> Selection -viewSelection App.Selection{..} = Selection{def = selectedDef, node = viewNodeSelection <$> selectedNode} - --- | 'App.NodeSelection' without any node metadata. -data NodeSelection = NodeSelection - { nodeType :: NodeType - , id :: ID - } - deriving stock (Eq, Show, Read, Generic) - deriving (FromJSON, ToJSON) via PrimerJSON NodeSelection - deriving anyclass (NFData) - -viewNodeSelection :: App.NodeSelection -> NodeSelection -viewNodeSelection sel@App.NodeSelection{nodeType} = NodeSelection{nodeType, id = getID sel} +type Selection = App.Selection' ID diff --git a/primer/src/Primer/Action.hs b/primer/src/Primer/Action.hs index 1d0e7c6dc..7d3b24cab 100644 --- a/primer/src/Primer/Action.hs +++ b/primer/src/Primer/Action.hs @@ -1,4 +1,6 @@ +{-# LANGUAGE BlockArguments #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} module Primer.Action ( @@ -17,6 +19,7 @@ module Primer.Action ( uniquifyDefName, toProgActionInput, toProgActionNoInput, + applyActionsToField, ) where import Foreword hiding (mod) @@ -24,18 +27,31 @@ import Foreword hiding (mod) import Control.Monad.Fresh (MonadFresh) import Data.Aeson (Value) import Data.Bifunctor.Swap qualified as Swap +import Data.Bitraversable (bisequence) +import Data.Functor.Compose (Compose (..)) import Data.Generics.Product (typed) import Data.List (findIndex) import Data.List.NonEmpty qualified as NE +import Data.Map (insert) import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T -import Optics (set, (%), (?~), (^.), (^?), _Just) +import Data.Tuple.Extra ((&&&)) +import Optics (over, set, (%), (?~), (^.), (^?), _Just) import Primer.Action.Actions (Action (..), Movement (..), QualifiedText) import Primer.Action.Available qualified as Available import Primer.Action.Errors (ActionError (..)) import Primer.Action.ProgAction (ProgAction (..)) -import Primer.App.Base (NodeType (..)) +import Primer.App.Base ( + DefSelection (..), + NodeSelection (..), + NodeType (..), + Selection' (..), + TypeDefConsFieldSelection (..), + TypeDefConsSelection (..), + TypeDefNodeSelection (..), + TypeDefSelection (..), + ) import Primer.Core ( Expr, Expr' (..), @@ -52,6 +68,7 @@ import Primer.Core ( Type' (..), TypeCache (..), TypeCacheBoth (..), + TypeMeta, ValConName, baseName, bindName, @@ -94,7 +111,7 @@ import Primer.Def ( Def (..), DefMap, ) -import Primer.Module (Module, insertDef) +import Primer.Module (Module (moduleTypes), insertDef) import Primer.Name (Name, NameCounter, unName, unsafeMkName) import Primer.Name.Fresh ( isFresh, @@ -230,6 +247,56 @@ applyActionsToTypeSig smartHoles imports (mod, mods) (defName, def) actions = -- In this case we just refocus on the top of the type. z -> maybe unwrapError pure (focusType (unfocusLoc z)) +applyActionsToField :: + (MonadFresh ID m, MonadFresh NameCounter m) => + SmartHoles -> + [Module] -> + (Module, [Module]) -> + (Name, ValConName, Int, ASTTypeDef TypeMeta) -> + [Action] -> + m (Either ActionError ([Module], TypeZ)) +applyActionsToField smartHoles imports (mod, mods) (tyName, conName', index, tyDef) actions = + runReaderT + go + (buildTypingContextFromModules (mod : mods <> imports) smartHoles) + & runExceptT + where + go :: ActionM m => m ([Module], TypeZ) + go = do + (tz, cs) <- + getCompose + . flip (findAndAdjustA ((== conName') . valConName)) (astTypeDefConstructors tyDef) + $ Compose . \(ValCon _ ts) -> do + (tz', cs') <- + getCompose . flip (adjustAtA index) ts $ + Compose + . fmap (First . Just &&& target . top) + . flip withWrappedType \tz'' -> + foldlM (\l a -> local addParamsToCxt $ applyActionAndSynth a l) (InType tz'') actions + maybe + (throwError $ InternalFailure "applyActionsToField: con field index out of bounds") + (pure . first (First . Just)) + $ bisequence (getFirst tz', ValCon conName' <$> cs') + (valCons, zt) <- + maybe (throwError $ InternalFailure "applyActionsToField: con name not found") pure $ + bisequence (cs, getFirst tz) + let mod' = mod{moduleTypes = insert tyName (TypeDefAST tyDef{astTypeDefConstructors = valCons}) $ moduleTypes mod} + (,zt) <$> checkEverything smartHoles (CheckEverything{trusted = imports, toCheck = mod' : mods}) + addParamsToCxt :: TC.Cxt -> TC.Cxt + addParamsToCxt = over #localCxt (<> Map.fromList (map (bimap unLocalName TC.K) $ astTypeDefParameters tyDef)) + withWrappedType :: ActionM m => Type -> (TypeZ -> m Loc) -> m TypeZ + withWrappedType ty f = do + wrappedType <- ann emptyHole (pure ty) + let unwrapError = throwError $ InternalFailure "applyActionsToField: failed to unwrap type" + wrapError = throwError $ InternalFailure "applyActionsToField: failed to wrap type" + focusedType = focusType $ focus wrappedType + case focusedType of + Nothing -> wrapError + Just wrappedTy -> + f wrappedTy >>= \case + InType zt -> pure zt + z -> maybe unwrapError pure (focusType (unfocusLoc z)) + data Refocus = Refocus { pre :: Loc , post :: Expr @@ -858,12 +925,11 @@ renameForall b zt = case target zt of -- | Convert a high-level 'Available.NoInputAction' to a concrete sequence of 'ProgAction's. toProgActionNoInput :: DefMap -> - ASTDef -> - GVarName -> - Maybe (NodeType, ID) -> + Either (ASTTypeDef a) ASTDef -> + Selection' ID -> Available.NoInputAction -> Either ActionError [ProgAction] -toProgActionNoInput defs def defName mNodeSel = \case +toProgActionNoInput defs def0 sel0 = \case Available.MakeCase -> toProgAction [ConstructCase] Available.MakeApp -> @@ -877,8 +943,9 @@ toProgActionNoInput defs def defName mNodeSel = \case Available.LetToRec -> toProgAction [ConvertLetToLetrec] Available.Raise -> do - id <- mid - pure [MoveToDef defName, CopyPasteBody (defName, id) [SetCursor id, Move Parent, Delete]] + id <- nodeID + sel <- termSel + pure [MoveToDef sel.def, CopyPasteBody (sel.def, id) [SetCursor id, Move Parent, Delete]] Available.EnterHole -> toProgAction [EnterHole] Available.RemoveHole -> @@ -894,7 +961,8 @@ toProgActionNoInput defs def defName mNodeSel = \case -- resulting in a new argument type. The result type is unchanged. -- The cursor location is also unchanged. -- e.g. A -> B -> C ==> A -> B -> ? -> C - id <- mid + id <- nodeID + def <- termDef type_ <- case findType id $ astDefType def of Just t -> pure t Nothing -> case map fst $ findNodeWithParent id $ astDefExpr def of @@ -910,35 +978,72 @@ toProgActionNoInput defs def defName mNodeSel = \case Available.MakeTApp -> toProgAction [ConstructTApp, Move Child1] Available.RaiseType -> do - id <- mid - pure [MoveToDef defName, CopyPasteSig (defName, id) [SetCursor id, Move Parent, Delete]] + id <- nodeID + sel <- termSel + pure [MoveToDef sel.def, CopyPasteSig (sel.def, id) [SetCursor id, Move Parent, Delete]] Available.DeleteType -> toProgAction [Delete] - Available.DuplicateDef -> + Available.DuplicateDef -> do + sel <- termSel + def <- termDef let sigID = getID $ astDefType def bodyID = getID $ astDefExpr def - copyName = uniquifyDefName (qualifiedModule defName) (unName (baseName defName) <> "Copy") defs + copyName = uniquifyDefName (qualifiedModule sel.def) (unName (baseName sel.def) <> "Copy") defs in pure - [ CreateDef (qualifiedModule defName) (Just copyName) - , CopyPasteSig (defName, sigID) [] - , CopyPasteBody (defName, bodyID) [] + [ CreateDef (qualifiedModule sel.def) (Just copyName) + , CopyPasteSig (sel.def, sigID) [] + , CopyPasteBody (sel.def, bodyID) [] ] - Available.DeleteDef -> - pure [DeleteDef defName] + Available.DeleteDef -> do + sel <- termSel + pure [DeleteDef sel.def] + Available.AddConField -> do + (defName, sel) <- conSel + d <- typeDef + vc <- + maybe (Left $ ValConNotFound defName sel.con) pure + . find ((== sel.con) . valConName) + $ astTypeDefConstructors d + let index = length $ valConArgs vc -- for now, we always add on to the end + pure [AddConField defName sel.con index $ TEmptyHole ()] where - toProgAction actions = toProg' actions defName <$> maybeToEither NoNodeSelection mNodeSel - mid = maybeToEither NoNodeSelection $ snd <$> mNodeSel + termSel = case sel0 of + SelectionDef s -> pure s + SelectionTypeDef _ -> Left NeedTermDefSelection + nodeID = do + sel <- termSel + maybeToEither NoNodeSelection $ (.meta) <$> sel.node + typeSel = case sel0 of + SelectionDef _ -> Left NeedTypeDefSelection + SelectionTypeDef s -> pure s + typeNodeSel = do + sel <- typeSel + maybe (Left NeedTypeDefNodeSelection) (pure . (sel.def,)) sel.node + conSel = + typeNodeSel >>= \case + (s0, TypeDefConsNodeSelection s) -> pure (s0, s) + _ -> Left NeedTypeDefConsSelection + conFieldSel = do + (ty, s) <- conSel + maybe (Left NeedTypeDefConsFieldSelection) (pure . (ty,s.con,)) s.field + toProgAction actions = do + case sel0 of + SelectionDef sel -> toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node + SelectionTypeDef _ -> do + (t, c, f) <- conFieldSel + pure [ConFieldAction t c f.index $ SetCursor f.meta : actions] + termDef = first (const NeedTermDef) def0 + typeDef = either Right (Left . const NeedTypeDef) def0 -- | Convert a high-level 'Available.InputAction', and associated 'Available.Option', -- to a concrete sequence of 'ProgAction's. toProgActionInput :: - ASTDef -> - GVarName -> - Maybe (NodeType, ID) -> + Either (ASTTypeDef a) ASTDef -> + Selection' ID -> Available.Option -> Available.InputAction -> Either ActionError [ProgAction] -toProgActionInput def defName mNodeSel opt0 = \case +toProgActionInput def0 sel0 opt0 = \case Available.MakeCon -> do opt <- optGlobal toProg [ConstructSaturatedCon opt] @@ -994,9 +1099,49 @@ toProgActionInput def defName mNodeSel opt0 = \case toProg [RenameForall opt] Available.RenameDef -> do opt <- optNoCxt - pure [RenameDef defName opt] + sel <- termSel + pure [RenameDef sel.def opt] + Available.RenameType -> do + opt <- optNoCxt + td <- typeSel + pure [RenameType td.def opt] + Available.RenameCon -> do + opt <- optNoCxt + (defName, sel) <- conSel + pure [RenameCon defName sel.con opt] + Available.RenameTypeParam -> do + opt <- optNoCxt + (defName, sel) <- typeParamSel + pure [RenameTypeParam defName sel opt] + Available.AddCon -> do + opt <- optNoCxt + sel <- typeSel + d <- typeDef + let index = length $ astTypeDefConstructors d -- for now, we always add on the end + pure [AddCon sel.def index opt] where - mid = maybeToEither NoNodeSelection $ snd <$> mNodeSel + termSel = case sel0 of + SelectionDef s -> pure s + SelectionTypeDef _ -> Left NeedTermDefSelection + nodeID = do + sel <- termSel + maybeToEither NoNodeSelection $ (.meta) <$> sel.node + typeSel = case sel0 of + SelectionDef _ -> Left NeedTypeDefSelection + SelectionTypeDef s -> pure s + typeNodeSel = do + sel <- typeSel + maybe (Left NeedTypeDefNodeSelection) (pure . (sel.def,)) sel.node + typeParamSel = + typeNodeSel >>= \case + (s0, TypeDefParamNodeSelection s) -> pure (s0, s) + _ -> Left NeedTypeDefParamSelection + conSel = + typeNodeSel >>= \case + (s0, TypeDefConsNodeSelection s) -> pure (s0, s) + _ -> Left NeedTypeDefConsSelection + termDef = first (const NeedTermDef) def0 + typeDef = either Right (Left . const NeedTypeDef) def0 optVar = case opt0.context of Just q -> GlobalVarRef $ unsafeMkGlobalName (q, opt0.option) Nothing -> LocalVarRef $ unsafeMkLocalName opt0.option @@ -1009,9 +1154,18 @@ toProgActionInput def defName mNodeSel opt0 = \case optGlobal = case opt0.context of Nothing -> Left $ NeedLocal opt0 Just q -> pure (q, opt0.option) - toProg actions = toProg' actions defName <$> maybeToEither NoNodeSelection mNodeSel + conFieldSel = do + (ty, s) <- conSel + maybe (Left NeedTypeDefConsFieldSelection) (pure . (ty,s.con,)) s.field + toProg actions = do + case sel0 of + SelectionDef sel -> toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node + SelectionTypeDef _ -> do + (t, c, f) <- conFieldSel + pure [ConFieldAction t c f.index $ SetCursor f.meta : actions] offerRefined = do - id <- mid + id <- nodeID + def <- termDef -- If we have a useful type, offer the refine action, otherwise offer the saturate action. case findNodeWithParent id $ astDefExpr def of Just (ExprNode e, _) -> pure $ case e ^. _exprMetaLens ^? _type % _Just % _chkedAt of @@ -1022,10 +1176,10 @@ toProgActionInput def defName mNodeSel opt0 = \case Just (sm, _) -> Left $ NeedType sm Nothing -> Left $ IDNotFound id -toProg' :: [Action] -> GVarName -> (NodeType, ID) -> [ProgAction] -toProg' actions defName (nt, id) = +toProg' :: [Action] -> GVarName -> NodeSelection ID -> [ProgAction] +toProg' actions defName sel = [ MoveToDef defName - , (SetCursor id : actions) & case nt of + , (SetCursor sel.meta : actions) & case sel.nodeType of SigNode -> SigAction BodyNode -> BodyAction ] diff --git a/primer/src/Primer/Action/Available.hs b/primer/src/Primer/Action/Available.hs index 278aada79..1e1b3c5a3 100644 --- a/primer/src/Primer/Action/Available.hs +++ b/primer/src/Primer/Action/Available.hs @@ -1,4 +1,5 @@ {-# LANGUAGE BlockArguments #-} +{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE NoFieldSelectors #-} -- | Compute all the possible actions which can be performed on a definition. @@ -14,10 +15,15 @@ module Primer.Action.Available ( FreeInput (..), Options (..), options, + forTypeDef, + forTypeDefParamNode, + forTypeDefConsNode, + forTypeDefConsFieldNode, ) where import Foreword +import Data.Either.Extra (eitherToMaybe) import Data.Map qualified as Map import Data.Set qualified as Set import Data.Tuple.Extra (fst3) @@ -31,9 +37,17 @@ import Optics ( ) import Primer.Action.Priorities qualified as P import Primer.App.Base ( + DefSelection (..), Editable (..), Level (..), + NodeSelection (..), NodeType (..), + Selection' (..), + TypeDefConsFieldSelection (..), + TypeDefConsSelection (..), + TypeDefNodeSelection (..), + TypeDefSelection (..), + getTypeDefConFieldType, ) import Primer.Core ( Expr, @@ -44,6 +58,8 @@ import Primer.Core ( ModuleName (unModuleName), Type, Type' (..), + TypeMeta, + ValConName, getID, unLocalName, _bindMeta, @@ -65,6 +81,7 @@ import Primer.Primitives (tChar, tInt) import Primer.Questions ( generateNameExpr, generateNameTy, + generateNameTyAvoiding, variablesInScopeExpr, variablesInScopeTy, ) @@ -89,6 +106,7 @@ import Primer.Zipper ( focusOn, focusOnTy, locToEither, + target, ) -- | An offered action. @@ -117,6 +135,7 @@ data NoInputAction | DeleteType | DuplicateDef | DeleteDef + | AddConField deriving stock (Eq, Ord, Show, Read, Enum, Bounded, Generic) deriving (ToJSON, FromJSON) via PrimerJSON NoInputAction @@ -140,6 +159,10 @@ data InputAction | MakeForall | RenameForall | RenameDef + | RenameType + | RenameCon + | RenameTypeParam + | AddCon deriving stock (Eq, Ord, Show, Read, Enum, Bounded, Generic) deriving (ToJSON, FromJSON) via PrimerJSON InputAction @@ -285,6 +308,54 @@ forType l type_ = ] delete = [NoInput DeleteType] +forTypeDef :: + Level -> + Editable -> + [Action] +forTypeDef _ NonEditable = mempty +forTypeDef l Editable = + sortByPriority + l + [ Input RenameType + , Input AddCon + ] + +forTypeDefParamNode :: + Level -> + Editable -> + [Action] +forTypeDefParamNode _ NonEditable = mempty +forTypeDefParamNode l Editable = + sortByPriority + l + [ Input RenameTypeParam + ] + +forTypeDefConsNode :: + Level -> + Editable -> + [Action] +forTypeDefConsNode _ NonEditable = mempty +forTypeDefConsNode l Editable = + sortByPriority + l + [ NoInput AddConField + , Input RenameCon + ] + +forTypeDefConsFieldNode :: + Level -> + Editable -> + ASTTypeDef TypeMeta -> + ValConName -> + Int -> + ID -> + [Action] +forTypeDefConsFieldNode _ NonEditable _ _ _ _ = mempty +forTypeDefConsFieldNode l Editable def con index id = + maybe mempty (sortByPriority l . forType l) $ + findType id =<< getTypeDefConFieldType def con index + -- | An input for an 'InputAction'. data Option = Option { option :: Text @@ -319,13 +390,13 @@ options :: DefMap -> Cxt -> Level -> - ASTDef -> - Maybe (NodeType, ID) -> + Either (ASTTypeDef TypeMeta) ASTDef -> + Selection' ID -> InputAction -> -- | Returns 'Nothing' if an ID was required but not passed, passed but not found in the tree, -- or found but didn't correspond to the expected sort of entity (type/expr/pattern). Maybe Options -options typeDefs defs cxt level def mNodeSel = \case +options typeDefs defs cxt level def0 sel0 = \case MakeCon -> pure . noFree @@ -380,6 +451,14 @@ options typeDefs defs cxt level def mNodeSel = \case freeVar <$> genNames (Right $ Just k) RenameDef -> pure $ freeVar [] + RenameType -> + pure $ freeVar [] + RenameCon -> + pure $ freeVar [] + RenameTypeParam -> + pure $ freeVar [] + AddCon -> + pure $ freeVar [] where freeVar opts = Options{opts, free = FreeVarName} noFree opts = Options{opts, free = FreeNone} @@ -394,24 +473,47 @@ options typeDefs defs cxt level def mNodeSel = \case pure $ (first (localOpt . unLocalName) <$> locals) <> (first globalOpt <$> globals) - findNode = do - (nt, id) <- mNodeSel - case nt of - BodyNode -> fst <$> findNodeWithParent id (astDefExpr def) - SigNode -> TypeNode <$> findType id (astDefType def) - genNames typeOrKind = do - z <- focusNode =<< mNodeSel - pure $ map localOpt $ flip runReader cxt $ case z of - Left zE -> generateNameExpr typeOrKind zE - Right zT -> generateNameTy typeOrKind zT - varsInScope = do - nodeSel <- mNodeSel - focusNode nodeSel <&> \case - Left zE -> variablesInScopeExpr defs zE - Right zT -> (variablesInScopeTy zT, [], []) - focusNode (nt, id) = case nt of - BodyNode -> Left . locToEither <$> focusOn id (astDefExpr def) - SigNode -> fmap Right $ focusOnTy id $ astDefType def + findNode = case sel0 of + SelectionDef sel -> do + nodeSel <- sel.node + def <- eitherToMaybe def0 + case nodeSel.nodeType of + BodyNode -> fst <$> findNodeWithParent nodeSel.meta (astDefExpr def) + SigNode -> TypeNode <$> findType nodeSel.meta (astDefType def) + SelectionTypeDef sel -> do + (_, zT) <- conField sel + pure $ TypeNode $ target zT + genNames typeOrKind = + map localOpt . flip runReader cxt <$> case sel0 of + SelectionDef sel -> do + z <- focusNode =<< sel.node + pure $ case z of + Left zE -> generateNameExpr typeOrKind zE + Right zT -> generateNameTy typeOrKind zT + SelectionTypeDef sel -> do + (def, zT) <- conField sel + pure $ generateNameTyAvoiding (unLocalName . fst <$> astTypeDefParameters def) typeOrKind zT + varsInScope = case sel0 of + SelectionDef sel -> do + nodeSel <- sel.node + focusNode nodeSel <&> \case + Left zE -> variablesInScopeExpr defs zE + Right zT -> (variablesInScopeTy zT, [], []) + SelectionTypeDef sel -> do + (def, zT) <- conField sel + pure (astTypeDefParameters def <> variablesInScopeTy zT, [], []) + focusNode nodeSel = do + def <- eitherToMaybe def0 + case nodeSel.nodeType of + BodyNode -> Left . locToEither <$> focusOn nodeSel.meta (astDefExpr def) + SigNode -> fmap Right $ focusOnTy nodeSel.meta $ astDefType def + conField sel = do + (con, field) <- case sel of + TypeDefSelection _ (Just (TypeDefConsNodeSelection (TypeDefConsSelection con (Just field)))) -> + Just (con, field) + _ -> Nothing + def <- either Just (const Nothing) def0 + map (def,) $ focusOnTy field.meta =<< getTypeDefConFieldType def con field.index -- Extract the source of the function type we were checked at -- i.e. the type that a lambda-bound variable would have here lamVarTy = \case @@ -455,6 +557,7 @@ sortByPriority l = DeleteType -> P.delete DuplicateDef -> P.duplicate DeleteDef -> P.delete + AddConField -> P.addConField Input a -> case a of MakeCon -> P.useSaturatedValueCon MakeInt -> P.makeInt @@ -474,3 +577,7 @@ sortByPriority l = MakeForall -> P.constructForall RenameForall -> P.rename RenameDef -> P.rename + RenameType -> P.rename + AddCon -> P.addCon + RenameCon -> P.rename + RenameTypeParam -> P.rename diff --git a/primer/src/Primer/Action/Errors.hs b/primer/src/Primer/Action/Errors.hs index 9e6d705c4..18b92789f 100644 --- a/primer/src/Primer/Action/Errors.hs +++ b/primer/src/Primer/Action/Errors.hs @@ -13,7 +13,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Primer.Action.Actions (Action) import Primer.Action.Available qualified as Available import Primer.Action.Movement (Movement) -import Primer.Core (Expr, GVarName, ID, LVarName, ModuleName, Type) +import Primer.Core (Expr, GVarName, ID, LVarName, ModuleName, TyConName, Type, ValConName) import Primer.JSON (CustomJSON (..), PrimerJSON) import Primer.Typecheck.TypeError (TypeError) import Primer.Zipper (SomeNode) @@ -63,6 +63,15 @@ data ActionError | NeedLocal Available.Option | NeedInt Available.Option | NeedChar Available.Option + | NeedTermDef + | NeedTypeDef + | NeedTermDefSelection + | NeedTypeDefSelection + | NeedTypeDefNodeSelection + | NeedTypeDefConsSelection + | NeedTypeDefConsFieldSelection + | NeedTypeDefParamSelection | NoNodeSelection + | ValConNotFound TyConName ValConName deriving stock (Eq, Show, Read, Generic) deriving (FromJSON, ToJSON) via PrimerJSON ActionError diff --git a/primer/src/Primer/Action/Priorities.hs b/primer/src/Primer/Action/Priorities.hs index 12d09c847..bbc84293c 100644 --- a/primer/src/Primer/Action/Priorities.hs +++ b/primer/src/Primer/Action/Priorities.hs @@ -55,6 +55,10 @@ module Primer.Action.Priorities ( constructTypeApp, constructForall, + -- * Type def actions. + addCon, + addConField, + -- * Generic actions. rename, duplicate, @@ -146,3 +150,9 @@ raise _ = 300 delete :: Level -> Int delete _ = maxBound + +addCon :: Level -> Int +addCon _ = 10 + +addConField :: Level -> Int +addConField _ = 10 diff --git a/primer/src/Primer/Action/ProgAction.hs b/primer/src/Primer/Action/ProgAction.hs index 47653b24e..d61fea903 100644 --- a/primer/src/Primer/Action/ProgAction.hs +++ b/primer/src/Primer/Action/ProgAction.hs @@ -39,14 +39,14 @@ data ProgAction RenameTypeParam TyConName TyVarName Text | -- | Add a value constructor at the given position, in the given type AddCon TyConName Int Text - | -- | Change the type of the field at the given index of the given constructor - SetConFieldType TyConName ValConName Int (Type' ()) | -- | Add a new field, at the given index, to the given constructor AddConField TyConName ValConName Int (Type' ()) | -- | Execute a sequence of actions on the body of the definition BodyAction [Action] | -- | Execute a sequence of actions on the type annotation of the definition SigAction [Action] + | -- | Execute a sequence of actions on the type of a field of a constructor in a typedef + ConFieldAction TyConName ValConName Int [Action] | SetSmartHoles SmartHoles | -- | CopyPaste (d,i) as -- remembers the tree in def d, node i diff --git a/primer/src/Primer/Action/ProgError.hs b/primer/src/Primer/Action/ProgError.hs index a2fa35c27..6b5ece12b 100644 --- a/primer/src/Primer/Action/ProgError.hs +++ b/primer/src/Primer/Action/ProgError.hs @@ -11,6 +11,7 @@ import Primer.Name (Name) data ProgError = NoDefSelected + | NoTypeDefSelected | DefNotFound GVarName | DefAlreadyExists GVarName | DefInUse GVarName diff --git a/primer/src/Primer/App.hs b/primer/src/Primer/App.hs index f758670d5..9ff3a3001 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -1,5 +1,8 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE ViewPatterns #-} -- This module defines the high level application functions. @@ -31,6 +34,7 @@ module Primer.App ( progAllModules, progAllDefs, progAllTypeDefs, + progAllTypeDefsMeta, allValConNames, allTyConNames, progCxt, @@ -48,8 +52,6 @@ module Primer.App ( handleEvalFullRequest, importModules, MutationRequest (..), - Selection (..), - NodeSelection (..), EvalReq (..), EvalResp (..), EvalFullReq (..), @@ -63,13 +65,12 @@ import Foreword hiding (mod) import Control.Monad.Fresh (MonadFresh (..)) import Control.Monad.Log (MonadLog, WithSeverity) import Control.Monad.NestedError (MonadNestedError, throwError') -import Data.Data (Data) import Data.Generics.Uniplate.Operations (transform, transformM) import Data.Generics.Uniplate.Zipper ( fromZipper, ) import Data.List (intersect, (\\)) -import Data.List.Extra (anySame, disjoint, (!?)) +import Data.List.Extra (anySame, disjoint) import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Optics ( @@ -78,7 +79,6 @@ import Optics ( Field3 (_3), ReversibleOptic (re), ifoldMap, - lens, mapped, over, set, @@ -101,24 +101,32 @@ import Primer.Action ( ProgAction (..), applyAction', applyActionsToBody, + applyActionsToField, applyActionsToTypeSig, ) import Primer.Action.ProgError (ProgError (..)) import Primer.App.Base ( + DefSelection (..), Editable (..), Level (..), + NodeSelection (..), NodeType (..), + Selection, + Selection' (..), + TypeDefConsFieldSelection (..), + TypeDefConsSelection (..), + TypeDefNodeSelection (..), + TypeDefSelection (..), + getTypeDefConFieldType, ) import Primer.Core ( Bind' (Bind), CaseBranch, CaseBranch' (CaseBranch), Expr, - Expr' (Case, Con, EmptyHole, Hole, Var), - ExprMeta, + Expr' (Case, Con, EmptyHole, Var), GVarName, GlobalName (baseName, qualifiedModule), - HasID (_id), ID (..), LocalName (LocalName, unLocalName), Meta (..), @@ -163,6 +171,7 @@ import Primer.Module ( insertDef, moduleDefsQualified, moduleTypesQualified, + moduleTypesQualifiedMeta, nextModuleID, primitiveModule, qualifyDefName, @@ -196,7 +205,6 @@ import Primer.Typecheck ( checkTypeDefs, synth, ) -import Primer.Typecheck qualified as TC import Primer.Zipper ( ExprZ, Loc' (InBind, InExpr, InType), @@ -273,6 +281,11 @@ progAllTypeDefs p = foldMap' (fmap (Editable,) . moduleTypesQualified) (progModules p) <> foldMap' (fmap (NonEditable,) . moduleTypesQualified) (progImports p) +progAllTypeDefsMeta :: Prog -> Map TyConName (Editable, TypeDef TypeMeta) +progAllTypeDefsMeta p = + foldMap' (fmap (Editable,) . moduleTypesQualifiedMeta) (progModules p) + <> foldMap' (fmap (NonEditable,) . moduleTypesQualifiedMeta) (progImports p) + progAllDefs :: Prog -> Map GVarName (Editable, Def) progAllDefs p = foldMap' (fmap (Editable,) . moduleDefsQualified) (progModules p) @@ -325,11 +338,12 @@ newEmptyProgImporting imported = ] , progImports = imported' , progSelection = - Just - Selection - { selectedDef = qualifyName moduleName defName - , selectedNode = Nothing - } + Just $ + SelectionDef + DefSelection + { def = qualifyName moduleName defName + , node = Nothing + } } , nextID , toEnum 0 @@ -396,6 +410,9 @@ importModules ms = do allTypes :: Prog -> TypeDefMap allTypes = fmap snd . progAllTypeDefs +allTypesMeta :: Prog -> Map TyConName (TypeDef TypeMeta) +allTypesMeta = fmap snd . progAllTypeDefsMeta + -- | Get all definitions from all modules (including imports) allDefs :: Prog -> DefMap allDefs = fmap snd . progAllDefs @@ -415,33 +432,6 @@ newtype Log = Log {unlog :: [[ProgAction]]} defaultLog :: Log defaultLog = Log mempty --- | Describes what interface element the student has selected. --- A definition in the left hand nav bar, and possibly a node in that definition. -data Selection = Selection - { selectedDef :: GVarName - -- ^ the ID of some ASTDef - , selectedNode :: Maybe NodeSelection - } - deriving stock (Eq, Show, Read, Generic, Data) - deriving (FromJSON, ToJSON) via PrimerJSON Selection - deriving anyclass (NFData) - --- | A selected node, in the body or type signature of some definition. --- We have the following invariant: @nodeType = SigNode ==> isRight meta@ -data NodeSelection = NodeSelection - { nodeType :: NodeType - , meta :: Either ExprMeta TypeMeta - } - deriving stock (Eq, Show, Read, Generic, Data) - deriving (FromJSON, ToJSON) via PrimerJSON NodeSelection - deriving anyclass (NFData) - -instance HasID NodeSelection where - _id = - lens - (either getID getID . meta) - (flip $ \id -> over #meta $ bimap (set _id id) (set _id id)) - -- | The type of requests which can mutate the application state. data MutationRequest = Undo @@ -587,7 +577,7 @@ applyProgAction prog = \case m <- lookupEditableModule (qualifiedModule d) prog case Map.lookup d $ moduleDefsQualified m of Nothing -> throwError $ DefNotFound d - Just _ -> pure $ prog & #progSelection ?~ Selection d Nothing + Just _ -> pure $ prog & #progSelection ?~ SelectionDef (DefSelection d Nothing) DeleteDef d -> editModuleCross (qualifiedModule d) prog $ \(m, ms) -> case deleteDef m d of Nothing -> throwError $ DefNotFound d @@ -610,7 +600,7 @@ applyProgAction prog = \case (traversed % #moduleDefs % traversed % #_DefAST % #astDefExpr) (renameVar (GlobalVarRef d) (GlobalVarRef newName)) (m' : ms) - pure (renamedModules, Just $ Selection newName Nothing) + pure (renamedModules, Just $ SelectionDef $ DefSelection newName Nothing) CreateDef modName n -> editModule modName prog $ \mod -> do let defs = moduleDefs mod name <- case n of @@ -624,29 +614,35 @@ applyProgAction prog = \case expr <- newExpr ty <- newType let def = ASTDef expr ty - pure (insertDef mod name $ DefAST def, Just $ Selection (qualifyName modName name) Nothing) - AddTypeDef tc td -> editModuleSameSelection (qualifiedModule tc) prog $ \m -> do + pure (insertDef mod name $ DefAST def, Just $ SelectionDef $ DefSelection (qualifyName modName name) Nothing) + AddTypeDef tc td -> editModule (qualifiedModule tc) prog $ \m -> do td' <- generateTypeDefIDs $ TypeDefAST td let tydefs' = moduleTypes m <> Map.singleton (baseName tc) td' - m{moduleTypes = tydefs'} - <$ liftError - -- The frontend should never let this error case happen, - -- so we just dump out a raw string for debugging/logging purposes - -- (This is not currently true! We should synchronise the frontend with - -- the typechecker rules. For instance, the form allows to create - -- data T (T : *) = T - -- but the TC rejects it. - -- see https://github.com/hackworthltd/primer/issues/3) - (TypeDefError . show @TypeError) - ( runReaderT - (checkTypeDefs $ Map.singleton tc (TypeDefAST td)) - (buildTypingContextFromModules (progAllModules prog) NoSmartHoles) - ) - RenameType old (unsafeMkName -> nameRaw) -> editModuleSameSelectionCross (qualifiedModule old) prog $ \(m, ms) -> do + liftError + -- The frontend should never let this error case happen, + -- so we just dump out a raw string for debugging/logging purposes + -- (This is not currently true! We should synchronise the frontend with + -- the typechecker rules. For instance, the form allows to create + -- data T (T : *) = T + -- but the TC rejects it. + -- see https://github.com/hackworthltd/primer/issues/3) + (TypeDefError . show @TypeError) + ( runReaderT + (checkTypeDefs $ Map.singleton tc (TypeDefAST td)) + (buildTypingContextFromModules (progAllModules prog) NoSmartHoles) + ) + pure + ( m{moduleTypes = tydefs'} + , Just $ SelectionTypeDef $ TypeDefSelection tc Nothing + ) + RenameType old (unsafeMkName -> nameRaw) -> editModuleCross (qualifiedModule old) prog $ \(m, ms) -> do when (new `elem` allTyConNames prog) $ throwError $ TypeDefAlreadyExists new m' <- traverseOf #moduleTypes updateType m let renamedInTypes = over (traversed % #moduleTypes) updateRefsInTypes $ m' : ms - pure $ over (traversed % #moduleDefs % traversed % #_DefAST) (updateDefBody . updateDefType) renamedInTypes + pure + ( over (traversed % #moduleDefs % traversed % #_DefAST) (updateDefBody . updateDefType) renamedInTypes + , Just $ SelectionTypeDef $ TypeDefSelection new Nothing + ) where new = qualifyName (qualifiedModule old) nameRaw updateType m = do @@ -676,10 +672,13 @@ applyProgAction prog = \case $ over (#_TCon % _2) updateName updateName n = if n == old then new else n RenameCon type_ old (unsafeMkGlobalName . (fmap unName (unModuleName (qualifiedModule type_)),) -> new) -> - editModuleSameSelectionCross (qualifiedModule type_) prog $ \(m, ms) -> do + editModuleCross (qualifiedModule type_) prog $ \(m, ms) -> do when (new `elem` allValConNames prog) $ throwError $ ConAlreadyExists new m' <- updateType m - pure $ over (mapped % #moduleDefs) updateDefs (m' : ms) + pure + ( over (mapped % #moduleDefs) updateDefs (m' : ms) + , Just $ SelectionTypeDef $ TypeDefSelection type_ $ Just $ TypeDefConsNodeSelection $ TypeDefConsSelection new Nothing + ) where updateType = alterTypeDef @@ -697,7 +696,12 @@ applyProgAction prog = \case . over (#_Case % _3 % traversed % #_CaseBranch % _1) updateName updateName n = if n == old then new else n RenameTypeParam type_ old (unsafeMkLocalName -> new) -> - editModuleSameSelection (qualifiedModule type_) prog updateType + editModule (qualifiedModule type_) prog $ \m -> do + m' <- updateType m + pure + ( m' + , Just $ SelectionTypeDef $ TypeDefSelection type_ $ Just $ TypeDefParamNodeSelection new + ) where updateType = alterTypeDef @@ -725,13 +729,18 @@ applyProgAction prog = \case $ \(_, v) -> tvar $ updateName v updateName n = if n == old then new else n AddCon type_ index (unsafeMkGlobalName . (fmap unName (unModuleName (qualifiedModule type_)),) -> con) -> - editModuleSameSelectionCross (qualifiedModule type_) prog $ \(m, ms) -> do + editModuleCross (qualifiedModule type_) prog $ \(m, ms) -> do when (con `elem` allValConNames prog) $ throwError $ ConAlreadyExists con m' <- updateType m - traverseOf - (traversed % #moduleDefs % traversed % #_DefAST % #astDefExpr) - updateDefs - $ m' : ms + ms' <- + traverseOf + (traversed % #moduleDefs % traversed % #_DefAST % #astDefExpr) + updateDefs + $ m' : ms + pure + ( ms' + , Just $ SelectionTypeDef $ TypeDefSelection type_ $ Just $ TypeDefConsNodeSelection $ TypeDefConsSelection con Nothing + ) where updateDefs = transformCaseBranches prog type_ $ \bs -> do m' <- DSL.meta @@ -743,72 +752,20 @@ applyProgAction prog = \case (maybe (throwError $ IndexOutOfRange index) pure . insertAt index (ValCon con [])) ) type_ - SetConFieldType type_ con index new -> - editModuleSameSelectionCross (qualifiedModule type_) prog $ \(m, ms) -> do - m' <- updateType m - traverseOf (traversed % #moduleDefs) updateDefs (m' : ms) - where - updateType = - alterTypeDef - ( traverseOf #astTypeDefConstructors $ - maybe (throwError $ ConNotFound con) pure - <=< findAndAdjustA - ((== con) . valConName) - ( traverseOf - #valConArgs - ( maybe (throwError $ IndexOutOfRange index) pure - <=< adjustAtA index (const $ generateTypeIDs new) - ) - ) - ) - type_ - updateDefs = traverseOf (traversed % #_DefAST % #astDefExpr) (updateDecons <=< updateCons) - updateCons = - let enhole x = case x of - -- Previously the @index@th argument @t@ to this - -- constructor would have been typechecked against the old - -- field type @S@, @S ∋ t@. - -- With the new field type @T@, we need to change @t@ to - -- @t'@ such that @T ∋ t'@, which we do by: - -- - if @t@ is a hole, set @t'=t@ - -- - otherwise (whether @t@ were synthesisable or checkable), set @t' = {? t ?}@ - -- Note also that we assume the metadata (typecache) is up - -- to date or blank, and we ensure this is the case in our - -- output. - -- We must work here to ensure that the result is - -- well-typed, since we wish to avoid rechecking the whole - -- program just to fix it up using smartholes. - EmptyHole{} -> pure x - Hole{} -> pure x - _ -> TC.enhole new x - in transformM $ \case - Con m con' tms | con' == con -> do - adjustAtA index enhole tms >>= \case - Just args' -> pure $ Con m con' args' - Nothing -> throwError $ ConNotSaturated con - e -> pure e - updateDecons = transformCaseBranches prog type_ $ - traverse $ \cb@(CaseBranch vc binds e) -> - if vc == con - then do - Bind _ v <- maybe (throwError $ IndexOutOfRange index) pure $ binds !? index - CaseBranch vc binds - <$> - -- TODO a custom traversal could be more efficient - reusing `_freeTmVars` means that we continue in - -- to parts of the tree where `v` is shadowed, and thus where the traversal will never have any effect - traverseOf - _freeTmVars - ( \(m, v') -> - if v' == v - then Hole <$> DSL.meta <*> pure (Var m $ LocalVarRef v') - else pure (Var m $ LocalVarRef v') - ) - e - else pure cb AddConField type_ con index new -> - editModuleSameSelectionCross (qualifiedModule type_) prog $ \(m, ms) -> do + editModuleCross (qualifiedModule type_) prog $ \(m, ms) -> do m' <- updateType m - traverseOf (traversed % #moduleDefs) updateDefs (m' : ms) + ms' <- traverseOf (traversed % #moduleDefs) updateDefs (m' : ms) + pure + ( ms' + , Just + . SelectionTypeDef + . TypeDefSelection type_ + . Just + . TypeDefConsNodeSelection + . TypeDefConsSelection con + $ Nothing + ) where updateType = alterTypeDef @@ -853,8 +810,8 @@ applyProgAction prog = \case let meta = bimap (view _exprMetaLens . target) (view _typeMetaLens . target) $ locToEither z pure ( insertDef m defName (DefAST def') - , Just $ - Selection (qualifyDefName m defName) $ + , Just . SelectionDef $ + DefSelection (qualifyDefName m defName) $ Just NodeSelection { nodeType = BodyNode @@ -871,14 +828,39 @@ applyProgAction prog = \case meta = view _typeMetaLens node in pure ( mod' - , Just $ - Selection (qualifyDefName curMod defName) $ + , Just . SelectionDef $ + DefSelection (qualifyDefName curMod defName) $ Just NodeSelection { nodeType = SigNode , meta = Right meta } ) + ConFieldAction tyName con index actions -> editModuleOfCrossType (Just tyName) prog $ \ms defName def -> do + let smartHoles = progSmartHoles prog + applyActionsToField smartHoles (progImports prog) ms (defName, con, index, def) actions >>= \case + Left err -> throwError $ ActionError err + Right (mod', zt) -> + pure + ( mod' + , Just $ + SelectionTypeDef + TypeDefSelection + { def = tyName + , node = + Just $ + TypeDefConsNodeSelection + TypeDefConsSelection + { con + , field = + Just + TypeDefConsFieldSelection + { index + , meta = Right $ zt ^. _target % _typeMetaLens + } + } + } + ) SetSmartHoles smartHoles -> pure $ prog & #progSmartHoles .~ smartHoles CopyPasteSig fromIds setup -> case mdefName of @@ -912,7 +894,9 @@ applyProgAction prog = \case ActionError $ InternalFailure "RenameModule: imported modules were edited by renaming" where - mdefName = selectedDef <$> progSelection prog + mdefName = case progSelection prog of + Just (SelectionDef s) -> Just s.def + _ -> Nothing -- Helper for RenameModule action data RenameMods a = RM {imported :: [a], editable :: [a]} @@ -974,15 +958,6 @@ editModuleSameSelection :: m Prog editModuleSameSelection n p f = editModule n p (fmap (,progSelection p) . f) --- A variant of 'editModuleSameSelection' for actions which can affect multiple modules -editModuleSameSelectionCross :: - MonadError ProgError m => - ModuleName -> - Prog -> - ((Module, [Module]) -> m [Module]) -> - m Prog -editModuleSameSelectionCross n p f = editModuleCross n p (fmap (,progSelection p) . f) - editModuleOf :: MonadError ProgError m => Maybe GVarName -> @@ -1010,6 +985,19 @@ editModuleOfCross mdefName prog f = case mdefName of Just (DefAST def) -> f ms (baseName defname) def _ -> throwError $ DefNotFound defname +editModuleOfCrossType :: + MonadError ProgError m => + Maybe TyConName -> + Prog -> + ((Module, [Module]) -> Name -> ASTTypeDef TypeMeta -> m ([Module], Maybe Selection)) -> + m Prog +editModuleOfCrossType mdefName prog f = case mdefName of + Nothing -> throwError NoTypeDefSelected + Just defname -> editModuleCross (qualifiedModule defname) prog $ \ms@(m, _) -> + case Map.lookup (baseName defname) (moduleTypes m) of + Just (TypeDefAST def) -> f ms (baseName defname) def + _ -> throwError $ TypeDefNotFound defname + -- | Undo the last block of actions. -- -- If there are no actions in the log we return the program unchanged. @@ -1340,7 +1328,7 @@ copyPasteSig p (fromDefName, fromTyId) toDefName setup = do _ -> throwError $ CopyPasteError "copy/paste setup didn't select an empty hole" let newDef = oldDef{astDefType = fromZipper pasted} let newSel = NodeSelection SigNode (pasted ^. _target % _typeMetaLens % re _Right) - pure (insertDef mod toDefBaseName (DefAST newDef), Just (Selection toDefName $ Just newSel)) + pure (insertDef mod toDefBaseName (DefAST newDef), Just (SelectionDef $ DefSelection toDefName $ Just newSel)) liftError ActionError $ tcWholeProg finalProg -- We cannot use bindersAbove as that works on names only, and different scopes @@ -1408,9 +1396,9 @@ tcWholeProg p = do let oldSel = progSelection p newSel <- case oldSel of Nothing -> pure Nothing - Just s -> do - let defName_ = s ^. #selectedDef - updatedNode <- case s ^. #selectedNode of + Just (SelectionDef s) -> do + let defName_ = s.def + updatedNode <- case s.node of Nothing -> pure Nothing Just sel@NodeSelection{nodeType} -> do n <- runExceptT $ focusNode p' defName_ $ getID sel @@ -1419,11 +1407,23 @@ tcWholeProg p = do (SigNode, Right (Right x)) -> pure $ Just $ NodeSelection SigNode $ x ^. _target % _typeMetaLens % re _Right _ -> pure Nothing -- something's gone wrong: expected a SigNode, but found it in the body, or vv, or just not found it pure $ - Just $ - Selection - { selectedDef = defName_ - , selectedNode = updatedNode + Just . SelectionDef $ + DefSelection + { def = defName_ + , node = updatedNode } + Just (SelectionTypeDef s) -> + pure . Just . SelectionTypeDef $ + s & over (#node % mapped % #_TypeDefConsNodeSelection) \conSel -> + conSel & over #field \case + Nothing -> Nothing + Just fieldSel -> + flip (set #meta) fieldSel . (Right . (^. _typeMetaLens)) <$> do + -- If something goes wrong in finding the metadata, we just don't set a field selection. + -- This is similar to what we do when selection is in a term, above. + td <- Map.lookup s.def $ allTypesMeta p + tda <- typeDefAST td + getTypeDefConFieldType tda conSel.con fieldSel.index pure $ p'{progSelection = newSel} -- | Do a full check of a 'Prog', both the imports and the local modules @@ -1495,7 +1495,7 @@ copyPasteBody p (fromDefName, fromId) toDefName setup = do _ -> throwError $ CopyPasteError "copy/paste setup didn't select an empty hole" let newDef = oldDef{astDefExpr = unfocusExpr $ unfocusType pasted} let newSel = NodeSelection BodyNode (pasted ^. _target % _typeMetaLens % re _Right) - pure (insertDef mod toDefBaseName (DefAST newDef), Just (Selection toDefName $ Just newSel)) + pure (insertDef mod toDefBaseName (DefAST newDef), Just (SelectionDef $ DefSelection toDefName $ Just newSel)) (Left srcE, InExpr tgtE) -> do let sharedScope = if fromDefName == toDefName @@ -1552,7 +1552,7 @@ copyPasteBody p (fromDefName, fromId) toDefName setup = do _ -> throwError $ CopyPasteError "copy/paste setup didn't select an empty hole" let newDef = oldDef{astDefExpr = unfocusExpr pasted} let newSel = NodeSelection BodyNode (pasted ^. _target % _exprMetaLens % re _Left) - pure (insertDef mod toDefBaseName (DefAST newDef), Just (Selection toDefName $ Just newSel)) + pure (insertDef mod toDefBaseName (DefAST newDef), Just (SelectionDef $ DefSelection toDefName $ Just newSel)) liftError ActionError $ tcWholeProg finalProg lookupASTDef :: GVarName -> DefMap -> Maybe ASTDef diff --git a/primer/src/Primer/App/Base.hs b/primer/src/Primer/App/Base.hs index 6f4f834f5..0abf4544d 100644 --- a/primer/src/Primer/App/Base.hs +++ b/primer/src/Primer/App/Base.hs @@ -1,20 +1,48 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE PartialTypeSignatures #-} +{-# LANGUAGE NoFieldSelectors #-} + -- | Definitions needed to build the app. -- These are not part of the core language, but we may want to use them in dependencies of 'Primer.App'. module Primer.App.Base ( Level (..), Editable (..), NodeType (..), + Selection, + Selection' (..), + TypeDefSelection (..), + TypeDefNodeSelection (..), + TypeDefConsSelection (..), + TypeDefConsFieldSelection (..), + DefSelection (..), + NodeSelection (..), + getTypeDefConFieldType, ) where import Protolude import Data.Data (Data) +import Optics +import Primer.Core ( + ExprMeta, + GVarName, + HasID (..), + TyConName, + TyVarName, + Type', + TypeMeta, + ValConName, + getID, + ) import Primer.JSON ( CustomJSON (CustomJSON), FromJSON, PrimerJSON, ToJSON, ) +import Primer.TypeDef (ASTTypeDef, ValCon (..), astTypeDefConstructors) -- | The current programming "level". This setting determines which -- actions are displayed to the student, the labels on UI elements, @@ -38,3 +66,76 @@ data NodeType = BodyNode | SigNode deriving stock (Eq, Show, Read, Bounded, Enum, Generic, Data) deriving (FromJSON, ToJSON) via PrimerJSON NodeType deriving anyclass (NFData) + +-- | Describes which element of a (type or term) definition the student has selected. +-- We have the following invariant: when this contains a `NodeSelection` with @nodeType = SigNode@, +-- or any `TypeDefConsFieldSelection`, then they will always have @meta = Right _@. +type Selection = Selection' (Either ExprMeta TypeMeta) + +data Selection' a + = SelectionDef (DefSelection a) + | SelectionTypeDef (TypeDefSelection a) + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (Selection' a) + deriving anyclass (NFData) + +-- | Some element of a type definition. +data TypeDefSelection a = TypeDefSelection + { def :: TyConName + , node :: Maybe (TypeDefNodeSelection a) + } + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (TypeDefSelection a) + deriving anyclass (NFData) + +-- | Some element in a type definition, other than simply the definition itself. +data TypeDefNodeSelection a + = TypeDefParamNodeSelection TyVarName + | TypeDefConsNodeSelection (TypeDefConsSelection a) + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (TypeDefNodeSelection a) + deriving anyclass (NFData) + +-- | Some element of a definition of a constructor. +data TypeDefConsSelection a = TypeDefConsSelection + { con :: ValConName + , field :: Maybe (TypeDefConsFieldSelection a) + } + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (TypeDefConsSelection a) + deriving anyclass (NFData) + +-- | Some element of a field in the definition of a constructor. +data TypeDefConsFieldSelection a = TypeDefConsFieldSelection + { index :: Int + , meta :: a + } + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (TypeDefConsFieldSelection a) + deriving anyclass (NFData) + +-- | Some element of a term definition. +data DefSelection a = DefSelection + { def :: GVarName + , node :: Maybe (NodeSelection a) + } + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (DefSelection a) + deriving anyclass (NFData) + +-- | Some element of a node, in the body or type signature of a term definition. +data NodeSelection a = NodeSelection + { nodeType :: NodeType + , meta :: a + } + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (NodeSelection a) + deriving anyclass (NFData) + +instance HasID a => HasID (NodeSelection a) where + _id = lens (getID . (.meta)) (flip $ over #meta . set _id) + +getTypeDefConFieldType :: ASTTypeDef a -> ValConName -> Int -> Maybe (Type' a) +getTypeDefConFieldType def con index = + flip atMay index . valConArgs + =<< find ((== con) . valConName) (astTypeDefConstructors def) diff --git a/primer/src/Primer/Core/Meta.hs b/primer/src/Primer/Core/Meta.hs index c9101b8ec..161a473be 100644 --- a/primer/src/Primer/Core/Meta.hs +++ b/primer/src/Primer/Core/Meta.hs @@ -159,6 +159,12 @@ instance HasID ID where instance HasID (Meta a) where _id = position @1 +instance (HasID a, HasID b) => HasID (Either a b) where + _id = + lens + (either getID getID) + (flip $ \id -> bimap (set _id id) (set _id id)) + -- This instance is used in 'Primer.Zipper', but it would be an orphan if we defined it there. instance HasID a => HasID (Zipper a a) where _id = lens getter setter diff --git a/primer/src/Primer/JSON.hs b/primer/src/Primer/JSON.hs index d72dec9a6..ef7a17428 100644 --- a/primer/src/Primer/JSON.hs +++ b/primer/src/Primer/JSON.hs @@ -13,8 +13,7 @@ import Data.Aeson ( ToJSON, ToJSONKey, ) -import Deriving.Aeson (CustomJSON (..)) -import Deriving.Aeson.Stock (Vanilla) +import Deriving.Aeson (CustomJSON (..), OmitNothingFields) -- | A type for Primer API JSON encodings. -- @@ -43,4 +42,10 @@ import Deriving.Aeson.Stock (Vanilla) -- -- * @SumTwoElemArray@ is unsupported by openapi3 as it is unrepresentable in -- a schema. -type PrimerJSON a = Vanilla a +type PrimerJSON a = + CustomJSON + '[ -- This protects some clients (e.g. TypeScript) from spurious equality failures + -- due to null-vs-omitted inconsistency. + OmitNothingFields + ] + a diff --git a/primer/src/Primer/Module.hs b/primer/src/Primer/Module.hs index 35213bd2f..2f31cc452 100644 --- a/primer/src/Primer/Module.hs +++ b/primer/src/Primer/Module.hs @@ -2,6 +2,7 @@ module Primer.Module ( Module (..), qualifyTyConName, moduleTypesQualified, + moduleTypesQualifiedMeta, qualifyDefName, moduleDefsQualified, insertDef, @@ -77,7 +78,10 @@ qualifyTyConName :: Module -> Name -> TyConName qualifyTyConName m = qualifyName (moduleName m) moduleTypesQualified :: Module -> TypeDefMap -moduleTypesQualified m = mapKeys (qualifyTyConName m) $ forgetTypeDefMetadata <$> moduleTypes m +moduleTypesQualified = map forgetTypeDefMetadata . moduleTypesQualifiedMeta + +moduleTypesQualifiedMeta :: Module -> Map TyConName (TypeDef TypeMeta) +moduleTypesQualifiedMeta m = mapKeys (qualifyTyConName m) $ moduleTypes m qualifyDefName :: Module -> Name -> GVarName qualifyDefName m = qualifyName (moduleName m) diff --git a/primer/src/Primer/Questions.hs b/primer/src/Primer/Questions.hs index 8ee6f064b..47df5f2e6 100644 --- a/primer/src/Primer/Questions.hs +++ b/primer/src/Primer/Questions.hs @@ -10,6 +10,7 @@ module Primer.Questions ( ShadowedVarsTy (..), -- only exported for testing generateNameExpr, generateNameTy, + generateNameTyAvoiding, uniquify, ) where @@ -100,9 +101,18 @@ generateNameTy :: Either (Maybe (Type' ())) (Maybe Kind) -> TypeZip -> m [Name] +generateNameTy = generateNameTyAvoiding [] + +generateNameTyAvoiding :: + MonadReader Cxt m => + [Name] -> + Either (Maybe (Type' ())) (Maybe Kind) -> + TypeZip -> + m [Name] -- It doesn't really make sense to ask for a term variable (Left) here, but -- it doesn't harm to support it -generateNameTy tk z = uniquifyMany <$> mkAvoidForFreshNameTy z <*> baseNames tk +generateNameTyAvoiding avoiding tk z = + uniquifyMany <$> ((Set.fromList avoiding <>) <$> mkAvoidForFreshNameTy z) <*> baseNames tk baseNames :: MonadReader Cxt m => diff --git a/primer/src/Primer/TypeDef.hs b/primer/src/Primer/TypeDef.hs index 8c8c123eb..d1c04fb8f 100644 --- a/primer/src/Primer/TypeDef.hs +++ b/primer/src/Primer/TypeDef.hs @@ -54,7 +54,7 @@ type TypeDefMap = Map TyConName (TypeDef ()) -- | Definition of a primitive data type data PrimTypeDef = PrimTypeDef - { primTypeDefParameters :: [Kind] + { primTypeDefParameters :: [(TyVarName, Kind)] , primTypeDefNameHints :: [Name] } deriving stock (Eq, Show, Read, Data, Generic) @@ -94,16 +94,16 @@ typeDefNameHints :: TypeDef b -> [Name] typeDefNameHints = \case TypeDefPrim t -> primTypeDefNameHints t TypeDefAST t -> astTypeDefNameHints t -typeDefParameters :: TypeDef b -> [Kind] +typeDefParameters :: TypeDef b -> [(TyVarName, Kind)] typeDefParameters = \case TypeDefPrim t -> primTypeDefParameters t - TypeDefAST t -> snd <$> astTypeDefParameters t + TypeDefAST t -> astTypeDefParameters t typeDefAST :: TypeDef b -> Maybe (ASTTypeDef b) typeDefAST = \case TypeDefPrim _ -> Nothing TypeDefAST t -> Just t typeDefKind :: TypeDef b -> Kind -typeDefKind = foldr KFun KType . typeDefParameters +typeDefKind = foldr (KFun . snd) KType . typeDefParameters -- | A traversal over the contstructor fields in an typedef. _typedefFields :: Traversal (TypeDef b) (TypeDef c) (Type' b) (Type' c) diff --git a/primer/src/Primer/Typecheck/Cxt.hs b/primer/src/Primer/Typecheck/Cxt.hs index 6ffd63c4d..ada1aad01 100644 --- a/primer/src/Primer/Typecheck/Cxt.hs +++ b/primer/src/Primer/Typecheck/Cxt.hs @@ -28,4 +28,4 @@ data Cxt = Cxt , globalCxt :: Map GVarName Type -- ^ global variables (i.e. IDs of top-level definitions) } - deriving stock (Show) + deriving stock (Show, Generic) diff --git a/primer/src/Primer/Zipper.hs b/primer/src/Primer/Zipper.hs index 5dad673eb..55d98d0e7 100644 --- a/primer/src/Primer/Zipper.hs +++ b/primer/src/Primer/Zipper.hs @@ -448,7 +448,7 @@ findNodeWithParent id x = do InBind (BindCase bz) -> (CaseBindNode $ caseBindZFocus bz, Just . ExprNode . target . unfocusCaseBind $ bz) -- | Find a sub-type in a larger type by its ID. -findType :: ID -> Type -> Maybe Type +findType :: (Data a, HasID a) => ID -> Type' a -> Maybe (Type' a) findType id ty = target <$> focusOnTy id ty -- | An AST node tagged with its "sort" - i.e. if it's a type or expression or binding etc. diff --git a/primer/test/Tests/Action/Available.hs b/primer/test/Tests/Action/Available.hs index 1b47a3a1c..378c72ea5 100644 --- a/primer/test/Tests/Action/Available.hs +++ b/primer/test/Tests/Action/Available.hs @@ -46,11 +46,14 @@ import Primer.Action.Available ( import Primer.Action.Available qualified as Available import Primer.App ( App, + DefSelection (..), EditAppM, Editable (..), Level (Beginner, Expert, Intermediate), + NodeSelection (..), NodeType (..), ProgError (ActionError, DefAlreadyExists), + Selection' (..), appProg, checkAppWellFormed, checkProgWellFormed, @@ -187,16 +190,16 @@ mkTests deps (defName, DefAST def') = Available.Input a -> Input a . fromMaybe (error "id not found") - $ Available.options typeDefs defs cxt level def id a + $ Available.options typeDefs defs cxt level (Right def) id a in testGroup testName $ enumeratePairs <&> \(level, mut) -> - let defActions = map (offered level Nothing) $ Available.forDef defs level mut d + let defActions = map (offered level $ SelectionDef $ DefSelection defName Nothing) $ Available.forDef defs level mut d bodyActions = map ( \id -> ( id - , map (offered level (Just (BodyNode, id))) $ + , map (offered level $ SelectionDef $ DefSelection defName $ Just $ NodeSelection BodyNode id) $ Available.forBody typeDefs level @@ -211,7 +214,7 @@ mkTests deps (defName, DefAST def') = map ( \id -> ( id - , map (offered level (Just (SigNode, id))) $ Available.forSig level mut (astDefType def) id + , map (offered level (SelectionDef $ DefSelection defName $ Just $ NodeSelection SigNode id)) $ Available.forSig level mut (astDefType def) id ) ) . toListOf (_typeMeta % _id) @@ -275,7 +278,7 @@ tasty_available_actions_accepted = withTests 500 $ DefAST{} -> label "AST" DefPrim{} -> label "Prim" (loc, acts) <- - fmap snd . forAllWithT fst $ + fmap (first (SelectionDef . DefSelection defName) . snd) . forAllWithT fst $ Gen.frequency $ catMaybes [ Just (1, pure ("actionsForDef", (Nothing, Available.forDef (snd <$> allDefs) l defMut defName))) @@ -284,13 +287,13 @@ tasty_available_actions_accepted = withTests 500 $ ids = ty ^.. typeIDs i <- Gen.element ids let hedgehogMsg = "actionsForDefSig id " <> show i - pure (hedgehogMsg, (Just (SigNode, i), Available.forSig l defMut ty i)) + pure (hedgehogMsg, (Just $ NodeSelection SigNode i, Available.forSig l defMut ty i)) , defAST def <&> \d' -> (7,) $ do let expr = astDefExpr d' ids = expr ^.. exprIDs i <- Gen.element ids let hedgehogMsg = "actionsForDefBody id " <> show i - pure (hedgehogMsg, (Just (BodyNode, i), Available.forBody (snd <$> progAllTypeDefs (appProg a)) l defMut expr i)) + pure (hedgehogMsg, (Just $ NodeSelection BodyNode i, Available.forBody (snd <$> progAllTypeDefs (appProg a)) l defMut expr i)) ] case acts of [] -> label "no offered actions" >> success @@ -302,7 +305,7 @@ tasty_available_actions_accepted = withTests 500 $ def' <- maybe (annotate "primitive def" >> failure) pure $ defAST def progActs <- either (\e -> annotateShow e >> failure) pure $ - toProgActionNoInput (map snd $ progAllDefs $ appProg a) def' defName loc act' + toProgActionNoInput (map snd $ progAllDefs $ appProg a) (Right def') loc act' actionSucceeds (handleEditRequest progActs) a Available.Input act' -> do def' <- maybe (annotate "primitive def" >> failure) pure $ defAST def @@ -313,7 +316,7 @@ tasty_available_actions_accepted = withTests 500 $ (map snd $ progAllDefs $ appProg a) (progCxt $ appProg a) l - def' + (Right def') loc act' let opts' = [Gen.element $ (Offered,) <$> opts | not (null opts)] @@ -327,7 +330,7 @@ tasty_available_actions_accepted = withTests 500 $ [] -> annotate "no options" >> success options -> do opt <- forAllT $ Gen.choice options - progActs <- either (\e -> annotateShow e >> failure) pure $ toProgActionInput def' defName loc (snd opt) act' + progActs <- either (\e -> annotateShow e >> failure) pure $ toProgActionInput (Right def') loc (snd opt) act' actionSucceedsOrCapture (fst opt) (handleEditRequest progActs) a where runEditAppMLogs :: @@ -595,12 +598,12 @@ offeredActionTest' sh l inputDef position action = do let offered = case position of InBody _ -> Available.forBody cxt.typeDefs l Editable expr id InSig _ -> Available.forSig l Editable sig id - let options = Available.options cxt.typeDefs defs cxt l exprDef (Just (BodyNode, id)) + let options = Available.options cxt.typeDefs defs cxt l (Right exprDef) (SelectionDef $ DefSelection exprDefName $ Just $ NodeSelection BodyNode id) action' <- case action of Left a -> pure $ if Available.NoInput a `elem` offered - then Right $ toProgActionNoInput (foldMap' moduleDefsQualified $ progModules prog) exprDef exprDefName (Just (BodyNode, id)) a + then Right $ toProgActionNoInput (foldMap' moduleDefsQualified $ progModules prog) (Right exprDef) (SelectionDef $ DefSelection exprDefName $ Just $ NodeSelection BodyNode id) a else Left $ ActionNotOffered (Available.NoInput a) offered Right (a, o) -> do if Available.Input a `elem` offered @@ -609,7 +612,7 @@ offeredActionTest' sh l inputDef position action = do Just os -> pure $ if o `elem` os.opts - then Right $ toProgActionInput exprDef exprDefName (Just (BodyNode, id)) o a + then Right $ toProgActionInput (Right exprDef) (SelectionDef $ DefSelection exprDefName $ Just $ NodeSelection BodyNode id) o a else Left $ OptionNotOffered o os.opts else pure $ Left $ ActionNotOffered (Available.Input a) offered action'' <- for action' $ \case diff --git a/primer/test/Tests/Action/Prog.hs b/primer/test/Tests/Action/Prog.hs index 2a126dfbf..75825c328 100644 --- a/primer/test/Tests/Action/Prog.hs +++ b/primer/test/Tests/Action/Prog.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedRecordDot #-} module Tests.Action.Prog where @@ -35,6 +36,7 @@ import Primer.Action ( ) import Primer.App ( App, + DefSelection (..), Log (..), NodeSelection (..), NodeType (..), @@ -42,7 +44,7 @@ import Primer.App ( ProgAction (..), ProgError (..), Question (GenerateName, VariablesInScope), - Selection (..), + Selection' (..), appIdCounter, appNameCounter, appProg, @@ -95,7 +97,6 @@ import Primer.Core ( import Primer.Core.DSL ( S, ann, - app, branch, case_, con, @@ -115,7 +116,7 @@ import Primer.Core.DSL ( tfun, tvar, ) -import Primer.Core.Utils (forgetMetadata, forgetTypeMetadata) +import Primer.Core.Utils (forgetMetadata) import Primer.Def (ASTDef (..), Def (..), DefMap, defAST) import Primer.Log (PureLogT, runPureLogT) import Primer.Module (Module (Module, moduleDefs, moduleName, moduleTypes), builtinModule, moduleDefsQualified, moduleTypesQualified, primitiveModule) @@ -160,7 +161,7 @@ unit_move_to_def_main = progActionTest defaultEmptyProg [moveToDef "main"] $ prog' @?= prog { progLog = Log [[moveToDef "main"]] - , progSelection = Just $ Selection (gvn "main") Nothing + , progSelection = Just $ SelectionDef $ DefSelection (gvn "main") Nothing } -- Expression actions are tested in ActionTest - here we just check that we can modify the correct @@ -502,7 +503,7 @@ unit_construct_arrow_in_sig = TFun _ lhs _ -> -- Check that the selection is focused on the lhs, as we instructed case progSelection prog' of - Just (Selection d (Just sel@NodeSelection{nodeType = SigNode})) -> do + Just (SelectionDef (DefSelection d (Just sel@NodeSelection{nodeType = SigNode}))) -> do d @?= qualifyName mainModuleName "other" getID sel @?= getID lhs _ -> assertFailure "no selection" @@ -1019,193 +1020,6 @@ unit_AddCon = ] ) --- change the type of a field which currently wraps a constructor (which is checkable) -unit_SetConFieldType_con :: Assertion -unit_SetConFieldType_con = - progActionTest - ( defaultProgEditableTypeDefs . sequence . pure $ do - x <- - con - cA - [ con0 (vcn "True") - , con0 (vcn "True") - , con0 (vcn "True") - ] - astDef "def" x <$> tEmptyHole - ) - [SetConFieldType tT cA 1 $ TCon () (tcn "Int")] - $ expectSuccess - $ \_ prog' -> do - td <- findTypeDef tT prog' - astTypeDefConstructors td - @?= [ ValCon cA [TCon () (tcn "Bool"), TCon () (tcn "Int"), TCon () (tcn "Bool")] - , ValCon cB [TApp () (TApp () (TCon () tT) (TVar () "b")) (TVar () "a"), TVar () "b"] - ] - def <- findDef (gvn "def") prog' - forgetMetadata (astDefExpr def) - @?= forgetMetadata - ( create' $ - con - cA - [ con0 (vcn "True") - , hole (con0 (vcn "True")) - , con0 (vcn "True") - ] - ) - --- Change the type of one field from ty1 to ty2, and test what happens to that subterm --- We use that @T u v ∋ B emptyHole t@ iff @v ∋ t@. -setConFieldTypeHelper :: S Type -> S Expr -> S Type -> S Expr -> Assertion -setConFieldTypeHelper ty1 tmInput ty2' tmExpected = - let ty2 = forgetTypeMetadata $ create' ty2' - in progActionTest - ( defaultProgEditableTypeDefs . sequence . pure $ do - x <- - con cB [emptyHole, tmInput] - astDef "def" x <$> ((tcon tT `tapp` tEmptyHole) `tapp` ty1) - ) - [SetConFieldType tT cB 1 ty2] - $ expectSuccess - $ \_ prog' -> do - td <- findTypeDef tT prog' - astTypeDefConstructors td - @?= [ ValCon cA [TCon () (tcn "Bool"), TCon () (tcn "Bool"), TCon () (tcn "Bool")] - , ValCon cB [TApp () (TApp () (TCon () tT) (TVar () "b")) (TVar () "a"), ty2] - ] - def <- findDef (gvn "def") prog' - forgetMetadata (astDefExpr def) - @?= forgetMetadata - ( create' $ - con cB [emptyHole, tmExpected] - ) - --- change the type of a field which currently wraps a checkable term -unit_SetConFieldType_chk :: Assertion -unit_SetConFieldType_chk = - setConFieldTypeHelper - (tcon (tcn "Nat") `tfun` tcon (tcn "Bool")) - (lam "x" emptyHole) - (tcon (tcn "Int")) - (hole $ lam "x" emptyHole) - --- change the type of a field which currently wraps a checkable term --- this result could have the hole elided, but we don't run smartholes --- so we can't tell -unit_SetConFieldType_match :: Assertion -unit_SetConFieldType_match = - setConFieldTypeHelper - (tEmptyHole `tfun` tcon (tcn "Bool")) - (lam "x" emptyHole) - (tcon (tcn "Int") `tfun` tEmptyHole) - (hole $ lam "x" emptyHole) - --- change the type of a field which currently wraps a synthesisable argument -unit_SetConFieldType_syn :: Assertion -unit_SetConFieldType_syn = - setConFieldTypeHelper - (tcon $ tcn "Int") - (emptyHole `app` emptyHole) - (tcon tBool) - (hole $ emptyHole `app` emptyHole) - --- change the type of a field which currently wraps an emptyHole argument -unit_SetConFieldType_emptyHole :: Assertion -unit_SetConFieldType_emptyHole = - setConFieldTypeHelper - (tcon $ tcn "Int") - emptyHole - (tcon $ tcn "Bool") - emptyHole - --- change the type of a field which currently wraps a non-empty hole argument -unit_SetConFieldType_nehole :: Assertion -unit_SetConFieldType_nehole = do - setConFieldTypeHelper - (tcon $ tcn "Bool") - (hole $ (lam "x" (lvar "x") `ann` (tcon (tcn "Bool") `tfun` tcon (tcn "Bool"))) `app` con0 (vcn "True")) - (tcon (tcn "tBool") `tfun` tcon (tcn "Bool")) - (hole $ (lam "x" (lvar "x") `ann` (tcon (tcn "Bool") `tfun` tcon (tcn "Bool"))) `app` con0 (vcn "True")) - --- change the type of a field which currently wraps a non-empty hole argument, --- where the result could have a hole elided, but we don't run smartholes --- so we can't tell -unit_SetConFieldType_nehole_2 :: Assertion -unit_SetConFieldType_nehole_2 = - setConFieldTypeHelper - (tcon $ tcn "Int") - (hole $ (lam "x" (lvar "x") `ann` (tcon (tcn "Bool") `tfun` tcon (tcn "Bool"))) `app` con0 (vcn "True")) - (tcon $ tcn "Bool") - (hole $ (lam "x" (lvar "x") `ann` (tcon (tcn "Bool") `tfun` tcon (tcn "Bool"))) `app` con0 (vcn "True")) - -unit_SetConFieldType_case :: Assertion -unit_SetConFieldType_case = - progActionTest - ( defaultProgEditableTypeDefs $ do - x <- - case_ - (emptyHole `ann` (tcon tT `tapp` tEmptyHole `tapp` tEmptyHole)) - [ branch - cA - [("x", Nothing), ("y", Nothing), ("z", Nothing)] - (lvar "y") - , branch cB [("s", Nothing), ("t", Nothing)] emptyHole - ] - sequence - [ astDef "def" x <$> tcon (tcn "Bool") - ] - ) - [SetConFieldType tT cA 1 $ TCon () (tcn "Int")] - $ expectSuccess - $ \_ prog' -> do - def <- findDef (gvn "def") prog' - forgetMetadata (astDefExpr def) - @?= forgetMetadata - ( create' $ - case_ - (emptyHole `ann` (tcon tT `tapp` tEmptyHole `tapp` tEmptyHole)) - [ branch - cA - [("x", Nothing), ("y", Nothing), ("z", Nothing)] - (hole $ lvar "y") - , branch cB [("s", Nothing), ("t", Nothing)] emptyHole - ] - ) - -unit_SetConFieldType_shadow :: Assertion -unit_SetConFieldType_shadow = - progActionTest - ( defaultProgEditableTypeDefs $ do - x <- - case_ - (emptyHole `ann` (tcon tT `tapp` tEmptyHole `tapp` tEmptyHole)) - [ branch - cA - [("x", Nothing), ("y", Nothing), ("z", Nothing)] - ((lam "y" (lvar "y") `ann` (tcon (tcn "Bool") `tfun` tcon (tcn "Bool"))) `app` lvar "y") - , branch cB [("s", Nothing), ("t", Nothing)] emptyHole - ] - sequence - [ astDef "def" x <$> tcon (tcn "Bool") - ] - ) - [SetConFieldType tT cA 1 $ TCon () (tcn "Int")] - $ expectSuccess - $ \_ prog' -> do - def <- findDef (gvn "def") prog' - forgetMetadata (astDefExpr def) - @?= forgetMetadata - ( create' $ - case_ - (emptyHole `ann` (tcon tT `tapp` tEmptyHole `tapp` tEmptyHole)) - [ branch - cA - [("x", Nothing), ("y", Nothing), ("z", Nothing)] - -- only the free `y` should be put in to a hole - ((lam "y" (lvar "y") `ann` (tcon (tcn "Bool") `tfun` tcon (tcn "Bool"))) `app` hole (lvar "y")) - , branch cB [("s", Nothing), ("t", Nothing)] emptyHole - ] - ) - unit_AddConField :: Assertion unit_AddConField = progActionTest @@ -1288,6 +1102,40 @@ unit_AddConField_case_ann = ] ) +unit_ConFieldAction :: Assertion +unit_ConFieldAction = + progActionTest + ( defaultProgEditableTypeDefs $ do + e <- con cA $ replicate 3 $ con0 $ vcn "True" + t <- tEmptyHole + pure [astDef "def" e t] + ) + [ConFieldAction tT cA 1 [ConstructArrowL]] + $ expectSuccess + $ \_ prog' -> do + td <- findTypeDef tT prog' + def <- findDef (gvn "def") prog' + astTypeDefConstructors td + @?= [ ValCon + cA + [ TCon () (tcn "Bool") + , TFun () (TCon () (tcn "Bool")) (TEmptyHole ()) + , TCon () (tcn "Bool") + ] + , ValCon cB [TApp () (TApp () (TCon () tT) (TVar () "b")) (TVar () "a"), TVar () "b"] + ] + forgetMetadata (astDefExpr def) + @?= forgetMetadata + ( create' $ + do + con + cA + [ con0 $ vcn "True" + , hole $ con0 $ vcn "True" + , con0 $ vcn "True" + ] + ) + -- Check that we see name hints from imported modules -- (This differs from the tests in Tests.Question by testing the actual action, -- rather than the underlying functionality) @@ -1326,7 +1174,11 @@ unit_rename_module = Left err -> assertFailure $ show err Right p -> do fmap (unModuleName . moduleName) (progModules p) @?= [["Module2"]] - selectedDef <$> progSelection p @?= Just (qualifyName (ModuleName ["Module2"]) "main") + sel <- case progSelection p of + Just (SelectionDef s) -> pure s + Just (SelectionTypeDef _) -> assertFailure "typedef selected" + Nothing -> assertFailure "no selection" + sel.def @?= qualifyName (ModuleName ["Module2"]) "main" case fmap (Map.assocs . moduleDefsQualified) (progModules p) of [[(n, DefAST d)]] -> do let expectedName = qualifyName (ModuleName ["Module2"]) "main" @@ -1402,7 +1254,6 @@ unit_cross_module_actions = handleAndTC [RenameType (qualifyM "T") "R"] handleAndTC [RenameCon (qualifyM "R") (qualifyM "C") "D"] handleAndTC [AddCon (qualifyM "R") 1 "X"] - handleAndTC [SetConFieldType (qualifyM "R") (qualifyM "D") 0 (TCon () tBool)] handleAndTC [AddConField (qualifyM "R") (qualifyM "D") 0 (TCon () tNat)] handleAndTC [RenameModule (moduleName m) ["AnotherModule"]] -- NB: SigAction relies on SmartHoles to fix any introduced inconsistencies @@ -1509,7 +1360,7 @@ unit_sh_lost_id = Just def -> case astDefExpr <$> defAST def of Just (Var m (GlobalVarRef f)) | f == foo -> case progSelection prog' of - Just Selection{selectedDef, selectedNode = Just sel} -> + Just (SelectionDef DefSelection{def = selectedDef, node = Just sel}) -> unless (selectedDef == foo && getID sel == getID m) $ assertFailure "expected selection to point at the recursive reference" _ -> assertFailure "expected the selection to point at some node" @@ -1547,8 +1398,8 @@ defaultEmptyProg = do in pure $ newEmptyProg' { progSelection = - Just $ - Selection (gvn "main") $ + Just . SelectionDef $ + DefSelection (gvn "main") $ Just NodeSelection { nodeType = BodyNode diff --git a/primer/test/Tests/Serialization.hs b/primer/test/Tests/Serialization.hs index 3594a1f08..24186a519 100644 --- a/primer/test/Tests/Serialization.hs +++ b/primer/test/Tests/Serialization.hs @@ -13,6 +13,7 @@ import Data.Map.Strict qualified as Map import Data.String (String) import Primer.Action (Action (Move, SetCursor), ActionError (IDNotFound), Movement (Child1)) import Primer.App ( + DefSelection (..), EvalResp (EvalResp, evalRespDetail, evalRespExpr, evalRespRedexes), Log (..), NodeSelection (..), @@ -20,7 +21,8 @@ import Primer.App ( Prog (..), ProgAction (BodyAction, MoveToDef), ProgError (NoDefSelected), - Selection (..), + Selection, + Selection' (..), defaultLog, ) import Primer.Builtins (tNat) @@ -151,12 +153,13 @@ fixtures = } selection :: Selection selection = - Selection (qualifyName modName defName) $ - Just - NodeSelection - { nodeType = BodyNode - , meta = Left exprMeta - } + SelectionDef $ + DefSelection (qualifyName modName defName) $ + Just + NodeSelection + { nodeType = BodyNode + , meta = Left exprMeta + } reductionDetail :: EvalDetail reductionDetail = BetaReduction $ diff --git a/primer/test/Tests/Typecheck.hs b/primer/test/Tests/Typecheck.hs index 58f4d8ec2..d5cfe39e9 100644 --- a/primer/test/Tests/Typecheck.hs +++ b/primer/test/Tests/Typecheck.hs @@ -18,6 +18,9 @@ import Primer.App ( progSmartHoles, redoLog ), + TypeDefConsFieldSelection (..), + TypeDefConsSelection (..), + TypeDefSelection (..), appIdCounter, appInit, appNameCounter, @@ -30,6 +33,7 @@ import Primer.App ( tcWholeProgWithImports, ) import Primer.App qualified as App +import Primer.App.Base (TypeDefNodeSelection (..)) import Primer.Builtins ( boolDef, cCons, @@ -782,6 +786,10 @@ tcaFunctorial :: (Functor f, Eq (f (TypeCacheAlpha a))) => TypeCacheAlpha (f a) tcaFunctorial = (==) `on` (fmap TypeCacheAlpha . unTypeCacheAlpha) instance Eq (TypeCacheAlpha a) => Eq (TypeCacheAlpha (Maybe a)) where (==) = tcaFunctorial +instance (Eq (TypeCacheAlpha a), Eq (TypeCacheAlpha b)) => Eq (TypeCacheAlpha (Either a b)) where + TypeCacheAlpha (Left a1) == TypeCacheAlpha (Left a2) = TypeCacheAlpha a1 == TypeCacheAlpha a2 + TypeCacheAlpha (Right b1) == TypeCacheAlpha (Right b2) = TypeCacheAlpha b1 == TypeCacheAlpha b2 + _ == _ = False instance (Eq (TypeCacheAlpha a), Eq b) => Eq (TypeCacheAlpha (Expr' (Meta a) b)) where (==) = (==) `on` (((_exprMeta % _type) %~ TypeCacheAlpha) . unTypeCacheAlpha) instance Eq (TypeCacheAlpha Def) where @@ -799,12 +807,22 @@ instance Eq (TypeCacheAlpha [Module]) where (==) = tcaFunctorial instance Eq (TypeCacheAlpha ExprMeta) where (==) = tcaFunctorial -instance Eq (TypeCacheAlpha App.NodeSelection) where +instance Eq (TypeCacheAlpha TypeMeta) where + (==) = tcaFunctorial +instance Eq (TypeCacheAlpha Kind) where + TypeCacheAlpha k1 == TypeCacheAlpha k2 = k1 == k2 +instance Eq (TypeCacheAlpha (App.NodeSelection (Either ExprMeta TypeMeta))) where TypeCacheAlpha (App.NodeSelection t1 m1) == TypeCacheAlpha (App.NodeSelection t2 m2) = t1 == t2 && ((==) `on` first TypeCacheAlpha) m1 m2 instance Eq (TypeCacheAlpha App.Selection) where - TypeCacheAlpha (App.Selection d1 n1) == TypeCacheAlpha (App.Selection d2 n2) = + TypeCacheAlpha (App.SelectionDef (App.DefSelection d1 n1)) == TypeCacheAlpha (App.SelectionDef (App.DefSelection d2 n2)) = d1 == d2 && TypeCacheAlpha n1 == TypeCacheAlpha n2 + TypeCacheAlpha (App.SelectionTypeDef (TypeDefSelection a1 (Just (TypeDefConsNodeSelection (TypeDefConsSelection n1 (Just (TypeDefConsFieldSelection b1 m1))))))) + == TypeCacheAlpha (App.SelectionTypeDef (TypeDefSelection a2 (Just (TypeDefConsNodeSelection (TypeDefConsSelection n2 (Just (TypeDefConsFieldSelection b2 m2))))))) = + a1 == a2 && b1 == b2 && n1 == n2 && TypeCacheAlpha m1 == TypeCacheAlpha m2 + TypeCacheAlpha (App.SelectionTypeDef (TypeDefSelection n1 s1)) == TypeCacheAlpha (App.SelectionTypeDef (TypeDefSelection n2 s2)) = + n1 == n2 && s1 == s2 + _ == _ = False instance Eq (TypeCacheAlpha Prog) where TypeCacheAlpha (Prog i1 m1 s1 sh1 l1 r1) == TypeCacheAlpha (Prog i2 m2 s2 sh2 l2 r2) = TypeCacheAlpha i1 == TypeCacheAlpha i2 diff --git a/primer/test/outputs/serialization/edit_response_2.json b/primer/test/outputs/serialization/edit_response_2.json index 5fd634d7a..ebdaa70a6 100644 --- a/primer/test/outputs/serialization/edit_response_2.json +++ b/primer/test/outputs/serialization/edit_response_2.json @@ -147,28 +147,31 @@ } ], "progSelection": { - "selectedDef": { - "baseName": "main", - "qualifiedModule": [ - "M" - ] - }, - "selectedNode": { - "meta": { - "Left": [ - 0, - { - "contents": { - "contents": [], - "tag": "TEmptyHole" - }, - "tag": "TCSynthed" - }, - null + "contents": { + "def": { + "baseName": "main", + "qualifiedModule": [ + "M" ] }, - "nodeType": "BodyNode" - } + "node": { + "meta": { + "Left": [ + 0, + { + "contents": { + "contents": [], + "tag": "TEmptyHole" + }, + "tag": "TCSynthed" + }, + null + ] + }, + "nodeType": "BodyNode" + } + }, + "tag": "SelectionDef" }, "progSmartHoles": "SmartHoles", "redoLog": { diff --git a/primer/test/outputs/serialization/prog.json b/primer/test/outputs/serialization/prog.json index 02b6dc308..fcfeb7ab3 100644 --- a/primer/test/outputs/serialization/prog.json +++ b/primer/test/outputs/serialization/prog.json @@ -146,28 +146,31 @@ } ], "progSelection": { - "selectedDef": { - "baseName": "main", - "qualifiedModule": [ - "M" - ] - }, - "selectedNode": { - "meta": { - "Left": [ - 0, - { - "contents": { - "contents": [], - "tag": "TEmptyHole" - }, - "tag": "TCSynthed" - }, - null + "contents": { + "def": { + "baseName": "main", + "qualifiedModule": [ + "M" ] }, - "nodeType": "BodyNode" - } + "node": { + "meta": { + "Left": [ + 0, + { + "contents": { + "contents": [], + "tag": "TEmptyHole" + }, + "tag": "TCSynthed" + }, + null + ] + }, + "nodeType": "BodyNode" + } + }, + "tag": "SelectionDef" }, "progSmartHoles": "SmartHoles", "redoLog": { diff --git a/primer/test/outputs/serialization/selection.json b/primer/test/outputs/serialization/selection.json index 0ac964006..3f33224c0 100644 --- a/primer/test/outputs/serialization/selection.json +++ b/primer/test/outputs/serialization/selection.json @@ -1,24 +1,27 @@ { - "selectedDef": { - "baseName": "main", - "qualifiedModule": [ - "M" - ] - }, - "selectedNode": { - "meta": { - "Left": [ - 0, - { - "contents": { - "contents": [], - "tag": "TEmptyHole" - }, - "tag": "TCSynthed" - }, - null + "contents": { + "def": { + "baseName": "main", + "qualifiedModule": [ + "M" ] }, - "nodeType": "BodyNode" - } + "node": { + "meta": { + "Left": [ + 0, + { + "contents": { + "contents": [], + "tag": "TEmptyHole" + }, + "tag": "TCSynthed" + }, + null + ] + }, + "nodeType": "BodyNode" + } + }, + "tag": "SelectionDef" } \ No newline at end of file