From 9078305a8a496f60fce8dec525c62dd7402d15c4 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 17 Apr 2023 23:19:00 +0100 Subject: [PATCH 01/16] fix: Avoid long IDs for nodes in type of primitive def Signed-off-by: George Thomas --- primer/src/Primer/API.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index 76f968907..19521eb12 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -681,7 +681,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) From b127690c783f18626483d0944fad23ddaefb6023 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 17 Apr 2023 13:40:56 +0100 Subject: [PATCH 02/16] refactor: Move selection types to `App.Base` This will be necessary in order to make use of them in actions-related modules. Signed-off-by: George Thomas --- primer/src/Primer/App.hs | 36 ++------------------------------ primer/src/Primer/App/Base.hs | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/primer/src/Primer/App.hs b/primer/src/Primer/App.hs index f758670d5..8b5a3b865 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -48,8 +48,6 @@ module Primer.App ( handleEvalFullRequest, importModules, MutationRequest (..), - Selection (..), - NodeSelection (..), EvalReq (..), EvalResp (..), EvalFullReq (..), @@ -63,7 +61,6 @@ 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, @@ -78,10 +75,8 @@ import Optics ( Field3 (_3), ReversibleOptic (re), ifoldMap, - lens, mapped, over, - set, traverseOf, traversed, view, @@ -107,7 +102,9 @@ import Primer.Action.ProgError (ProgError (..)) import Primer.App.Base ( Editable (..), Level (..), + NodeSelection (..), NodeType (..), + Selection (..), ) import Primer.Core ( Bind' (Bind), @@ -115,10 +112,8 @@ import Primer.Core ( CaseBranch' (CaseBranch), Expr, Expr' (Case, Con, EmptyHole, Hole, Var), - ExprMeta, GVarName, GlobalName (baseName, qualifiedModule), - HasID (_id), ID (..), LocalName (LocalName, unLocalName), Meta (..), @@ -415,33 +410,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 diff --git a/primer/src/Primer/App/Base.hs b/primer/src/Primer/App/Base.hs index 6f4f834f5..aaae6d3d5 100644 --- a/primer/src/Primer/App/Base.hs +++ b/primer/src/Primer/App/Base.hs @@ -1,14 +1,26 @@ +{-# LANGUAGE OverloadedLabels #-} + -- | 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 (..), + NodeSelection (..), ) where import Protolude import Data.Data (Data) +import Optics +import Primer.Core ( + ExprMeta, + GVarName, + HasID (..), + TypeMeta, + getID, + ) import Primer.JSON ( CustomJSON (CustomJSON), FromJSON, @@ -38,3 +50,30 @@ data NodeType = BodyNode | SigNode deriving stock (Eq, Show, Read, Bounded, Enum, Generic, Data) deriving (FromJSON, ToJSON) via PrimerJSON NodeType deriving anyclass (NFData) + +-- | 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)) From 2a8f64570c32f6da6b33d477ccc05fc677be47dc Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 17 Apr 2023 14:46:16 +0100 Subject: [PATCH 03/16] refactor: Parameterise selection types We parameterise this way, rather than parameterising `ExprMeta` and `TypeMeta` separately, since we otherwise wouldn't be able to reconstruct even a basic `Selection' ID` from the OpenAPI API, without clients knowing whether a particular ID corresponds to a type or term. We want clients to be able to be dumber than that. Note that, for `Selection`, we use a synonym for the non-parameterised version, but not for `NodeSelection`, and we will not in future for `DefSelection` and `TypeDefSelection`. That's because this synonym is actually widely useful, and we use it in several modules to make things more readable. With `NodeSelection` etc. we never really require such a synonym, and we avoid it because of all the ceremony it would add, particularly around imports. Signed-off-by: George Thomas --- primer/src/Primer/API.hs | 4 +++- primer/src/Primer/App.hs | 3 ++- primer/src/Primer/App/Base.hs | 28 ++++++++++++++-------------- primer/src/Primer/Core/Meta.hs | 6 ++++++ primer/test/Tests/Action/Prog.hs | 2 +- primer/test/Tests/Serialization.hs | 3 ++- primer/test/Tests/Typecheck.hs | 2 +- 7 files changed, 29 insertions(+), 19 deletions(-) diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index 19521eb12..9524c5e73 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -139,6 +139,7 @@ import Primer.Core ( CaseBranch' (..), Expr, Expr' (..), + ExprMeta, GVarName, GlobalName (..), HasID (..), @@ -152,6 +153,7 @@ import Primer.Core ( TyVarName, Type, Type' (..), + TypeMeta, ValConName, getID, unLocalName, @@ -1169,5 +1171,5 @@ data NodeSelection = NodeSelection deriving (FromJSON, ToJSON) via PrimerJSON NodeSelection deriving anyclass (NFData) -viewNodeSelection :: App.NodeSelection -> NodeSelection +viewNodeSelection :: App.NodeSelection (Either ExprMeta TypeMeta) -> NodeSelection viewNodeSelection sel@App.NodeSelection{nodeType} = NodeSelection{nodeType, id = getID sel} diff --git a/primer/src/Primer/App.hs b/primer/src/Primer/App.hs index 8b5a3b865..fd4fcd153 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -104,7 +104,8 @@ import Primer.App.Base ( Level (..), NodeSelection (..), NodeType (..), - Selection (..), + Selection, + Selection' (..), ) import Primer.Core ( Bind' (Bind), diff --git a/primer/src/Primer/App/Base.hs b/primer/src/Primer/App/Base.hs index aaae6d3d5..b82c937db 100644 --- a/primer/src/Primer/App/Base.hs +++ b/primer/src/Primer/App/Base.hs @@ -6,7 +6,8 @@ module Primer.App.Base ( Level (..), Editable (..), NodeType (..), - Selection (..), + Selection, + Selection' (..), NodeSelection (..), ) where @@ -53,27 +54,26 @@ data NodeType = BodyNode | SigNode -- | 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 +type Selection = Selection' (Either ExprMeta TypeMeta) + +data Selection' a = Selection { selectedDef :: GVarName -- ^ the ID of some ASTDef - , selectedNode :: Maybe NodeSelection + , selectedNode :: Maybe (NodeSelection a) } - deriving stock (Eq, Show, Read, Generic, Data) - deriving (FromJSON, ToJSON) via PrimerJSON Selection + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (Selection' a) 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 +data NodeSelection a = NodeSelection { nodeType :: NodeType - , meta :: Either ExprMeta TypeMeta + , meta :: a } - deriving stock (Eq, Show, Read, Generic, Data) - deriving (FromJSON, ToJSON) via PrimerJSON NodeSelection + deriving stock (Eq, Show, Read, Functor, Generic, Data) + deriving (FromJSON, ToJSON) via PrimerJSON (NodeSelection a) deriving anyclass (NFData) -instance HasID NodeSelection where - _id = - lens - (either getID getID . meta) - (flip $ \id -> over #meta $ bimap (set _id id) (set _id id)) +instance HasID a => HasID (NodeSelection a) where + _id = lens (getID . meta) (flip $ over #meta . set _id) 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/test/Tests/Action/Prog.hs b/primer/test/Tests/Action/Prog.hs index 2a126dfbf..4a9b54447 100644 --- a/primer/test/Tests/Action/Prog.hs +++ b/primer/test/Tests/Action/Prog.hs @@ -42,7 +42,7 @@ import Primer.App ( ProgAction (..), ProgError (..), Question (GenerateName, VariablesInScope), - Selection (..), + Selection' (..), appIdCounter, appNameCounter, appProg, diff --git a/primer/test/Tests/Serialization.hs b/primer/test/Tests/Serialization.hs index 3594a1f08..47f2bc4c6 100644 --- a/primer/test/Tests/Serialization.hs +++ b/primer/test/Tests/Serialization.hs @@ -20,7 +20,8 @@ import Primer.App ( Prog (..), ProgAction (BodyAction, MoveToDef), ProgError (NoDefSelected), - Selection (..), + Selection, + Selection' (..), defaultLog, ) import Primer.Builtins (tNat) diff --git a/primer/test/Tests/Typecheck.hs b/primer/test/Tests/Typecheck.hs index 58f4d8ec2..b1b7eaadd 100644 --- a/primer/test/Tests/Typecheck.hs +++ b/primer/test/Tests/Typecheck.hs @@ -799,7 +799,7 @@ instance Eq (TypeCacheAlpha [Module]) where (==) = tcaFunctorial instance Eq (TypeCacheAlpha ExprMeta) where (==) = tcaFunctorial -instance Eq (TypeCacheAlpha App.NodeSelection) where +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 From 47a4c1c90a67ae95dd001436a23125b456df4cbe Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 17 Apr 2023 21:26:13 +0100 Subject: [PATCH 04/16] refactor!: Rename selection fields This will significantly decrease the amount of breakage when we unify our selection types. Signed-off-by: George Thomas --- primer/src/Primer/API.hs | 2 +- primer/src/Primer/App.hs | 15 ++++++++------- primer/src/Primer/App/Base.hs | 8 +++++--- primer/test/Tests/Action/Prog.hs | 5 +++-- .../outputs/serialization/edit_response_2.json | 4 ++-- primer/test/outputs/serialization/prog.json | 4 ++-- primer/test/outputs/serialization/selection.json | 4 ++-- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index 9524c5e73..18724b015 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -1160,7 +1160,7 @@ data Selection = Selection deriving anyclass (NFData) viewSelection :: App.Selection -> Selection -viewSelection App.Selection{..} = Selection{def = selectedDef, node = viewNodeSelection <$> selectedNode} +viewSelection App.Selection{..} = Selection{def = def, node = viewNodeSelection <$> node} -- | 'App.NodeSelection' without any node metadata. data NodeSelection = NodeSelection diff --git a/primer/src/Primer/App.hs b/primer/src/Primer/App.hs index fd4fcd153..79b93f4e6 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -1,5 +1,6 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE ViewPatterns #-} -- This module defines the high level application functions. @@ -323,8 +324,8 @@ newEmptyProgImporting imported = , progSelection = Just Selection - { selectedDef = qualifyName moduleName defName - , selectedNode = Nothing + { def = qualifyName moduleName defName + , node = Nothing } } , nextID @@ -881,7 +882,7 @@ applyProgAction prog = \case ActionError $ InternalFailure "RenameModule: imported modules were edited by renaming" where - mdefName = selectedDef <$> progSelection prog + mdefName = (.def) <$> progSelection prog -- Helper for RenameModule action data RenameMods a = RM {imported :: [a], editable :: [a]} @@ -1378,8 +1379,8 @@ tcWholeProg p = do newSel <- case oldSel of Nothing -> pure Nothing Just s -> do - let defName_ = s ^. #selectedDef - updatedNode <- case s ^. #selectedNode of + let defName_ = s.def + updatedNode <- case s.node of Nothing -> pure Nothing Just sel@NodeSelection{nodeType} -> do n <- runExceptT $ focusNode p' defName_ $ getID sel @@ -1390,8 +1391,8 @@ tcWholeProg p = do pure $ Just $ Selection - { selectedDef = defName_ - , selectedNode = updatedNode + { def = defName_ + , node = updatedNode } pure $ p'{progSelection = newSel} diff --git a/primer/src/Primer/App/Base.hs b/primer/src/Primer/App/Base.hs index b82c937db..b2648af44 100644 --- a/primer/src/Primer/App/Base.hs +++ b/primer/src/Primer/App/Base.hs @@ -1,4 +1,6 @@ {-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# 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'. @@ -57,9 +59,9 @@ data NodeType = BodyNode | SigNode type Selection = Selection' (Either ExprMeta TypeMeta) data Selection' a = Selection - { selectedDef :: GVarName + { def :: GVarName -- ^ the ID of some ASTDef - , selectedNode :: Maybe (NodeSelection a) + , node :: Maybe (NodeSelection a) } deriving stock (Eq, Show, Read, Functor, Generic, Data) deriving (FromJSON, ToJSON) via PrimerJSON (Selection' a) @@ -76,4 +78,4 @@ data NodeSelection a = NodeSelection deriving anyclass (NFData) instance HasID a => HasID (NodeSelection a) where - _id = lens (getID . meta) (flip $ over #meta . set _id) + _id = lens (getID . (.meta)) (flip $ over #meta . set _id) diff --git a/primer/test/Tests/Action/Prog.hs b/primer/test/Tests/Action/Prog.hs index 4a9b54447..63ba2ef0d 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 @@ -1326,7 +1327,7 @@ 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") + (.def) <$> progSelection p @?= Just (qualifyName (ModuleName ["Module2"]) "main") case fmap (Map.assocs . moduleDefsQualified) (progModules p) of [[(n, DefAST d)]] -> do let expectedName = qualifyName (ModuleName ["Module2"]) "main" @@ -1509,7 +1510,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 Selection{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" diff --git a/primer/test/outputs/serialization/edit_response_2.json b/primer/test/outputs/serialization/edit_response_2.json index 5fd634d7a..6f9f6749d 100644 --- a/primer/test/outputs/serialization/edit_response_2.json +++ b/primer/test/outputs/serialization/edit_response_2.json @@ -147,13 +147,13 @@ } ], "progSelection": { - "selectedDef": { + "def": { "baseName": "main", "qualifiedModule": [ "M" ] }, - "selectedNode": { + "node": { "meta": { "Left": [ 0, diff --git a/primer/test/outputs/serialization/prog.json b/primer/test/outputs/serialization/prog.json index 02b6dc308..f7dff71af 100644 --- a/primer/test/outputs/serialization/prog.json +++ b/primer/test/outputs/serialization/prog.json @@ -146,13 +146,13 @@ } ], "progSelection": { - "selectedDef": { + "def": { "baseName": "main", "qualifiedModule": [ "M" ] }, - "selectedNode": { + "node": { "meta": { "Left": [ 0, diff --git a/primer/test/outputs/serialization/selection.json b/primer/test/outputs/serialization/selection.json index 0ac964006..2fe4a279a 100644 --- a/primer/test/outputs/serialization/selection.json +++ b/primer/test/outputs/serialization/selection.json @@ -1,11 +1,11 @@ { - "selectedDef": { + "def": { "baseName": "main", "qualifiedModule": [ "M" ] }, - "selectedNode": { + "node": { "meta": { "Left": [ 0, From b4746aa1f9f7b412ae8b790f6e61930e1648c48a Mon Sep 17 00:00:00 2001 From: George Thomas Date: Tue, 16 May 2023 12:15:55 +0100 Subject: [PATCH 05/16] refactor!: Don't use API-specific selection types This removes some boilerplate where we converted between types which are essentially the same. That boilerplate would have become much bigger once we extend selections to cover type definitions. Despite the previous commit, there is one small breaking change, seen in `openapi.json` (`meta` instead of `id`). Signed-off-by: George Thomas --- primer-service/src/Primer/OpenAPI.hs | 28 +++++++++--- primer-service/test/Tests/OpenAPI.hs | 9 ++-- .../test/outputs/OpenAPI/openapi.json | 4 +- primer/src/Primer/API.hs | 44 ++++--------------- 4 files changed, 37 insertions(+), 48 deletions(-) diff --git a/primer-service/src/Primer/OpenAPI.hs b/primer-service/src/Primer/OpenAPI.hs index 91db986b5..2e4c7f6da 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,9 +49,8 @@ import Primer.API ( Module, NewSessionReq, NodeBody, - NodeSelection (..), Prog, - Selection (..), + Selection, Tree, ) import Primer.API qualified as API @@ -56,7 +62,7 @@ import Primer.API.NodeFlavor ( ) import Primer.API.RecordPair (RecordPair) import Primer.Action.Available qualified as Available -import Primer.App (NodeType) +import Primer.App (NodeSelection, NodeType) import Primer.App.Base (Level) import Primer.Core ( GlobalName, @@ -71,10 +77,14 @@ import Primer.Database ( 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 +99,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 @@ -146,8 +162,8 @@ 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 "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/test/Tests/OpenAPI.hs b/primer-service/test/Tests/OpenAPI.hs index a00773e92..3ea78addc 100644 --- a/primer-service/test/Tests/OpenAPI.hs +++ b/primer-service/test/Tests/OpenAPI.hs @@ -30,9 +30,8 @@ import Primer.API ( Module (Module), NewSessionReq (..), NodeBody (BoxBody, NoBody, PrimBody, TextBody), - NodeSelection (..), Prog (Prog), - Selection (..), + Selection, Tree, viewTreeExpr, viewTreeType, @@ -45,7 +44,7 @@ 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 (Level, NodeSelection (NodeSelection), NodeType, Selection' (Selection)) import Primer.Core (GVarName, ID (ID), ModuleName, PrimCon (PrimChar, PrimInt)) import Primer.Database ( LastModified (..), @@ -224,7 +223,7 @@ tasty_Module = testToJSON $ evalExprGen 0 genModule genNodeType :: ExprGen NodeType genNodeType = G.enumBounded -genNodeSelection :: ExprGen NodeSelection +genNodeSelection :: ExprGen (NodeSelection ID) genNodeSelection = NodeSelection <$> genNodeType <*> genID genSelection :: ExprGen Selection @@ -307,7 +306,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..95f003c71 100644 --- a/primer-service/test/outputs/OpenAPI/openapi.json +++ b/primer-service/test/outputs/OpenAPI/openapi.json @@ -413,7 +413,7 @@ }, "NodeSelection": { "properties": { - "id": { + "meta": { "maximum": 9223372036854775807, "minimum": -9223372036854775808, "type": "integer" @@ -424,7 +424,7 @@ }, "required": [ "nodeType", - "id" + "meta" ], "type": "object" }, diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index 18724b015..b29e1c520 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -63,10 +63,7 @@ module Primer.API ( viewTreeType, viewTreeExpr, getApp, - Selection (..), - viewSelection, - NodeSelection (..), - viewNodeSelection, + Selection, undoAvailable, redoAvailable, Name (..), @@ -139,7 +136,6 @@ import Primer.Core ( CaseBranch' (..), Expr, Expr' (..), - ExprMeta, GVarName, GlobalName (..), HasID (..), @@ -153,7 +149,6 @@ import Primer.Core ( TyVarName, Type, Type' (..), - TypeMeta, ValConName, getID, unLocalName, @@ -659,7 +654,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 } @@ -1045,10 +1040,10 @@ availableActions = curry3 $ logAPI (noError AvailableActions) $ \(sid, level, se case selection.node of Nothing -> pure $ Available.forDef (snd <$> allDefs) level editable selection.def - Just NodeSelection{..} -> do + Just App.NodeSelection{..} -> do pure $ case nodeType of - SigNode -> Available.forSig level editable type_ id - BodyNode -> Available.forBody (snd <$> allTypeDefs) level editable expr id + SigNode -> Available.forSig level editable type_ meta + BodyNode -> Available.forBody (snd <$> allTypeDefs) level editable expr meta actionOptions :: (MonadIO m, MonadThrow m, MonadAPILog l m) => @@ -1062,7 +1057,7 @@ 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) + nodeSel = selection.node <&> \s -> (s.nodeType, s.meta) def' <- snd <$> findASTDef allDefs selection.def maybe (throwM $ ActionOptionsNoID nodeSel) pure $ Available.options @@ -1095,7 +1090,7 @@ applyActionNoInput = curry3 $ logAPI (noError ApplyActionNoInput) $ \(sid, selec (snd <$> progAllDefs prog) def selection.def - (selection.node <&> \s -> (s.nodeType, s.id)) + (selection.node <&> \s -> (s.nodeType, s.meta)) action applyActions sid actions @@ -1113,7 +1108,7 @@ applyActionInput = curry3 $ logAPI (noError ApplyActionInput) $ \(sid, body, act toProgActionInput def body.selection.def - (body.selection.node <&> \s -> (s.nodeType, s.id)) + (body.selection.node <&> \s -> (s.nodeType, s.meta)) body.option action applyActions sid actions @@ -1151,25 +1146,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 = def, node = viewNodeSelection <$> node} - --- | '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 (Either ExprMeta TypeMeta) -> NodeSelection -viewNodeSelection sel@App.NodeSelection{nodeType} = NodeSelection{nodeType, id = getID sel} +type Selection = App.Selection' ID From 03c4de701d044c47913553fa51912b8de47fd44c Mon Sep 17 00:00:00 2001 From: George Thomas Date: Thu, 20 Apr 2023 23:36:52 +0100 Subject: [PATCH 06/16] refactor: Make further use of new deriving helper Signed-off-by: George Thomas --- primer-service/src/Primer/OpenAPI.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/primer-service/src/Primer/OpenAPI.hs b/primer-service/src/Primer/OpenAPI.hs index 2e4c7f6da..8417c5fa2 100644 --- a/primer-service/src/Primer/OpenAPI.hs +++ b/primer-service/src/Primer/OpenAPI.hs @@ -136,8 +136,7 @@ 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) From 9037f7eefcf419948432ea8a46f0ebaf9414707f Mon Sep 17 00:00:00 2001 From: George Thomas Date: Thu, 20 Apr 2023 23:37:09 +0100 Subject: [PATCH 07/16] refactor: Applying actions takes selection Signed-off-by: George Thomas --- primer/src/Primer/API.hs | 30 ++++--------------- primer/src/Primer/Action.hs | 42 +++++++++++++-------------- primer/src/Primer/Action/Available.hs | 25 +++++++++------- primer/test/Tests/Action/Available.hs | 22 +++++++------- 4 files changed, 52 insertions(+), 67 deletions(-) diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index b29e1c520..eb8144200 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -255,7 +255,7 @@ data PrimerErr | UnexpectedPrimDef GVarName | AddDefError ModuleName (Maybe Text) ProgError | AddTypeDefError TyConName [ValConName] ProgError - | ActionOptionsNoID (Maybe (NodeType, ID)) + | ActionOptionsNoID Selection | ToProgActionError Available.Action ActionError | ApplyActionError [ProgAction] ProgError | UndoError ProgError @@ -1057,17 +1057,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.meta) - 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 <$> findASTDef allDefs selection.def + 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 @@ -1086,12 +1078,7 @@ applyActionNoInput = curry3 $ logAPI (noError ApplyActionNoInput) $ \(sid, selec def <- snd <$> findASTDef (progAllDefs prog) selection.def actions <- either (throwM . ToProgActionError (Available.NoInput action)) pure $ - toProgActionNoInput - (snd <$> progAllDefs prog) - def - selection.def - (selection.node <&> \s -> (s.nodeType, s.meta)) - action + toProgActionNoInput (snd <$> progAllDefs prog) def selection action applyActions sid actions applyActionInput :: @@ -1105,12 +1092,7 @@ applyActionInput = curry3 $ logAPI (noError ApplyActionInput) $ \(sid, body, act def <- snd <$> findASTDef (progAllDefs prog) body.selection.def actions <- either (throwM . ToProgActionError (Available.Input action)) pure $ - toProgActionInput - def - body.selection.def - (body.selection.node <&> \s -> (s.nodeType, s.meta)) - body.option - action + toProgActionInput def body.selection body.option action applyActions sid actions data ApplyActionBody = ApplyActionBody diff --git a/primer/src/Primer/Action.hs b/primer/src/Primer/Action.hs index 1d0e7c6dc..e968efd8c 100644 --- a/primer/src/Primer/Action.hs +++ b/primer/src/Primer/Action.hs @@ -35,7 +35,7 @@ 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 (NodeSelection (..), NodeType (..), Selection' (..)) import Primer.Core ( Expr, Expr' (..), @@ -859,11 +859,10 @@ renameForall b zt = case target zt of toProgActionNoInput :: DefMap -> ASTDef -> - GVarName -> - Maybe (NodeType, ID) -> + Selection' ID -> Available.NoInputAction -> Either ActionError [ProgAction] -toProgActionNoInput defs def defName mNodeSel = \case +toProgActionNoInput defs def sel = \case Available.MakeCase -> toProgAction [ConstructCase] Available.MakeApp -> @@ -878,7 +877,7 @@ toProgActionNoInput defs def defName mNodeSel = \case toProgAction [ConvertLetToLetrec] Available.Raise -> do id <- mid - pure [MoveToDef defName, CopyPasteBody (defName, id) [SetCursor id, Move Parent, Delete]] + pure [MoveToDef sel.def, CopyPasteBody (sel.def, id) [SetCursor id, Move Parent, Delete]] Available.EnterHole -> toProgAction [EnterHole] Available.RemoveHole -> @@ -911,34 +910,33 @@ toProgActionNoInput defs def defName mNodeSel = \case toProgAction [ConstructTApp, Move Child1] Available.RaiseType -> do id <- mid - pure [MoveToDef defName, CopyPasteSig (defName, id) [SetCursor id, Move Parent, Delete]] + pure [MoveToDef sel.def, CopyPasteSig (sel.def, id) [SetCursor id, Move Parent, Delete]] Available.DeleteType -> toProgAction [Delete] Available.DuplicateDef -> 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] + pure [DeleteDef sel.def] where - toProgAction actions = toProg' actions defName <$> maybeToEither NoNodeSelection mNodeSel - mid = maybeToEither NoNodeSelection $ snd <$> mNodeSel + toProgAction actions = toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node + mid = maybeToEither NoNodeSelection $ (.meta) <$> sel.node -- | Convert a high-level 'Available.InputAction', and associated 'Available.Option', -- to a concrete sequence of 'ProgAction's. toProgActionInput :: ASTDef -> - GVarName -> - Maybe (NodeType, ID) -> + Selection' ID -> Available.Option -> Available.InputAction -> Either ActionError [ProgAction] -toProgActionInput def defName mNodeSel opt0 = \case +toProgActionInput def sel opt0 = \case Available.MakeCon -> do opt <- optGlobal toProg [ConstructSaturatedCon opt] @@ -994,9 +992,9 @@ toProgActionInput def defName mNodeSel opt0 = \case toProg [RenameForall opt] Available.RenameDef -> do opt <- optNoCxt - pure [RenameDef defName opt] + pure [RenameDef sel.def opt] where - mid = maybeToEither NoNodeSelection $ snd <$> mNodeSel + mid = maybeToEither NoNodeSelection $ (.meta) <$> sel.node optVar = case opt0.context of Just q -> GlobalVarRef $ unsafeMkGlobalName (q, opt0.option) Nothing -> LocalVarRef $ unsafeMkLocalName opt0.option @@ -1009,7 +1007,7 @@ 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 + toProg actions = toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node offerRefined = do id <- mid -- If we have a useful type, offer the refine action, otherwise offer the saturate action. @@ -1022,10 +1020,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..74a97c481 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. @@ -33,7 +34,9 @@ import Primer.Action.Priorities qualified as P import Primer.App.Base ( Editable (..), Level (..), + NodeSelection (..), NodeType (..), + Selection' (..), ) import Primer.Core ( Expr, @@ -320,12 +323,12 @@ options :: Cxt -> Level -> ASTDef -> - Maybe (NodeType, ID) -> + 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 def sel = \case MakeCon -> pure . noFree @@ -395,23 +398,23 @@ options typeDefs defs cxt level def mNodeSel = \case (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) + s <- sel.node + case s.nodeType of + BodyNode -> fst <$> findNodeWithParent s.meta (astDefExpr def) + SigNode -> TypeNode <$> findType s.meta (astDefType def) genNames typeOrKind = do - z <- focusNode =<< mNodeSel + z <- focusNode =<< sel.node pure $ map localOpt $ flip runReader cxt $ case z of Left zE -> generateNameExpr typeOrKind zE Right zT -> generateNameTy typeOrKind zT varsInScope = do - nodeSel <- mNodeSel + nodeSel <- sel.node 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 + focusNode nodeSel = case nodeSel.nodeType of + BodyNode -> Left . locToEither <$> focusOn nodeSel.meta (astDefExpr def) + SigNode -> fmap Right $ focusOnTy nodeSel.meta $ astDefType def -- 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 diff --git a/primer/test/Tests/Action/Available.hs b/primer/test/Tests/Action/Available.hs index 1b47a3a1c..8fc2e877e 100644 --- a/primer/test/Tests/Action/Available.hs +++ b/primer/test/Tests/Action/Available.hs @@ -49,8 +49,10 @@ import Primer.App ( EditAppM, Editable (..), Level (Beginner, Expert, Intermediate), + NodeSelection (..), NodeType (..), ProgError (ActionError, DefAlreadyExists), + Selection' (..), appProg, checkAppWellFormed, checkProgWellFormed, @@ -191,12 +193,12 @@ mkTests deps (defName, DefAST def') = in testGroup testName $ enumeratePairs <&> \(level, mut) -> - let defActions = map (offered level Nothing) $ Available.forDef defs level mut d + let defActions = map (offered level $ Selection defName Nothing) $ Available.forDef defs level mut d bodyActions = map ( \id -> ( id - , map (offered level (Just (BodyNode, id))) $ + , map (offered level $ Selection defName $ Just $ NodeSelection BodyNode id) $ Available.forBody typeDefs level @@ -211,7 +213,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 (Selection defName $ Just $ NodeSelection SigNode id)) $ Available.forSig level mut (astDefType def) id ) ) . toListOf (_typeMeta % _id) @@ -275,7 +277,7 @@ tasty_available_actions_accepted = withTests 500 $ DefAST{} -> label "AST" DefPrim{} -> label "Prim" (loc, acts) <- - fmap snd . forAllWithT fst $ + fmap (first (Selection defName) . snd) . forAllWithT fst $ Gen.frequency $ catMaybes [ Just (1, pure ("actionsForDef", (Nothing, Available.forDef (snd <$> allDefs) l defMut defName))) @@ -284,13 +286,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 +304,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) def' loc act' actionSucceeds (handleEditRequest progActs) a Available.Input act' -> do def' <- maybe (annotate "primitive def" >> failure) pure $ defAST def @@ -327,7 +329,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 def' loc (snd opt) act' actionSucceedsOrCapture (fst opt) (handleEditRequest progActs) a where runEditAppMLogs :: @@ -600,7 +602,7 @@ offeredActionTest' sh l inputDef position action = do 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) exprDef (Selection 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 +611,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 exprDef (Selection 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 From b7e1e02bea32f837188c258a2dec58f646020d76 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Sun, 23 Apr 2023 23:18:30 +0100 Subject: [PATCH 08/16] feat: Add canonical names to prim type def parameters This will make it easier to output typedefs in the API, due to increased uniformity with AST typedefs. Note that these names aren't even currently used anywhere since we don't actually have any higher-kinded primitives, only ints and chars. These would be used if we added, for example, `IO` or `Array` primitives, in which case the names would be likely useful at least for _displaying_ primitive typedefs, even though they don't actually scope over anything like they do for ASTs. Signed-off-by: George Thomas --- primer/src/Primer/TypeDef.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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) From 5d2efc62fb50e45b16303337cef64342ddd0e5da Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 24 Apr 2023 00:09:04 +0100 Subject: [PATCH 09/16] feat!: Output typedefs in API modules Signed-off-by: George Thomas --- primer-service/src/Primer/OpenAPI.hs | 6 +++ primer-service/test/Tests/OpenAPI.hs | 17 ++++++- .../test/outputs/OpenAPI/openapi.json | 51 ++++++++++++++++++- primer/src/Primer/API.hs | 49 ++++++++++++++++-- primer/src/Primer/Module.hs | 6 ++- 5 files changed, 121 insertions(+), 8 deletions(-) diff --git a/primer-service/src/Primer/OpenAPI.hs b/primer-service/src/Primer/OpenAPI.hs index 8417c5fa2..ce4028e6c 100644 --- a/primer-service/src/Primer/OpenAPI.hs +++ b/primer-service/src/Primer/OpenAPI.hs @@ -52,6 +52,8 @@ import Primer.API ( Prog, Selection, Tree, + TypeDef, + ValCon, ) import Primer.API qualified as API import Primer.API.NodeFlavor ( @@ -71,6 +73,7 @@ import Primer.Core ( LVarName, ModuleName, PrimCon, + TyVarName, ) import Primer.Database ( LastModified, @@ -141,6 +144,7 @@ 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 @@ -150,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 diff --git a/primer-service/test/Tests/OpenAPI.hs b/primer-service/test/Tests/OpenAPI.hs index 3ea78addc..ce0ac6c00 100644 --- a/primer-service/test/Tests/OpenAPI.hs +++ b/primer-service/test/Tests/OpenAPI.hs @@ -33,6 +33,8 @@ import Primer.API ( Prog (Prog), Selection, Tree, + TypeDef (..), + ValCon (..), viewTreeExpr, viewTreeType, ) @@ -63,6 +65,7 @@ import Primer.Gen.Core.Raw ( genModuleName, genName, genTyConName, + genTyVarName, genType, genValConName, ) @@ -206,6 +209,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 @@ -214,7 +229,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 diff --git a/primer-service/test/outputs/OpenAPI/openapi.json b/primer-service/test/outputs/OpenAPI/openapi.json index 95f003c71..7c3ef4328 100644 --- a/primer-service/test/outputs/OpenAPI/openapi.json +++ b/primer-service/test/outputs/OpenAPI/openapi.json @@ -224,7 +224,7 @@ }, "types": { "items": { - "$ref": "#/components/schemas/GlobalName" + "$ref": "#/components/schemas/TypeDef" }, "type": "array" } @@ -716,10 +716,59 @@ ], "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" + }, "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" } } }, diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index eb8144200..aca1d9e18 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', @@ -207,10 +209,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 (ASTTypeDef), astTypeDefConstructors, typeDefNameHints, typeDefParameters) +import Primer.TypeDef qualified as TypeDef import StmContainers.Map qualified as StmMap -- | The API environment. @@ -627,7 +630,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 @@ -638,6 +641,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 @@ -663,7 +685,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 @@ -1023,7 +1062,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 :: 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) From 93e86d51fc603b516e18e60e85353d3f9350110d Mon Sep 17 00:00:00 2001 From: George Thomas Date: Wed, 26 Apr 2023 18:17:33 +0100 Subject: [PATCH 10/16] feat: Expose available actions for typedefs Note that no new unit tests are added. This is because the underlying `ProgAction`s are already well-tested, e.g. in `unit_RenameType`. In due course, the property test `tasty_available_actions_accepted` will be generalised to cover typedef actions, which will therefore check that all of these new actions can be applied without error whenever they are available. We don't yet expose actions for constructor fields (i.e. `Available.forTypeDefConsFieldNode` always returns `[]`), since making this work will, unlike the other positions, require changes to the core of the library. We _could_ actually make all the `for*`s part of one definition, now that `Selection` is in `App.Base`. But we do actually use the individual functions in some tests, and with them separate, we have slightly more control, in that we don't need to provide as much context. Note that most of the changes in this commit are actually knock-on effects of generalising `Selection` to cover type defs. Signed-off-by: George Thomas --- primer-service/exe-server/Main.hs | 2 + primer-service/src/Primer/OpenAPI.hs | 9 +- primer-service/src/Primer/Server.hs | 2 + primer-service/test/Tests/OpenAPI.hs | 36 +++- .../test/outputs/OpenAPI/openapi.json | 162 ++++++++++++++++-- primer/src/Primer/API.hs | 53 ++++-- primer/src/Primer/Action.hs | 115 +++++++++++-- primer/src/Primer/Action/Available.hs | 85 ++++++++- primer/src/Primer/Action/Errors.hs | 10 +- primer/src/Primer/Action/Priorities.hs | 10 ++ primer/src/Primer/App.hs | 71 ++++++-- primer/src/Primer/App/Base.hs | 65 ++++++- primer/test/Tests/Action/Available.hs | 23 +-- primer/test/Tests/Action/Prog.hs | 17 +- primer/test/Tests/Serialization.hs | 14 +- primer/test/Tests/Typecheck.hs | 20 ++- .../serialization/edit_response_2.json | 43 ++--- primer/test/outputs/serialization/prog.json | 43 ++--- .../test/outputs/serialization/selection.json | 43 ++--- 19 files changed, 663 insertions(+), 160 deletions(-) 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 ce4028e6c..39f9bc2ff 100644 --- a/primer-service/src/Primer/OpenAPI.hs +++ b/primer-service/src/Primer/OpenAPI.hs @@ -64,8 +64,8 @@ import Primer.API.NodeFlavor ( ) import Primer.API.RecordPair (RecordPair) import Primer.Action.Available qualified as Available -import Primer.App (NodeSelection, 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), @@ -168,6 +168,11 @@ 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 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 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 ce0ac6c00..80a639f25 100644 --- a/primer-service/test/Tests/OpenAPI.hs +++ b/primer-service/test/Tests/OpenAPI.hs @@ -46,7 +46,17 @@ import Primer.API.NodeFlavor ( ) import Primer.API.RecordPair (RecordPair (RecordPair)) import Primer.Action.Available qualified as Available -import Primer.App (Level, NodeSelection (NodeSelection), NodeType, Selection' (Selection)) +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 (..), @@ -241,8 +251,30 @@ genNodeType = G.enumBounded 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 diff --git a/primer-service/test/outputs/OpenAPI/openapi.json b/primer-service/test/outputs/OpenAPI/openapi.json index 7c3ef4328..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" }, @@ -284,7 +302,8 @@ "RaiseType", "DeleteType", "DuplicateDef", - "DeleteDef" + "DeleteDef", + "AddConField" ], "type": "string" }, @@ -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": { @@ -747,6 +792,93 @@ ], "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", @@ -1127,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 aca1d9e18..234b2784f 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -102,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 (..), @@ -109,10 +110,14 @@ import Primer.App ( EvalResp (..), Level, MutationRequest, + NodeSelection (..), NodeType (..), ProgError, QueryAppM, Question (GenerateName), + Selection' (..), + TypeDefConsSelection (..), + TypeDefSelection (..), appProg, handleEvalFullRequest, handleEvalRequest, @@ -133,6 +138,7 @@ import Primer.App ( unlog, ) import Primer.App qualified as App +import Primer.App.Base (TypeDefNodeSelection (..)) import Primer.Core ( Bind' (..), CaseBranch' (..), @@ -212,7 +218,7 @@ import Primer.Log ( import Primer.Module (moduleDefsQualified, moduleName, moduleTypesQualifiedMeta) import Primer.Name qualified as Name import Primer.Primitives (primDefType) -import Primer.TypeDef (ASTTypeDef (ASTTypeDef), astTypeDefConstructors, typeDefNameHints, typeDefParameters) +import Primer.TypeDef (ASTTypeDef (..), typeDefNameHints, typeDefParameters) import Primer.TypeDef qualified as TypeDef import StmContainers.Map qualified as StmMap @@ -255,7 +261,9 @@ 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 Selection @@ -1075,14 +1083,22 @@ availableActions = curry3 $ logAPI (noError AvailableActions) $ \(sid, level, se 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 App.NodeSelection{..} -> do - pure $ case nodeType of - SigNode -> Available.forSig level editable type_ meta - BodyNode -> Available.forBody (snd <$> allTypeDefs) level editable expr meta + 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 (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 _ -> Available.forTypeDefConsFieldNode level editable actionOptions :: (MonadIO m, MonadThrow m, MonadAPILog l m) => @@ -1096,7 +1112,7 @@ actionOptions = curry4 $ logAPI (noError ActionOptions) $ \(sid, level, selectio let prog = appProg app allDefs = progAllDefs prog allTypeDefs = progAllTypeDefs prog - def <- snd <$> findASTDef allDefs selection.def + def <- snd <$> findASTTypeOrTermDef prog selection maybe (throwM $ ActionOptionsNoID selection) pure $ Available.options (snd <$> allTypeDefs) (snd <$> allDefs) (progCxt prog) level def selection action @@ -1106,6 +1122,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 ()) -> TyConName -> m (Editable, ASTTypeDef ()) +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' a -> f (Editable, Either (ASTTypeDef ()) ASTDef) +findASTTypeOrTermDef prog = \case + App.SelectionTypeDef sel -> + Left <<$>> findASTTypeDef (progAllTypeDefs prog) sel.def + App.SelectionDef sel -> + Right <<$>> findASTDef (progAllDefs prog) sel.def + applyActionNoInput :: (MonadIO m, MonadThrow m, MonadAPILog l m) => SessionId -> @@ -1114,7 +1143,7 @@ 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 action @@ -1128,7 +1157,7 @@ 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 body.option action diff --git a/primer/src/Primer/Action.hs b/primer/src/Primer/Action.hs index e968efd8c..c397a4bed 100644 --- a/primer/src/Primer/Action.hs +++ b/primer/src/Primer/Action.hs @@ -35,7 +35,15 @@ 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 (NodeSelection (..), NodeType (..), Selection' (..)) +import Primer.App.Base ( + DefSelection (..), + NodeSelection (..), + NodeType (..), + Selection' (..), + TypeDefConsSelection (..), + TypeDefNodeSelection (..), + TypeDefSelection (..), + ) import Primer.Core ( Expr, Expr' (..), @@ -858,11 +866,11 @@ renameForall b zt = case target zt of -- | Convert a high-level 'Available.NoInputAction' to a concrete sequence of 'ProgAction's. toProgActionNoInput :: DefMap -> - ASTDef -> + Either (ASTTypeDef ()) ASTDef -> Selection' ID -> Available.NoInputAction -> Either ActionError [ProgAction] -toProgActionNoInput defs def sel = \case +toProgActionNoInput defs def0 sel0 = \case Available.MakeCase -> toProgAction [ConstructCase] Available.MakeApp -> @@ -876,7 +884,8 @@ toProgActionNoInput defs def sel = \case Available.LetToRec -> toProgAction [ConvertLetToLetrec] Available.Raise -> do - id <- mid + id <- nodeID + sel <- termSel pure [MoveToDef sel.def, CopyPasteBody (sel.def, id) [SetCursor id, Move Parent, Delete]] Available.EnterHole -> toProgAction [EnterHole] @@ -893,7 +902,8 @@ toProgActionNoInput defs def sel = \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 @@ -909,11 +919,14 @@ toProgActionNoInput defs def sel = \case Available.MakeTApp -> toProgAction [ConstructTApp, Move Child1] Available.RaiseType -> do - id <- mid + 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 sel.def) (unName (baseName sel.def) <> "Copy") defs @@ -922,21 +935,50 @@ toProgActionNoInput defs def sel = \case , CopyPasteSig (sel.def, sigID) [] , CopyPasteBody (sel.def, bodyID) [] ] - Available.DeleteDef -> + 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 sel.def <$> maybeToEither NoNodeSelection sel.node - mid = maybeToEither NoNodeSelection $ (.meta) <$> sel.node + 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 + toProgAction actions = do + sel <- termSel + toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node + 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 -> + Either (ASTTypeDef ()) ASTDef -> Selection' ID -> Available.Option -> Available.InputAction -> Either ActionError [ProgAction] -toProgActionInput def sel opt0 = \case +toProgActionInput def0 sel0 opt0 = \case Available.MakeCon -> do opt <- optGlobal toProg [ConstructSaturatedCon opt] @@ -992,9 +1034,49 @@ toProgActionInput def sel opt0 = \case toProg [RenameForall opt] Available.RenameDef -> do opt <- optNoCxt + 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 $ (.meta) <$> sel.node + 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 @@ -1007,9 +1089,12 @@ toProgActionInput def sel opt0 = \case optGlobal = case opt0.context of Nothing -> Left $ NeedLocal opt0 Just q -> pure (q, opt0.option) - toProg actions = toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node + toProg actions = do + sel <- termSel + toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node 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 diff --git a/primer/src/Primer/Action/Available.hs b/primer/src/Primer/Action/Available.hs index 74a97c481..cbc6b9613 100644 --- a/primer/src/Primer/Action/Available.hs +++ b/primer/src/Primer/Action/Available.hs @@ -15,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) @@ -32,6 +37,7 @@ import Optics ( ) import Primer.Action.Priorities qualified as P import Primer.App.Base ( + DefSelection (..), Editable (..), Level (..), NodeSelection (..), @@ -120,6 +126,7 @@ data NoInputAction | DeleteType | DuplicateDef | DeleteDef + | AddConField deriving stock (Eq, Ord, Show, Read, Enum, Bounded, Generic) deriving (ToJSON, FromJSON) via PrimerJSON NoInputAction @@ -143,6 +150,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 @@ -288,6 +299,48 @@ 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 -> + [Action] +forTypeDefConsFieldNode _ NonEditable = mempty +forTypeDefConsFieldNode l Editable = sortByPriority l [] + -- | An input for an 'InputAction'. data Option = Option { option :: Text @@ -322,13 +375,13 @@ options :: DefMap -> Cxt -> Level -> - ASTDef -> + Either (ASTTypeDef ()) 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 sel = \case +options typeDefs defs cxt level def0 sel0 = \case MakeCon -> pure . noFree @@ -383,7 +436,18 @@ options typeDefs defs cxt level def sel = \case freeVar <$> genNames (Right $ Just k) RenameDef -> pure $ freeVar [] + RenameType -> + pure $ freeVar [] + RenameCon -> + pure $ freeVar [] + RenameTypeParam -> + pure $ freeVar [] + AddCon -> + pure $ freeVar [] where + defSel = case sel0 of + SelectionDef s -> pure s + SelectionTypeDef _ -> Nothing freeVar opts = Options{opts, free = FreeVarName} noFree opts = Options{opts, free = FreeNone} localOpt = flip Option Nothing . unName @@ -398,23 +462,29 @@ options typeDefs defs cxt level def sel = \case (first (localOpt . unLocalName) <$> locals) <> (first globalOpt <$> globals) findNode = do + sel <- defSel s <- sel.node + def <- eitherToMaybe def0 case s.nodeType of BodyNode -> fst <$> findNodeWithParent s.meta (astDefExpr def) SigNode -> TypeNode <$> findType s.meta (astDefType def) genNames typeOrKind = do + sel <- defSel z <- focusNode =<< sel.node pure $ map localOpt $ flip runReader cxt $ case z of Left zE -> generateNameExpr typeOrKind zE Right zT -> generateNameTy typeOrKind zT varsInScope = do + sel <- defSel nodeSel <- sel.node focusNode nodeSel <&> \case Left zE -> variablesInScopeExpr defs zE Right zT -> (variablesInScopeTy zT, [], []) - focusNode nodeSel = case nodeSel.nodeType of - BodyNode -> Left . locToEither <$> focusOn nodeSel.meta (astDefExpr def) - SigNode -> fmap Right $ focusOnTy nodeSel.meta $ astDefType def + 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 -- 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 @@ -458,6 +528,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 @@ -477,3 +548,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..9a3deb46c 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,14 @@ data ActionError | NeedLocal Available.Option | NeedInt Available.Option | NeedChar Available.Option + | NeedTermDef + | NeedTypeDef + | NeedTermDefSelection + | NeedTypeDefSelection + | NeedTypeDefNodeSelection + | NeedTypeDefConsSelection + | 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/App.hs b/primer/src/Primer/App.hs index 79b93f4e6..97a878264 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} @@ -78,6 +80,7 @@ import Optics ( ifoldMap, mapped, over, + set, traverseOf, traversed, view, @@ -101,12 +104,17 @@ import Primer.Action ( ) import Primer.Action.ProgError (ProgError (..)) import Primer.App.Base ( + DefSelection (..), Editable (..), Level (..), NodeSelection (..), NodeType (..), Selection, Selection' (..), + TypeDefConsFieldSelection (..), + TypeDefConsSelection (..), + TypeDefNodeSelection (..), + TypeDefSelection (..), ) import Primer.Core ( Bind' (Bind), @@ -160,6 +168,7 @@ import Primer.Module ( insertDef, moduleDefsQualified, moduleTypesQualified, + moduleTypesQualifiedMeta, nextModuleID, primitiveModule, qualifyDefName, @@ -270,6 +279,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) @@ -322,11 +336,12 @@ newEmptyProgImporting imported = ] , progImports = imported' , progSelection = - Just - Selection - { def = qualifyName moduleName defName - , node = Nothing - } + Just $ + SelectionDef + DefSelection + { def = qualifyName moduleName defName + , node = Nothing + } } , nextID , toEnum 0 @@ -393,6 +408,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 @@ -557,7 +575,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 @@ -580,7 +598,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 @@ -594,7 +612,7 @@ applyProgAction prog = \case expr <- newExpr ty <- newType let def = ASTDef expr ty - pure (insertDef mod name $ DefAST def, Just $ Selection (qualifyName modName name) Nothing) + pure (insertDef mod name $ DefAST def, Just $ SelectionDef $ DefSelection (qualifyName modName name) Nothing) AddTypeDef tc td -> editModuleSameSelection (qualifiedModule tc) prog $ \m -> do td' <- generateTypeDefIDs $ TypeDefAST td let tydefs' = moduleTypes m <> Map.singleton (baseName tc) td' @@ -823,8 +841,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 @@ -841,8 +859,8 @@ 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 @@ -882,7 +900,9 @@ applyProgAction prog = \case ActionError $ InternalFailure "RenameModule: imported modules were edited by renaming" where - mdefName = (.def) <$> 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]} @@ -1310,7 +1330,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 @@ -1378,7 +1398,7 @@ tcWholeProg p = do let oldSel = progSelection p newSel <- case oldSel of Nothing -> pure Nothing - Just s -> do + Just (SelectionDef s) -> do let defName_ = s.def updatedNode <- case s.node of Nothing -> pure Nothing @@ -1389,11 +1409,24 @@ 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 + 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 + vc <- find ((== conSel.con) . valConName) $ astTypeDefConstructors tda + atMay (valConArgs vc) fieldSel.index pure $ p'{progSelection = newSel} -- | Do a full check of a 'Prog', both the imports and the local modules @@ -1465,7 +1498,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 @@ -1522,7 +1555,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 b2648af44..8f97e37a7 100644 --- a/primer/src/Primer/App/Base.hs +++ b/primer/src/Primer/App/Base.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE NoFieldSelectors #-} @@ -10,6 +11,11 @@ module Primer.App.Base ( NodeType (..), Selection, Selection' (..), + TypeDefSelection (..), + TypeDefNodeSelection (..), + TypeDefConsSelection (..), + TypeDefConsFieldSelection (..), + DefSelection (..), NodeSelection (..), ) where @@ -21,7 +27,10 @@ import Primer.Core ( ExprMeta, GVarName, HasID (..), + TyConName, + TyVarName, TypeMeta, + ValConName, getID, ) import Primer.JSON ( @@ -54,21 +63,63 @@ data NodeType = BodyNode | SigNode deriving (FromJSON, ToJSON) via PrimerJSON NodeType deriving anyclass (NFData) --- | Describes what interface element the student has selected. --- A definition in the left hand nav bar, and possibly a node in that definition. +-- | 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 = Selection +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 - -- ^ the ID of some ASTDef , node :: Maybe (NodeSelection a) } deriving stock (Eq, Show, Read, Functor, Generic, Data) - deriving (FromJSON, ToJSON) via PrimerJSON (Selection' a) + deriving (FromJSON, ToJSON) via PrimerJSON (DefSelection a) deriving anyclass (NFData) --- | A selected node, in the body or type signature of some definition. --- We have the following invariant: @nodeType = SigNode ==> isRight meta@ +-- | Some element of a node, in the body or type signature of a term definition. data NodeSelection a = NodeSelection { nodeType :: NodeType , meta :: a diff --git a/primer/test/Tests/Action/Available.hs b/primer/test/Tests/Action/Available.hs index 8fc2e877e..378c72ea5 100644 --- a/primer/test/Tests/Action/Available.hs +++ b/primer/test/Tests/Action/Available.hs @@ -46,6 +46,7 @@ import Primer.Action.Available ( import Primer.Action.Available qualified as Available import Primer.App ( App, + DefSelection (..), EditAppM, Editable (..), Level (Beginner, Expert, Intermediate), @@ -189,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 $ Selection defName 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 $ Selection defName $ Just $ NodeSelection BodyNode id) $ + , map (offered level $ SelectionDef $ DefSelection defName $ Just $ NodeSelection BodyNode id) $ Available.forBody typeDefs level @@ -213,7 +214,7 @@ mkTests deps (defName, DefAST def') = map ( \id -> ( id - , map (offered level (Selection defName $ Just $ NodeSelection 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) @@ -277,7 +278,7 @@ tasty_available_actions_accepted = withTests 500 $ DefAST{} -> label "AST" DefPrim{} -> label "Prim" (loc, acts) <- - fmap (first (Selection defName) . 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))) @@ -304,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' 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 @@ -315,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)] @@ -329,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' 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 :: @@ -597,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 (Selection exprDefName $ Just $ NodeSelection 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 @@ -611,7 +612,7 @@ offeredActionTest' sh l inputDef position action = do Just os -> pure $ if o `elem` os.opts - then Right $ toProgActionInput exprDef (Selection exprDefName $ Just $ NodeSelection 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 63ba2ef0d..3c0ab4519 100644 --- a/primer/test/Tests/Action/Prog.hs +++ b/primer/test/Tests/Action/Prog.hs @@ -36,6 +36,7 @@ import Primer.Action ( ) import Primer.App ( App, + DefSelection (..), Log (..), NodeSelection (..), NodeType (..), @@ -161,7 +162,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 @@ -503,7 +504,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" @@ -1327,7 +1328,11 @@ unit_rename_module = Left err -> assertFailure $ show err Right p -> do fmap (unModuleName . moduleName) (progModules p) @?= [["Module2"]] - (.def) <$> 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" @@ -1510,7 +1515,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{def = selectedDef, node = 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" @@ -1548,8 +1553,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 47f2bc4c6..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 (..), @@ -152,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 b1b7eaadd..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 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 6f9f6749d..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": { - "def": { - "baseName": "main", - "qualifiedModule": [ - "M" - ] - }, - "node": { - "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 f7dff71af..fcfeb7ab3 100644 --- a/primer/test/outputs/serialization/prog.json +++ b/primer/test/outputs/serialization/prog.json @@ -146,28 +146,31 @@ } ], "progSelection": { - "def": { - "baseName": "main", - "qualifiedModule": [ - "M" - ] - }, - "node": { - "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 2fe4a279a..3f33224c0 100644 --- a/primer/test/outputs/serialization/selection.json +++ b/primer/test/outputs/serialization/selection.json @@ -1,24 +1,27 @@ { - "def": { - "baseName": "main", - "qualifiedModule": [ - "M" - ] - }, - "node": { - "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 From c1096a3a870b8f7527e4bf18420359a0ec3fe445 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Thu, 27 Apr 2023 11:29:57 +0100 Subject: [PATCH 11/16] fix: Always omit nothing fields in JSON output This was motivated by a surprising deep equality failure on selections in a TypeScript frontend, due to a null field only present in one of the compared values. Signed-off-by: George Thomas --- primer/src/Primer/JSON.hs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 From 49fc7e5e55df1da101c9925313630210fa9e7e07 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Thu, 27 Apr 2023 10:21:52 +0100 Subject: [PATCH 12/16] feat: Set better selections after performing typedef actions Signed-off-by: George Thomas --- primer/src/Primer/App.hs | 83 +++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/primer/src/Primer/App.hs b/primer/src/Primer/App.hs index 97a878264..694e19c9c 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -613,28 +613,34 @@ applyProgAction prog = \case ty <- newType let def = ASTDef expr ty pure (insertDef mod name $ DefAST def, Just $ SelectionDef $ DefSelection (qualifyName modName name) Nothing) - AddTypeDef tc td -> editModuleSameSelection (qualifiedModule tc) prog $ \m -> do + 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 @@ -664,10 +670,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 @@ -685,7 +694,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 @@ -713,13 +727,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 @@ -794,9 +813,19 @@ applyProgAction prog = \case 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 From 0cbc3c45253db3fb8bb970dfb6aff437df2a76fe Mon Sep 17 00:00:00 2001 From: George Thomas Date: Wed, 24 May 2023 12:54:53 +0100 Subject: [PATCH 13/16] feat: Add actions for modifying constructor field types Signed-off-by: George Thomas --- primer/src/Foreword.hs | 12 ++++ primer/src/Primer/API.hs | 18 +++--- primer/src/Primer/Action.hs | 77 ++++++++++++++++++++++--- primer/src/Primer/Action/Available.hs | 78 +++++++++++++++++--------- primer/src/Primer/Action/Errors.hs | 1 + primer/src/Primer/Action/ProgAction.hs | 2 + primer/src/Primer/Action/ProgError.hs | 1 + primer/src/Primer/App.hs | 44 ++++++++++++++- primer/src/Primer/App/Base.hs | 9 +++ primer/src/Primer/Typecheck/Cxt.hs | 2 +- primer/src/Primer/Zipper.hs | 2 +- primer/test/Tests/Action/Prog.hs | 34 +++++++++++ 12 files changed, 235 insertions(+), 45 deletions(-) diff --git a/primer/src/Foreword.hs b/primer/src/Foreword.hs index 1ed8aebf3..ab980f6fd 100644 --- a/primer/src/Foreword.hs +++ b/primer/src/Foreword.hs @@ -32,6 +32,8 @@ module Foreword ( curry4, unsafeMaximum, spanMaybe, + adjustAtA', + findAndAdjustA', ) where -- In general, we should defer to "Protolude"'s exports and avoid name @@ -130,6 +132,11 @@ adjustAtA n f xs = case splitAt n xs of (a, b : bs) -> f b <&> \b' -> Just $ a ++ [b'] ++ bs _ -> pure Nothing +adjustAtA' :: Applicative f => Int -> (a -> f (a, z)) -> [a] -> f (Maybe ([a], z)) +adjustAtA' n f xs = case splitAt n xs of + (a, b : bs) -> f b <&> \(b', z) -> Just (a ++ [b'] ++ bs, z) + _ -> pure Nothing + -- | Adjust the first element of the list which satisfies the -- predicate. Returns 'Nothing' if there is no such element. findAndAdjust :: (a -> Bool) -> (a -> a) -> [a] -> Maybe [a] @@ -143,6 +150,11 @@ findAndAdjustA p f = \case [] -> pure Nothing x : xs -> if p x then Just . (: xs) <$> f x else (x :) <<$>> findAndAdjustA p f xs +findAndAdjustA' :: Applicative m => (a -> Bool) -> (a -> m (a, z)) -> [a] -> m (Maybe ([a], z)) +findAndAdjustA' p f = \case + [] -> pure Nothing + x : xs -> if p x then (\(x', z) -> Just . (,z) . (: xs) $ x') <$> f x else first (x :) <<$>> findAndAdjustA' p f xs + -- | Change the type of an error. modifyError :: MonadError e' m => (e -> e') -> ExceptT e m a -> m a modifyError f = runExceptT >=> either (throwError . f) pure diff --git a/primer/src/Primer/API.hs b/primer/src/Primer/API.hs index 234b2784f..481d24b3c 100644 --- a/primer/src/Primer/API.hs +++ b/primer/src/Primer/API.hs @@ -127,6 +127,7 @@ import Primer.App ( newApp, progAllDefs, progAllTypeDefs, + progAllTypeDefsMeta, progCxt, progImports, progLog, @@ -157,6 +158,7 @@ import Primer.Core ( TyVarName, Type, Type' (..), + TypeMeta, ValConName, getID, unLocalName, @@ -218,7 +220,7 @@ import Primer.Log ( import Primer.Module (moduleDefsQualified, moduleName, moduleTypesQualifiedMeta) import Primer.Name qualified as Name import Primer.Primitives (primDefType) -import Primer.TypeDef (ASTTypeDef (..), typeDefNameHints, typeDefParameters) +import Primer.TypeDef (ASTTypeDef (..), forgetTypeDefMetadata, typeDefNameHints, typeDefParameters) import Primer.TypeDef qualified as TypeDef import StmContainers.Map qualified as StmMap @@ -1082,7 +1084,7 @@ availableActions :: availableActions = curry3 $ logAPI (noError AvailableActions) $ \(sid, level, selection) -> do prog <- getProgram sid let allDefs = progAllDefs prog - allTypeDefs = progAllTypeDefs prog + allTypeDefs = progAllTypeDefsMeta prog case selection of SelectionDef sel -> do (editable, ASTDef{astDefType = type_, astDefExpr = expr}) <- findASTDef allDefs sel.def @@ -1090,15 +1092,15 @@ availableActions = curry3 $ logAPI (noError AvailableActions) $ \(sid, level, se Nothing -> Available.forDef (snd <$> allDefs) level editable sel.def Just NodeSelection{..} -> case nodeType of SigNode -> Available.forSig level editable type_ meta - BodyNode -> Available.forBody (snd <$> allTypeDefs) level editable expr meta + BodyNode -> Available.forBody (forgetTypeDefMetadata . snd <$> allTypeDefs) level editable expr meta SelectionTypeDef sel -> do - (editable, _def) <- findASTTypeDef allTypeDefs sel.def + (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 _ -> Available.forTypeDefConsFieldNode level editable + Just field -> Available.forTypeDefConsFieldNode level editable def s.con field.index field.meta actionOptions :: (MonadIO m, MonadThrow m, MonadAPILog l m) => @@ -1122,16 +1124,16 @@ 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 ()) -> TyConName -> m (Editable, ASTTypeDef ()) +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' a -> f (Editable, Either (ASTTypeDef ()) ASTDef) +findASTTypeOrTermDef :: MonadThrow f => App.Prog -> Selection -> f (Editable, Either (ASTTypeDef TypeMeta) ASTDef) findASTTypeOrTermDef prog = \case App.SelectionTypeDef sel -> - Left <<$>> findASTTypeDef (progAllTypeDefs prog) sel.def + Left <<$>> findASTTypeDef (progAllTypeDefsMeta prog) sel.def App.SelectionDef sel -> Right <<$>> findASTDef (progAllDefs prog) sel.def diff --git a/primer/src/Primer/Action.hs b/primer/src/Primer/Action.hs index c397a4bed..711b03c68 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) @@ -27,10 +30,11 @@ import Data.Bifunctor.Swap qualified as Swap 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 Optics (over, set, (%), (?~), (^.), (^?), _Just) import Primer.Action.Actions (Action (..), Movement (..), QualifiedText) import Primer.Action.Available qualified as Available import Primer.Action.Errors (ActionError (..)) @@ -40,6 +44,7 @@ import Primer.App.Base ( NodeSelection (..), NodeType (..), Selection' (..), + TypeDefConsFieldSelection (..), TypeDefConsSelection (..), TypeDefNodeSelection (..), TypeDefSelection (..), @@ -60,6 +65,7 @@ import Primer.Core ( Type' (..), TypeCache (..), TypeCacheBoth (..), + TypeMeta, ValConName, baseName, bindName, @@ -102,7 +108,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, @@ -238,6 +244,49 @@ 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 + (valCons, zt) <- + (maybe (throwError $ InternalFailure "applyActionsToField: con name not found") pure =<<) $ + flip (findAndAdjustA' ((== conName') . valConName)) (astTypeDefConstructors tyDef) \(ValCon _ ts) -> do + (t, zt) <- + maybe (throwError $ InternalFailure "applyActionsToField: con field index out of bounds") pure + =<< flip (adjustAtA' index) ts \fieldType -> do + zt <- withWrappedType fieldType \zt -> + foldlM (\l -> local addParamsToCxt . flip applyActionAndSynth l) (InType zt) actions + pure (target (top zt), zt) + pure (ValCon conName' t, zt) + 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 @@ -866,7 +915,7 @@ renameForall b zt = case target zt of -- | Convert a high-level 'Available.NoInputAction' to a concrete sequence of 'ProgAction's. toProgActionNoInput :: DefMap -> - Either (ASTTypeDef ()) ASTDef -> + Either (ASTTypeDef a) ASTDef -> Selection' ID -> Available.NoInputAction -> Either ActionError [ProgAction] @@ -964,16 +1013,22 @@ toProgActionNoInput defs def0 sel0 = \case 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 - sel <- termSel - toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node + 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 :: - Either (ASTTypeDef ()) ASTDef -> + Either (ASTTypeDef a) ASTDef -> Selection' ID -> Available.Option -> Available.InputAction -> @@ -1089,9 +1144,15 @@ toProgActionInput def0 sel0 opt0 = \case optGlobal = case opt0.context of Nothing -> Left $ NeedLocal opt0 Just q -> pure (q, opt0.option) + conFieldSel = do + (ty, s) <- conSel + maybe (Left NeedTypeDefConsFieldSelection) (pure . (ty,s.con,)) s.field toProg actions = do - sel <- termSel - toProg' actions sel.def <$> maybeToEither NoNodeSelection sel.node + 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 <- nodeID def <- termDef diff --git a/primer/src/Primer/Action/Available.hs b/primer/src/Primer/Action/Available.hs index cbc6b9613..245b0fd59 100644 --- a/primer/src/Primer/Action/Available.hs +++ b/primer/src/Primer/Action/Available.hs @@ -43,6 +43,11 @@ import Primer.App.Base ( NodeSelection (..), NodeType (..), Selection' (..), + TypeDefConsFieldSelection (..), + TypeDefConsSelection (..), + TypeDefNodeSelection (..), + TypeDefSelection (..), + getTypeDefConFieldType, ) import Primer.Core ( Expr, @@ -53,6 +58,8 @@ import Primer.Core ( ModuleName (unModuleName), Type, Type' (..), + TypeMeta, + ValConName, getID, unLocalName, _bindMeta, @@ -98,6 +105,7 @@ import Primer.Zipper ( focusOn, focusOnTy, locToEither, + target, ) -- | An offered action. @@ -337,9 +345,15 @@ forTypeDefConsNode l Editable = forTypeDefConsFieldNode :: Level -> Editable -> + ASTTypeDef TypeMeta -> + ValConName -> + Int -> + ID -> [Action] -forTypeDefConsFieldNode _ NonEditable = mempty -forTypeDefConsFieldNode l Editable = sortByPriority l [] +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 @@ -375,7 +389,7 @@ options :: DefMap -> Cxt -> Level -> - Either (ASTTypeDef ()) ASTDef -> + Either (ASTTypeDef TypeMeta) ASTDef -> Selection' ID -> InputAction -> -- | Returns 'Nothing' if an ID was required but not passed, passed but not found in the tree, @@ -445,9 +459,6 @@ options typeDefs defs cxt level def0 sel0 = \case AddCon -> pure $ freeVar [] where - defSel = case sel0 of - SelectionDef s -> pure s - SelectionTypeDef _ -> Nothing freeVar opts = Options{opts, free = FreeVarName} noFree opts = Options{opts, free = FreeNone} localOpt = flip Option Nothing . unName @@ -461,30 +472,47 @@ options typeDefs defs cxt level def0 sel0 = \case pure $ (first (localOpt . unLocalName) <$> locals) <> (first globalOpt <$> globals) - findNode = do - sel <- defSel - s <- sel.node - def <- eitherToMaybe def0 - case s.nodeType of - BodyNode -> fst <$> findNodeWithParent s.meta (astDefExpr def) - SigNode -> TypeNode <$> findType s.meta (astDefType def) - genNames typeOrKind = do - sel <- defSel - z <- focusNode =<< sel.node - pure $ map localOpt $ flip runReader cxt $ case z of - Left zE -> generateNameExpr typeOrKind zE - Right zT -> generateNameTy typeOrKind zT - varsInScope = do - sel <- defSel - nodeSel <- sel.node - focusNode nodeSel <&> \case - Left zE -> variablesInScopeExpr defs zE - Right zT -> (variablesInScopeTy zT, [], []) + 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 + (_, zT) <- conField sel + pure $ generateNameTy 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 diff --git a/primer/src/Primer/Action/Errors.hs b/primer/src/Primer/Action/Errors.hs index 9a3deb46c..18b92789f 100644 --- a/primer/src/Primer/Action/Errors.hs +++ b/primer/src/Primer/Action/Errors.hs @@ -69,6 +69,7 @@ data ActionError | NeedTypeDefSelection | NeedTypeDefNodeSelection | NeedTypeDefConsSelection + | NeedTypeDefConsFieldSelection | NeedTypeDefParamSelection | NoNodeSelection | ValConNotFound TyConName ValConName diff --git a/primer/src/Primer/Action/ProgAction.hs b/primer/src/Primer/Action/ProgAction.hs index 47653b24e..e278ec36b 100644 --- a/primer/src/Primer/Action/ProgAction.hs +++ b/primer/src/Primer/Action/ProgAction.hs @@ -47,6 +47,8 @@ data ProgAction 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 694e19c9c..1dc0b8e73 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -34,6 +34,7 @@ module Primer.App ( progAllModules, progAllDefs, progAllTypeDefs, + progAllTypeDefsMeta, allValConNames, allTyConNames, progCxt, @@ -100,6 +101,7 @@ import Primer.Action ( ProgAction (..), applyAction', applyActionsToBody, + applyActionsToField, applyActionsToTypeSig, ) import Primer.Action.ProgError (ProgError (..)) @@ -115,6 +117,7 @@ import Primer.App.Base ( TypeDefConsSelection (..), TypeDefNodeSelection (..), TypeDefSelection (..), + getTypeDefConFieldType, ) import Primer.Core ( Bind' (Bind), @@ -896,6 +899,31 @@ applyProgAction prog = \case , 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 @@ -1029,6 +1057,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. @@ -1454,8 +1495,7 @@ tcWholeProg p = do -- This is similar to what we do when selection is in a term, above. td <- Map.lookup s.def $ allTypesMeta p tda <- typeDefAST td - vc <- find ((== conSel.con) . valConName) $ astTypeDefConstructors tda - atMay (valConArgs vc) fieldSel.index + getTypeDefConFieldType tda conSel.con fieldSel.index pure $ p'{progSelection = newSel} -- | Do a full check of a 'Prog', both the imports and the local modules diff --git a/primer/src/Primer/App/Base.hs b/primer/src/Primer/App/Base.hs index 8f97e37a7..0abf4544d 100644 --- a/primer/src/Primer/App/Base.hs +++ b/primer/src/Primer/App/Base.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE PartialTypeSignatures #-} {-# LANGUAGE NoFieldSelectors #-} -- | Definitions needed to build the app. @@ -17,6 +18,7 @@ module Primer.App.Base ( TypeDefConsFieldSelection (..), DefSelection (..), NodeSelection (..), + getTypeDefConFieldType, ) where import Protolude @@ -29,6 +31,7 @@ import Primer.Core ( HasID (..), TyConName, TyVarName, + Type', TypeMeta, ValConName, getID, @@ -39,6 +42,7 @@ import Primer.JSON ( 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, @@ -130,3 +134,8 @@ data NodeSelection a = NodeSelection 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/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/Prog.hs b/primer/test/Tests/Action/Prog.hs index 3c0ab4519..1098518ec 100644 --- a/primer/test/Tests/Action/Prog.hs +++ b/primer/test/Tests/Action/Prog.hs @@ -1290,6 +1290,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) From dc1bcd222a2a8076c3cfe84f5af841f8c31bd4ff Mon Sep 17 00:00:00 2001 From: George Thomas Date: Tue, 30 May 2023 15:17:43 +0100 Subject: [PATCH 14/16] refactor: Remove `SetConFieldType` action It is of little use now that we have the more general `ConFieldAction`. Signed-off-by: George Thomas --- primer/src/Primer/Action/ProgAction.hs | 2 - primer/src/Primer/App.hs | 76 +--------- primer/test/Tests/Action/Prog.hs | 191 +------------------------ 3 files changed, 3 insertions(+), 266 deletions(-) diff --git a/primer/src/Primer/Action/ProgAction.hs b/primer/src/Primer/Action/ProgAction.hs index e278ec36b..d61fea903 100644 --- a/primer/src/Primer/Action/ProgAction.hs +++ b/primer/src/Primer/Action/ProgAction.hs @@ -39,8 +39,6 @@ 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 diff --git a/primer/src/Primer/App.hs b/primer/src/Primer/App.hs index 1dc0b8e73..9ff3a3001 100644 --- a/primer/src/Primer/App.hs +++ b/primer/src/Primer/App.hs @@ -70,7 +70,7 @@ 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 ( @@ -124,7 +124,7 @@ import Primer.Core ( CaseBranch, CaseBranch' (CaseBranch), Expr, - Expr' (Case, Con, EmptyHole, Hole, Var), + Expr' (Case, Con, EmptyHole, Var), GVarName, GlobalName (baseName, qualifiedModule), ID (..), @@ -205,7 +205,6 @@ import Primer.Typecheck ( checkTypeDefs, synth, ) -import Primer.Typecheck qualified as TC import Primer.Zipper ( ExprZ, Loc' (InBind, InExpr, InType), @@ -753,68 +752,6 @@ 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 -> editModuleCross (qualifiedModule type_) prog $ \(m, ms) -> do m' <- updateType m @@ -1021,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 -> diff --git a/primer/test/Tests/Action/Prog.hs b/primer/test/Tests/Action/Prog.hs index 1098518ec..75825c328 100644 --- a/primer/test/Tests/Action/Prog.hs +++ b/primer/test/Tests/Action/Prog.hs @@ -97,7 +97,6 @@ import Primer.Core ( import Primer.Core.DSL ( S, ann, - app, branch, case_, con, @@ -117,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) @@ -1021,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 @@ -1442,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 From 29566618c5893790efd610098206f17872b97b15 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Tue, 30 May 2023 20:09:41 +0100 Subject: [PATCH 15/16] refactor: Remove `findAndAdjustA` and `adjustAtA'` We can define these in terms of their un-primed equivalents, by choosing the correct applicative, so we effectively do so and then inline. Signed-off-by: George Thomas --- primer/src/Foreword.hs | 12 ------------ primer/src/Primer/Action.hs | 28 +++++++++++++++++++--------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/primer/src/Foreword.hs b/primer/src/Foreword.hs index ab980f6fd..1ed8aebf3 100644 --- a/primer/src/Foreword.hs +++ b/primer/src/Foreword.hs @@ -32,8 +32,6 @@ module Foreword ( curry4, unsafeMaximum, spanMaybe, - adjustAtA', - findAndAdjustA', ) where -- In general, we should defer to "Protolude"'s exports and avoid name @@ -132,11 +130,6 @@ adjustAtA n f xs = case splitAt n xs of (a, b : bs) -> f b <&> \b' -> Just $ a ++ [b'] ++ bs _ -> pure Nothing -adjustAtA' :: Applicative f => Int -> (a -> f (a, z)) -> [a] -> f (Maybe ([a], z)) -adjustAtA' n f xs = case splitAt n xs of - (a, b : bs) -> f b <&> \(b', z) -> Just (a ++ [b'] ++ bs, z) - _ -> pure Nothing - -- | Adjust the first element of the list which satisfies the -- predicate. Returns 'Nothing' if there is no such element. findAndAdjust :: (a -> Bool) -> (a -> a) -> [a] -> Maybe [a] @@ -150,11 +143,6 @@ findAndAdjustA p f = \case [] -> pure Nothing x : xs -> if p x then Just . (: xs) <$> f x else (x :) <<$>> findAndAdjustA p f xs -findAndAdjustA' :: Applicative m => (a -> Bool) -> (a -> m (a, z)) -> [a] -> m (Maybe ([a], z)) -findAndAdjustA' p f = \case - [] -> pure Nothing - x : xs -> if p x then (\(x', z) -> Just . (,z) . (: xs) $ x') <$> f x else first (x :) <<$>> findAndAdjustA' p f xs - -- | Change the type of an error. modifyError :: MonadError e' m => (e -> e') -> ExceptT e m a -> m a modifyError f = runExceptT >=> either (throwError . f) pure diff --git a/primer/src/Primer/Action.hs b/primer/src/Primer/Action.hs index 711b03c68..7d3b24cab 100644 --- a/primer/src/Primer/Action.hs +++ b/primer/src/Primer/Action.hs @@ -27,6 +27,8 @@ 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 @@ -34,6 +36,7 @@ import Data.Map (insert) import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T +import Data.Tuple.Extra ((&&&)) import Optics (over, set, (%), (?~), (^.), (^?), _Just) import Primer.Action.Actions (Action (..), Movement (..), QualifiedText) import Primer.Action.Available qualified as Available @@ -260,16 +263,23 @@ applyActionsToField smartHoles imports (mod, mods) (tyName, conName', index, tyD 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 =<<) $ - flip (findAndAdjustA' ((== conName') . valConName)) (astTypeDefConstructors tyDef) \(ValCon _ ts) -> do - (t, zt) <- - maybe (throwError $ InternalFailure "applyActionsToField: con field index out of bounds") pure - =<< flip (adjustAtA' index) ts \fieldType -> do - zt <- withWrappedType fieldType \zt -> - foldlM (\l -> local addParamsToCxt . flip applyActionAndSynth l) (InType zt) actions - pure (target (top zt), zt) - pure (ValCon conName' t, 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 From c42e03193169fcf5fceb5ac30ab90635d58987ca Mon Sep 17 00:00:00 2001 From: George Thomas Date: Tue, 30 May 2023 20:32:28 +0100 Subject: [PATCH 16/16] fix: Avoid generating names in constructor field which clash with params Signed-off-by: George Thomas --- primer/src/Primer/Action/Available.hs | 5 +++-- primer/src/Primer/Questions.hs | 12 +++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/primer/src/Primer/Action/Available.hs b/primer/src/Primer/Action/Available.hs index 245b0fd59..1e1b3c5a3 100644 --- a/primer/src/Primer/Action/Available.hs +++ b/primer/src/Primer/Action/Available.hs @@ -81,6 +81,7 @@ import Primer.Primitives (tChar, tInt) import Primer.Questions ( generateNameExpr, generateNameTy, + generateNameTyAvoiding, variablesInScopeExpr, variablesInScopeTy, ) @@ -490,8 +491,8 @@ options typeDefs defs cxt level def0 sel0 = \case Left zE -> generateNameExpr typeOrKind zE Right zT -> generateNameTy typeOrKind zT SelectionTypeDef sel -> do - (_, zT) <- conField sel - pure $ generateNameTy typeOrKind zT + (def, zT) <- conField sel + pure $ generateNameTyAvoiding (unLocalName . fst <$> astTypeDefParameters def) typeOrKind zT varsInScope = case sel0 of SelectionDef sel -> do nodeSel <- sel.node 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 =>