From 5f53082971b9c163e33aee9cd78377f29ba500c5 Mon Sep 17 00:00:00 2001 From: Karl Ostmo Date: Thu, 25 Jan 2024 17:02:14 -0800 Subject: [PATCH] Render command matrix (#1658) ## Demo 1. Run `scripts/play.sh` 2. Load http://localhost:5357/command-matrix.html The rows are sortable by column. ### Also stack build swarm:swarm-docs --fast && stack exec swarm-docs -- cheatsheet --matrix ## Screenshot ![Screenshot from 2024-01-21 21-32-56](https://github.com/swarm-game/swarm/assets/261693/f92f5ac9-8440-4aac-9a4b-9e5edac616f2) --- app/doc/Main.hs | 1 + app/doc/Swarm/Doc/Schema/Render.hs | 5 +- app/doc/Swarm/Doc/Wiki/Cheatsheet.hs | 7 +- app/doc/Swarm/Doc/Wiki/Matrix.hs | 40 ++ app/doc/Swarm/Doc/Wiki/Util.hs | 15 + src/Swarm/Doc/Command.hs | 76 ++++ src/swarm-lang/Swarm/Language/Syntax.hs | 368 +++++++++++------- .../Swarm/Language/Syntax/CommandMetadata.hs | 43 ++ src/swarm-web/Swarm/Web.hs | 6 + swarm.cabal | 7 +- web/command-matrix.html | 37 ++ web/script/command-matrix.js | 63 +++ web/style/command-matrix.css | 34 ++ web/style/tablesort.css | 48 +++ 14 files changed, 607 insertions(+), 143 deletions(-) create mode 100644 app/doc/Swarm/Doc/Wiki/Matrix.hs create mode 100644 app/doc/Swarm/Doc/Wiki/Util.hs create mode 100644 src/Swarm/Doc/Command.hs create mode 100644 src/swarm-lang/Swarm/Language/Syntax/CommandMetadata.hs create mode 100644 web/command-matrix.html create mode 100644 web/script/command-matrix.js create mode 100644 web/style/command-matrix.css create mode 100644 web/style/tablesort.css diff --git a/app/doc/Main.hs b/app/doc/Main.hs index 52acab2c..cb3accf5 100644 --- a/app/doc/Main.hs +++ b/app/doc/Main.hs @@ -50,6 +50,7 @@ cliParser = , Just Recipes <$ switch (long "recipes" <> help "Generate recipes page (uses data from recipes.yaml)") , Just Capabilities <$ switch (long "capabilities" <> help "Generate capabilities page (uses entity map)") , Just Commands <$ switch (long "commands" <> help "Generate commands page (uses constInfo, constCaps and inferConst)") + , Just CommandMatrix <$ switch (long "matrix" <> help "Generate commands matrix page") , Just Scenario <$ switch (long "scenario" <> help "Generate scenario schema page") ] diff --git a/app/doc/Swarm/Doc/Schema/Render.hs b/app/doc/Swarm/Doc/Schema/Render.hs index 30086d83..be620e3e 100644 --- a/app/doc/Swarm/Doc/Schema/Render.hs +++ b/app/doc/Swarm/Doc/Schema/Render.hs @@ -26,6 +26,7 @@ import Swarm.Doc.Schema.Parse import Swarm.Doc.Schema.Refined import Swarm.Doc.Schema.SchemaType import Swarm.Doc.Util +import Swarm.Doc.Wiki.Util import Swarm.Util (applyWhen, brackets, quote, showT) import System.Directory (listDirectory) import System.FilePath (splitExtension, (<.>), ()) @@ -112,9 +113,7 @@ recombineExtension (filenameStem, fileExtension) = genMarkdown :: [SchemaData] -> Either T.Text T.Text genMarkdown schemaThings = - left renderError $ - runPure $ - writeMarkdown (def {writerExtensions = extensionsFromList [Ext_pipe_tables]}) pd + pandocToText pd where titleMap = makeTitleMap schemaThings pd = diff --git a/app/doc/Swarm/Doc/Wiki/Cheatsheet.hs b/app/doc/Swarm/Doc/Wiki/Cheatsheet.hs index 3e739c68..b7c4a510 100644 --- a/app/doc/Swarm/Doc/Wiki/Cheatsheet.hs +++ b/app/doc/Swarm/Doc/Wiki/Cheatsheet.hs @@ -25,6 +25,8 @@ import Data.Text qualified as T import Data.Text.IO qualified as T import Swarm.Doc.Schema.Render import Swarm.Doc.Util +import Swarm.Doc.Wiki.Matrix +import Swarm.Doc.Wiki.Util import Swarm.Game.Display (displayChar) import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities) import Swarm.Game.Entity qualified as E @@ -52,7 +54,7 @@ data PageAddress = PageAddress deriving (Eq, Show) -- | An enumeration of the kinds of cheat sheets we can produce. -data SheetType = Entities | Commands | Capabilities | Recipes | Scenario +data SheetType = Entities | Commands | CommandMatrix | Capabilities | Recipes | Scenario deriving (Eq, Show, Enum, Bounded) -- * Functions @@ -62,6 +64,9 @@ makeWikiPage address s = case s of Nothing -> error "Not implemented for all Wikis" Just st -> case st of Commands -> T.putStrLn commandsPage + CommandMatrix -> case pandocToText commandsMatrix of + Right x -> T.putStrLn x + Left x -> error $ T.unpack x Capabilities -> simpleErrorHandle $ do entities <- loadEntities sendIO $ T.putStrLn $ capabilityPage address entities diff --git a/app/doc/Swarm/Doc/Wiki/Matrix.hs b/app/doc/Swarm/Doc/Wiki/Matrix.hs new file mode 100644 index 00000000..d398abba --- /dev/null +++ b/app/doc/Swarm/Doc/Wiki/Matrix.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Auto-generation of command attributes matrix. +module Swarm.Doc.Wiki.Matrix where + +import Data.List.NonEmpty qualified as NE +import Data.Text qualified as T +import Swarm.Doc.Command +import Text.Pandoc +import Text.Pandoc.Builder + +commandsMatrix :: Pandoc +commandsMatrix = + setTitle (text "Commands matrix") $ + doc (header 3 (text "Commands matrix")) + <> doc (makePropsTable ["Command", "Effects", "Actor Target", "Type"]) + +makePropsTable :: + [T.Text] -> + Blocks +makePropsTable headingsList = + simpleTable headerRow $ map genPropsRow catalogEntries + where + CommandCatalog catalogEntries = getCatalog + headerRow = map (plain . text) headingsList + +genPropsRow :: CommandEntry -> [Blocks] +genPropsRow e = + [ showCode (cmd e) + , showCode (effects e) + , showCode (hasActorTarget $ derivedAttrs e) + ] + <> NE.toList completeTypeMembers + where + showCode :: Show a => a -> Blocks + showCode = plain . code . T.pack . show + completeTypeMembers = NE.map showCode $ argTypes e diff --git a/app/doc/Swarm/Doc/Wiki/Util.hs b/app/doc/Swarm/Doc/Wiki/Util.hs new file mode 100644 index 00000000..09c63d3f --- /dev/null +++ b/app/doc/Swarm/Doc/Wiki/Util.hs @@ -0,0 +1,15 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Utilities for generating doc markup +module Swarm.Doc.Wiki.Util where + +import Control.Arrow (left) +import Data.Text (Text) +import Text.Pandoc + +pandocToText :: Pandoc -> Either Text Text +pandocToText = + left renderError + . runPure + . writeMarkdown (def {writerExtensions = extensionsFromList [Ext_pipe_tables]}) diff --git a/src/Swarm/Doc/Command.hs b/src/Swarm/Doc/Command.hs new file mode 100644 index 00000000..856a46df --- /dev/null +++ b/src/Swarm/Doc/Command.hs @@ -0,0 +1,76 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Auto-generation of command attributes matrix. +module Swarm.Doc.Command where + +import Data.Aeson (ToJSON) +import Data.List.NonEmpty qualified as NE +import Data.Set (Set) +import Data.Set qualified as Set +import GHC.Generics (Generic) +import Servant.Docs qualified as SD +import Swarm.Doc.Util +import Swarm.Language.Pretty (unchainFun) +import Swarm.Language.Syntax +import Swarm.Language.Syntax.CommandMetadata +import Swarm.Language.Typecheck (inferConst) +import Swarm.Language.Types +import Swarm.Util (listEnums) + +data DerivedAttrs = DerivedAttrs + { hasActorTarget :: Bool + , pureComputation :: Bool + , modifiesEnvironment :: Bool + , modifiesRobot :: Bool + , movesRobot :: Bool + , returnsValue :: Bool + , outputType :: String + } + deriving (Generic, ToJSON) + +data CommandEntry = CommandEntry + { cmd :: Const + , effects :: Set CommandEffect + , argTypes :: NE.NonEmpty Type + , derivedAttrs :: DerivedAttrs + } + deriving (Generic, ToJSON) + +newtype CommandCatalog = CommandCatalog + { entries :: [CommandEntry] + } + deriving (Generic, ToJSON) + +instance SD.ToSample CommandCatalog where + toSamples _ = SD.noSamples + +-- | Uses explicit effects documentation as well as +-- type signature information to compute various flags +mkEntry :: Const -> CommandEntry +mkEntry c = + CommandEntry c cmdEffects rawArgs $ + DerivedAttrs + { hasActorTarget = operatesOnActor inputArgs + , pureComputation = Set.null cmdEffects + , modifiesEnvironment = Mutation EntityChange `Set.member` cmdEffects + , modifiesRobot = not . Set.disjoint cmdEffects . Set.fromList $ map (Mutation . RobotChange) listEnums + , movesRobot = Mutation (RobotChange PositionChange) `Set.member` cmdEffects + , returnsValue = theOutputType /= TyCmd TyUnit + , outputType = show theOutputType + } + where + cmdInfo = constInfo c + cmdEffects = effectInfo $ constDoc cmdInfo + + getArgs ((Forall _ t)) = unchainFun t + + rawArgs = getArgs $ inferConst c + + inputArgs = NE.init rawArgs + theOutputType = NE.last rawArgs + + operatesOnActor = elem TyActor + +getCatalog :: CommandCatalog +getCatalog = CommandCatalog $ map mkEntry commands diff --git a/src/swarm-lang/Swarm/Language/Syntax.hs b/src/swarm-lang/Swarm/Language/Syntax.hs index 55dc7734..affa48a5 100644 --- a/src/swarm-lang/Swarm/Language/Syntax.hs +++ b/src/swarm-lang/Swarm/Language/Syntax.hs @@ -97,13 +97,15 @@ import Data.Int (Int32) import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NonEmpty import Data.Map.Strict (Map) +import Data.Set (Set) import Data.Set qualified as S -import Data.String (IsString (fromString)) +import Data.Set qualified as Set import Data.Text hiding (filter, length, map) import Data.Text qualified as T import Data.Tree import GHC.Generics (Generic) import Swarm.Language.Direction +import Swarm.Language.Syntax.CommandMetadata import Swarm.Language.Types import Swarm.Util qualified as Util import Witch.From (from) @@ -402,15 +404,21 @@ data ConstInfo = ConstInfo } deriving (Eq, Ord, Show) -data ConstDoc = ConstDoc {briefDoc :: Text, longDoc :: Text} +data ConstDoc = ConstDoc + { effectInfo :: Set CommandEffect + -- ^ NOTE: The absence of effects implies a "pure computation". + , briefDoc :: Text + , longDoc :: Text + } deriving (Eq, Ord, Show) -instance IsString ConstDoc where - fromString = flip ConstDoc "" . T.pack - data ConstMeta = -- | Function with arity of which some are commands - ConstMFunc Int Bool + ConstMFunc + -- | Arity + Int + -- | Is a command? + Bool | -- | Unary operator with fixity and associativity. ConstMUnOp MUnAssoc | -- | Binary operator with fixity and associativity. @@ -527,88 +535,163 @@ isLong c = case tangibility (constInfo c) of -- matching gives us warning if we add more constants. constInfo :: Const -> ConstInfo constInfo c = case c of - Wait -> command 0 long "Wait for a number of time steps." + Wait -> + command 0 long $ + shortDoc + (Set.singleton $ Mutation $ RobotChange BehaviorChange) + "Wait for a number of time steps." Noop -> - command 0 Intangible . doc "Do nothing." $ + command 0 Intangible . doc Set.empty "Do nothing." $ [ "This is different than `Wait` in that it does not take up a time step." , "It is useful for commands like if, which requires you to provide both branches." , "Usually it is automatically inserted where needed, so you do not have to worry about it." ] Selfdestruct -> - command 0 short . doc "Self-destruct a robot." $ - [ "Useful to not clutter the world." - , "This destroys the robot's inventory, so consider `salvage` as an alternative." - ] - Move -> command 0 short "Move forward one step." - Backup -> command 0 short "Move backward one step." + command 0 short + . doc + (Set.singleton $ Mutation $ RobotChange ExistenceChange) + "Self-destruct a robot." + $ [ "Useful to not clutter the world." + , "This destroys the robot's inventory, so consider `salvage` as an alternative." + ] + Move -> + command 0 short $ + shortDoc + (Set.singleton $ Mutation $ RobotChange PositionChange) + "Move forward one step." + Backup -> + command 0 short $ + shortDoc + (Set.singleton $ Mutation $ RobotChange PositionChange) + "Move backward one step." Path -> - command 2 short . doc "Obtain shortest path to the destination." $ - [ "Optionally supply a distance limit as the first argument." - , "Supply either a location (`inL`) or an entity (`inR`) as the second argument." - , "If a path exists, returns the direction to proceed along and the remaining distance." - ] + command 2 short + . doc + (Set.singleton $ Query $ Sensing EntitySensing) + "Obtain shortest path to the destination." + $ [ "Optionally supply a distance limit as the first argument." + , "Supply either a location (`inL`) or an entity (`inR`) as the second argument." + , "If a path exists, returns the direction to proceed along and the remaining distance." + ] Push -> - command 1 short . doc "Push an entity forward one step." $ - [ "Both entity and robot moves forward one step." - , "Destination must not contain an entity." - ] + command 1 short + . doc + (Set.fromList [Mutation EntityChange, Mutation $ RobotChange PositionChange]) + "Push an entity forward one step." + $ [ "Both entity and robot moves forward one step." + , "Destination must not contain an entity." + ] Stride -> - command 1 short . doc "Move forward multiple steps." $ - [ T.unwords ["Has a max range of", T.pack $ show maxStrideRange, "units."] - ] - Turn -> command 1 short "Turn in some direction." - Grab -> command 0 short "Grab an item from the current location." + command 1 short + . doc + (Set.singleton $ Mutation $ RobotChange PositionChange) + "Move forward multiple steps." + $ [ T.unwords ["Has a max range of", T.pack $ show maxStrideRange, "units."] + ] + Turn -> + command 1 short $ + shortDoc + (Set.singleton $ Mutation $ RobotChange PositionChange) + "Turn in some direction." + Grab -> + command 0 short $ + shortDoc + (Set.fromList [Mutation EntityChange, Mutation $ RobotChange InventoryChange]) + "Grab an item from the current location." Harvest -> - command 0 short . doc "Harvest an item from the current location." $ + command 0 short . doc (Set.fromList [Mutation EntityChange, Mutation $ RobotChange InventoryChange]) "Harvest an item from the current location." $ [ "Leaves behind a growing seed if the harvested item is growable." , "Otherwise it works exactly like `grab`." ] Ignite -> - command 1 short . doc "Ignite a combustible item in the specified direction." $ - [ "Combustion persists for a random duration and may spread." - ] + command 1 short + . doc + (Set.singleton $ Mutation EntityChange) + "Ignite a combustible item in the specified direction." + $ [ "Combustion persists for a random duration and may spread." + ] Place -> - command 1 short . doc "Place an item at the current location." $ - ["The current location has to be empty for this to work."] + command 1 short + . doc + (Set.fromList [Mutation EntityChange, Mutation $ RobotChange InventoryChange]) + "Place an item at the current location." + $ ["The current location has to be empty for this to work."] Ping -> - command 1 short . doc "Obtain the relative location of another robot." $ - [ "The other robot must be within transmission range, accounting for antennas installed on either end, and the invoking robot must be oriented in a cardinal direction." - , "The location (x, y) is given relative to one's current orientation:" - , "Positive x value is to the right, negative left. Likewise, positive y value is forward, negative back." - ] - Give -> command 2 short "Give an item to another actor nearby." - Equip -> command 1 short "Equip a device on oneself." - Unequip -> command 1 short "Unequip an equipped device, returning to inventory." - Make -> command 1 long "Make an item using a recipe." - Has -> command 1 Intangible "Sense whether the robot has a given item in its inventory." - Equipped -> command 1 Intangible "Sense whether the robot has a specific device equipped." - Count -> command 1 Intangible "Get the count of a given item in a robot's inventory." + command 1 short + . doc + (Set.singleton $ Query $ Sensing RobotSensing) + "Obtain the relative location of another robot." + $ [ "The other robot must be within transmission range, accounting for antennas installed on either end, and the invoking robot must be oriented in a cardinal direction." + , "The location (x, y) is given relative to one's current orientation:" + , "Positive x value is to the right, negative left. Likewise, positive y value is forward, negative back." + ] + Give -> + command 2 short $ + shortDoc + (Set.singleton $ Mutation $ RobotChange InventoryChange) + "Give an item to another actor nearby." + Equip -> + command 1 short $ + shortDoc + (Set.fromList [Mutation $ RobotChange InventoryChange, Mutation $ RobotChange BehaviorChange]) + "Equip a device on oneself." + Unequip -> + command 1 short $ + shortDoc + (Set.fromList [Mutation $ RobotChange InventoryChange, Mutation $ RobotChange BehaviorChange]) + "Unequip an equipped device, returning to inventory." + Make -> + command 1 long $ + shortDoc + (Set.singleton $ Mutation $ RobotChange InventoryChange) + "Make an item using a recipe." + Has -> + command 1 Intangible $ + shortDoc + (Set.singleton $ Query $ Sensing RobotSensing) + "Sense whether the robot has a given item in its inventory." + Equipped -> + command 1 Intangible $ + shortDoc + (Set.singleton $ Query $ Sensing RobotSensing) + "Sense whether the robot has a specific device equipped." + Count -> + command 1 Intangible $ + shortDoc + (Set.singleton $ Query $ Sensing RobotSensing) + "Get the count of a given item in a robot's inventory." Reprogram -> - command 2 long . doc "Reprogram another robot with a new command." $ - ["The other robot has to be nearby and idle."] + command 2 long + . doc + (Set.singleton $ Mutation $ RobotChange BehaviorChange) + "Reprogram another robot with a new command." + $ ["The other robot has to be nearby and idle."] Drill -> - command 1 long . doc "Drill through an entity." $ - [ "Usually you want to `drill forward` when exploring to clear out obstacles." - , "When you have found a source to drill, you can stand on it and `drill down`." - , "See what recipes with drill you have available." - , "The `drill` command may return the name of an entity added to your inventory." - ] + command 1 long + . doc + (Set.fromList [Mutation EntityChange, Mutation $ RobotChange InventoryChange]) + "Drill through an entity." + $ [ "Usually you want to `drill forward` when exploring to clear out obstacles." + , "When you have found a source to drill, you can stand on it and `drill down`." + , "See what recipes with drill you have available." + , "The `drill` command may return the name of an entity added to your inventory." + ] Use -> - command 2 long . doc "Use one entity upon another." $ + command 2 long . doc (Set.singleton $ Mutation EntityChange) "Use one entity upon another." $ [ "Which entities you can `use` with others depends on the available recipes." , "The object being used must be a 'required' entity in a recipe." ] Build -> - command 1 long . doc "Construct a new robot." $ + command 1 long . doc (Set.singleton $ Mutation $ RobotChange ExistenceChange) "Construct a new robot." $ [ "You can specify a command for the robot to execute." , "If the command requires devices they will be taken from your inventory and " <> "equipped on the new robot." ] Salvage -> - command 0 long . doc "Deconstruct an old robot." $ + command 0 long . doc (Set.singleton $ Mutation $ RobotChange ExistenceChange) "Deconstruct an old robot." $ ["Salvaging a robot will give you its inventory, equipped devices and log."] Say -> - command 1 short . doc "Emit a message." $ + command 1 short . doc (Set.singleton $ Mutation $ RobotChange BehaviorChange) "Emit a message." $ [ "The message will be in the robot's log (if it has one) and the global log." , "You can view the message that would be picked by `listen` from the global log " <> "in the messages panel, along with your own messages and logs." @@ -617,208 +700,217 @@ constInfo c = case c of , "In creative mode, there is of course no such limitation." ] Listen -> - command 1 long . doc "Listen for a message from other actors." $ + command 1 long . doc (Set.singleton $ Query $ Sensing RobotSensing) "Listen for a message from other actors." $ [ "It will take the first message said by the closest actor." , "You do not need to actively listen for the message to be logged though, " <> "that is done automatically once you have a listening device equipped." , "Note that you can see the messages either in your logger device or the message panel." ] - Log -> command 1 short "Log the string in the robot's logger." + Log -> command 1 short $ shortDoc (Set.singleton $ Mutation LogEmission) "Log the string in the robot's logger." View -> - command 1 short . doc "View the given actor." $ + command 1 short . doc (Set.singleton $ Query $ Sensing RobotSensing) "View the given actor." $ [ "This will recenter the map on the target robot and allow its inventory and logs to be inspected." ] Appear -> - command 1 short . doc "Set how the robot is displayed." $ + command 1 short . doc (Set.singleton $ Mutation Cosmetic) "Set how the robot is displayed." $ [ "You can either specify one character or five (for each direction)." , "The default is \"X^>v<\"." ] Create -> - command 1 short . doc "Create an item out of thin air." $ + command 1 short . doc (Set.fromList [Mutation EntityChange, Mutation $ RobotChange InventoryChange]) "Create an item out of thin air." $ ["Only available in creative mode."] - Halt -> command 1 short "Tell a robot to halt." - Time -> command 0 Intangible "Get the current time." + Halt -> command 1 short $ shortDoc (Set.singleton $ Mutation $ RobotChange BehaviorChange) "Tell a robot to halt." + Time -> + command 0 Intangible $ + shortDoc + (Set.singleton $ Query $ Sensing WorldCondition) + "Get the current time." Scout -> - command 1 short . doc "Detect whether a robot is within line-of-sight in a direction." $ + command 1 short . doc (Set.singleton $ Query $ Sensing RobotSensing) "Detect whether a robot is within line-of-sight in a direction." $ [ "Perception is blocked by 'Opaque' entities." , T.unwords ["Has a max range of", T.pack $ show maxScoutRange, "units."] ] - Whereami -> command 0 Intangible "Get the current x and y coordinates." + Whereami -> + command 0 Intangible $ + shortDoc + (Set.singleton $ Query $ Sensing RobotSensing) + "Get the current x and y coordinates." Waypoint -> - command 2 Intangible . doc "Get the x, y coordinates of a named waypoint, by index" $ + command 2 Intangible . doc (Set.singleton $ Query APriori) "Get the x, y coordinates of a named waypoint, by index" $ [ "Return only the waypoints in the same subworld as the calling robot." , "Since waypoint names can have plural multiplicity, returns a tuple of (count, (x, y))." , "The supplied index will be wrapped automatically, modulo the waypoint count." , "A robot can use the count to know whether they have iterated over the full waypoint circuit." ] Structure -> - command 2 Intangible . doc "Get the x, y coordinates of the southwest corner of a constructed structure, by name and index" $ + command 2 Intangible . doc (Set.singleton $ Query $ Sensing EntitySensing) "Get the x, y coordinates of the southwest corner of a constructed structure, by name and index" $ [ "The outermost type of the return value indicates whether any structure of such name exists." , "Since structures can have multiple occurrences, returns a tuple of (count, (x, y))." , "The supplied index will be wrapped automatically, modulo the structure count." , "A robot can use the count to know whether they have iterated over the full structure list." ] Floorplan -> - command 1 Intangible . doc "Get the dimensions of a structure template" $ + command 1 Intangible . doc (Set.singleton $ Query APriori) "Get the dimensions of a structure template" $ [ "Returns a tuple of (width, height) for the structure of the requested name." , "Yields an error if the supplied string is not the name of a structure." ] HasTag -> - command 2 Intangible . doc "Check whether the given entity has the given tag" $ + command 2 Intangible . doc (Set.singleton $ Query APriori) "Check whether the given entity has the given tag" $ [ "Returns true if the first argument is an entity that is labeled by the tag in the second argument." , "Yields an error if the first argument is not a valid entity." ] TagMembers -> - command 2 Intangible . doc "Get the entities labeled by a tag, by alphabetical index" $ + command 2 Intangible . doc (Set.singleton $ Query APriori) "Get the entities labeled by a tag, by alphabetical index" $ [ "Returns a tuple of (member count, entity)." , "The supplied index will be wrapped automatically, modulo the member count." , "A robot can use the count to know whether they have iterated over the full list." ] Detect -> - command 2 Intangible . doc "Detect an entity within a rectangle." $ + command 2 Intangible . doc (Set.singleton $ Query $ Sensing EntitySensing) "Detect an entity within a rectangle." $ ["Locate the closest instance of a given entity within the rectangle specified by opposite corners, relative to the current location."] Resonate -> - command 2 Intangible . doc "Count specific entities within a rectangle." $ + command 2 Intangible . doc (Set.singleton $ Query $ Sensing EntitySensing) "Count specific entities within a rectangle." $ [ "Applies a strong magnetic field over a given area and stimulates the matter within, generating a non-directional radio signal. A receiver tuned to the resonant frequency of the target entity is able to measure its quantity." , "Counts the entities within the rectangle specified by opposite corners, relative to the current location." ] Density -> - command 1 Intangible . doc "Count all entities within a rectangle." $ + command 1 Intangible . doc (Set.singleton $ Query $ Sensing EntitySensing) "Count all entities within a rectangle." $ [ "Applies a strong magnetic field over a given area and stimulates the matter within, generating a non-directional radio signal. A receiver measured the signal intensity to measure the quantity." , "Counts the entities within the rectangle specified by opposite corners, relative to the current location." ] Sniff -> - command 1 short . doc "Determine distance to entity." $ + command 1 short . doc (Set.singleton $ Query $ Sensing EntitySensing) "Determine distance to entity." $ [ "Measures concentration of airborne particles to infer distance to a certain kind of entity." , "If none is detected, returns (-1)." , T.unwords ["Has a max range of", T.pack $ show maxSniffRange, "units."] ] Chirp -> - command 1 short . doc "Determine direction to entity." $ + command 1 short . doc (Set.singleton $ Query $ Sensing EntitySensing) "Determine direction to entity." $ [ "Uses a directional sonic emitter and microphone tuned to the acoustic signature of a specific entity to determine its direction." , "Returns 'down' if out of range or the direction is indeterminate." , "Provides absolute directions if \"compass\" equipped, relative directions otherwise." , T.unwords ["Has a max range of", T.pack $ show maxSniffRange, "units."] ] Watch -> - command 1 short . doc "Interrupt `wait` upon location changes." $ + command 1 short . doc (Set.singleton $ Query $ Sensing EntitySensing) "Interrupt `wait` upon location changes." $ [ "Place seismic detectors to alert upon entity changes to the specified location." , "Supply a direction, as with the `scan` command, to specify a nearby location." , "Can be invoked more than once until the next `wait` command, at which time the only the registered locations that are currently nearby are preserved." , "Any change to entities at the monitored locations will cause the robot to wake up before the `wait` timeout." ] Surveil -> - command 1 short . doc "Interrupt `wait` upon (remote) location changes." $ + command 1 short . doc (Set.singleton $ Query $ Sensing EntitySensing) "Interrupt `wait` upon (remote) location changes." $ [ "Like `watch`, but with no restriction on distance." ] - Heading -> command 0 Intangible "Get the current heading." - Blocked -> command 0 Intangible "See if the robot can move forward." + Heading -> command 0 Intangible $ shortDoc (Set.singleton $ Query $ Sensing RobotSensing) "Get the current heading." + Blocked -> command 0 Intangible $ shortDoc (Set.singleton $ Query $ Sensing EntitySensing) "See if the robot can move forward." Scan -> - command 1 Intangible . doc "Scan a nearby location for entities." $ + command 1 Intangible . doc (Set.singleton $ Query $ Sensing EntitySensing) "Scan a nearby location for entities." $ [ "Adds the entity (not actor) to your inventory with count 0 if there is any." , "If you can use sum types, you can also inspect the result directly." ] - Upload -> command 1 short "Upload a robot's known entities and log to another robot." - Ishere -> command 1 Intangible "See if a specific entity is in the current location." + Upload -> command 1 short $ shortDoc (Set.singleton $ Mutation $ RobotChange BehaviorChange) "Upload a robot's known entities and log to another robot." + Ishere -> command 1 Intangible $ shortDoc (Set.singleton $ Query $ Sensing EntitySensing) "See if a specific entity is in the current location." Isempty -> - command 0 Intangible . doc "Check if the current location is empty." $ + command 0 Intangible . doc (Set.singleton $ Query $ Sensing EntitySensing) "Check if the current location is empty." $ [ "Detects whether or not the current location contains an entity." , "Does not detect robots or other actors." ] - Self -> function 0 "Get a reference to the current robot." - Parent -> function 0 "Get a reference to the robot's parent." - Base -> function 0 "Get a reference to the base." - Meet -> command 0 Intangible "Get a reference to a nearby actor, if there is one." - MeetAll -> command 0 long "Run a command for each nearby actor." - Whoami -> command 0 Intangible "Get the robot's display name." - Setname -> command 1 short "Set the robot's display name." + Self -> function 0 $ shortDoc (Set.singleton $ Query APriori) "Get a reference to the current robot." + Parent -> function 0 $ shortDoc (Set.singleton $ Query APriori) "Get a reference to the robot's parent." + Base -> function 0 $ shortDoc (Set.singleton $ Query APriori) "Get a reference to the base." + Meet -> command 0 Intangible $ shortDoc (Set.singleton $ Query $ Sensing RobotSensing) "Get a reference to a nearby actor, if there is one." + MeetAll -> command 0 long $ shortDoc (Set.fromList [Mutation $ RobotChange BehaviorChange, Query $ Sensing RobotSensing]) "Run a command for each nearby actor." + Whoami -> command 0 Intangible $ shortDoc (Set.singleton $ Query $ Sensing RobotSensing) "Get the robot's display name." + Setname -> command 1 short $ shortDoc (Set.singleton $ Mutation $ RobotChange BehaviorChange) "Set the robot's display name." Random -> - command 1 Intangible . doc "Get a uniformly random integer." $ + command 1 Intangible . doc (Set.singleton $ Query PRNG) "Get a uniformly random integer." $ ["The random integer will be chosen from the range 0 to n-1, exclusive of the argument."] - Run -> command 1 long "Run a program loaded from a file." - Return -> command 1 Intangible "Make the value a result in `cmd`." - Try -> command 2 Intangible "Execute a command, catching errors." - Undefined -> function 0 "A value of any type, that is evaluated as error." - Fail -> function 1 "A value of any type, that is evaluated as error with message." + Run -> command 1 long $ shortDoc (Set.singleton $ Mutation $ RobotChange BehaviorChange) "Run a program loaded from a file." + Return -> command 1 Intangible $ shortDoc Set.empty "Make the value a result in `cmd`." + Try -> command 2 Intangible $ shortDoc Set.empty "Execute a command, catching errors." + Undefined -> function 0 $ shortDoc Set.empty "A value of any type, that is evaluated as error." + Fail -> function 1 $ shortDoc Set.empty "A value of any type, that is evaluated as error with message." If -> - function 3 . doc "If-Then-Else function." $ + function 3 . doc Set.empty "If-Then-Else function." $ ["If the bool predicate is true then evaluate the first expression, otherwise the second."] - Inl -> function 1 "Put the value into the left component of a sum type." - Inr -> function 1 "Put the value into the right component of a sum type." - Case -> function 3 "Evaluate one of the given functions on a value of sum type." - Fst -> function 1 "Get the first value of a pair." - Snd -> function 1 "Get the second value of a pair." - Force -> function 1 "Force the evaluation of a delayed value." - Not -> function 1 "Negate the boolean value." - Neg -> unaryOp "-" 7 P "Negate the given integer value." - Add -> binaryOp "+" 6 L "Add the given integer values." - And -> binaryOp "&&" 3 R "Logical and (true if both values are true)." - Or -> binaryOp "||" 2 R "Logical or (true if either value is true)." - Sub -> binaryOp "-" 6 L "Subtract the given integer values." - Mul -> binaryOp "*" 7 L "Multiply the given integer values." - Div -> binaryOp "/" 7 L "Divide the left integer value by the right one, rounding down." - Exp -> binaryOp "^" 8 R "Raise the left integer value to the power of the right one." - Eq -> binaryOp "==" 4 N "Check that the left value is equal to the right one." - Neq -> binaryOp "!=" 4 N "Check that the left value is not equal to the right one." - Lt -> binaryOp "<" 4 N "Check that the left value is lesser than the right one." - Gt -> binaryOp ">" 4 N "Check that the left value is greater than the right one." - Leq -> binaryOp "<=" 4 N "Check that the left value is lesser or equal to the right one." - Geq -> binaryOp ">=" 4 N "Check that the left value is greater or equal to the right one." - Format -> function 1 "Turn an arbitrary value into a string." - Concat -> binaryOp "++" 6 R "Concatenate the given strings." - Chars -> function 1 "Counts the number of characters in the text." + Inl -> function 1 $ shortDoc Set.empty "Put the value into the left component of a sum type." + Inr -> function 1 $ shortDoc Set.empty "Put the value into the right component of a sum type." + Case -> function 3 $ shortDoc Set.empty "Evaluate one of the given functions on a value of sum type." + Fst -> function 1 $ shortDoc Set.empty "Get the first value of a pair." + Snd -> function 1 $ shortDoc Set.empty "Get the second value of a pair." + Force -> function 1 $ shortDoc Set.empty "Force the evaluation of a delayed value." + Not -> function 1 $ shortDoc Set.empty "Negate the boolean value." + Neg -> unaryOp "-" 7 P $ shortDoc Set.empty "Negate the given integer value." + Add -> binaryOp "+" 6 L $ shortDoc Set.empty "Add the given integer values." + And -> binaryOp "&&" 3 R $ shortDoc Set.empty "Logical and (true if both values are true)." + Or -> binaryOp "||" 2 R $ shortDoc Set.empty "Logical or (true if either value is true)." + Sub -> binaryOp "-" 6 L $ shortDoc Set.empty "Subtract the given integer values." + Mul -> binaryOp "*" 7 L $ shortDoc Set.empty "Multiply the given integer values." + Div -> binaryOp "/" 7 L $ shortDoc Set.empty "Divide the left integer value by the right one, rounding down." + Exp -> binaryOp "^" 8 R $ shortDoc Set.empty "Raise the left integer value to the power of the right one." + Eq -> binaryOp "==" 4 N $ shortDoc Set.empty "Check that the left value is equal to the right one." + Neq -> binaryOp "!=" 4 N $ shortDoc Set.empty "Check that the left value is not equal to the right one." + Lt -> binaryOp "<" 4 N $ shortDoc Set.empty "Check that the left value is lesser than the right one." + Gt -> binaryOp ">" 4 N $ shortDoc Set.empty "Check that the left value is greater than the right one." + Leq -> binaryOp "<=" 4 N $ shortDoc Set.empty "Check that the left value is lesser or equal to the right one." + Geq -> binaryOp ">=" 4 N $ shortDoc Set.empty "Check that the left value is greater or equal to the right one." + Format -> function 1 $ shortDoc Set.empty "Turn an arbitrary value into a string." + Concat -> binaryOp "++" 6 R $ shortDoc Set.empty "Concatenate the given strings." + Chars -> function 1 $ shortDoc Set.empty "Counts the number of characters in the text." Split -> - function 2 . doc "Split the text into two at given position." $ + function 2 . doc Set.empty "Split the text into two at given position." $ [ "To be more specific, the following holds for all `text` values `s1` and `s2`:" , "`(s1,s2) == split (chars s1) (s1 ++ s2)`" , "So split can be used to undo concatenation if you know the length of the original string." ] CharAt -> - function 2 . doc "Get the character at a given index." $ + function 2 . doc Set.empty "Get the character at a given index." $ [ "Gets the character (as an `int` representing a Unicode codepoint) at a specific index in a `text` value. Valid indices are 0 through `chars t - 1`." , "Throws an exception if given an out-of-bounds index." ] ToChar -> - function 1 . doc "Create a singleton `text` value from the given character code." $ + function 1 . doc Set.empty "Create a singleton `text` value from the given character code." $ [ "That is, `chars (toChar c) == 1` and `charAt 0 (toChar c) == c`." ] AppF -> - binaryOp "$" 0 R . doc "Apply the function on the left to the value on the right." $ + binaryOp "$" 0 R . doc Set.empty "Apply the function on the left to the value on the right." $ [ "This operator is useful to avoid nesting parentheses." , "For exaple:" , "`f $ g $ h x = f (g (h x))`" ] Swap -> - command 1 short . doc "Swap placed entity with one in inventory." $ + command 1 short . doc (Set.fromList [Mutation EntityChange, Mutation $ RobotChange InventoryChange]) "Swap placed entity with one in inventory." $ [ "This essentially works like atomic grab and place." , "Use this to avoid race conditions where more robots grab, scan or place in one location." ] Atomic -> - command 1 Intangible . doc "Execute a block of commands atomically." $ + command 1 Intangible . doc (Set.singleton MetaEffect) "Execute a block of commands atomically." $ [ "When executing `atomic c`, a robot will not be interrupted, that is, no other robots will execute any commands while the robot is executing @c@." ] Instant -> - command 1 Intangible . doc "Execute a block of commands instantly." $ + command 1 Intangible . doc (Set.singleton MetaEffect) "Execute a block of commands instantly." $ [ "Like `atomic`, but with no restriction on program size." ] Key -> - function 1 . doc "Create a key value from a text description." $ + function 1 . doc Set.empty "Create a key value from a text description." $ [ "The key description can optionally start with modifiers like 'C-', 'M-', 'A-', or 'S-', followed by either a regular key, or a special key name like 'Down' or 'End'" , "For example, 'M-C-x', 'Down', or 'S-4'." , "Which key combinations are actually possible to type may vary by keyboard and terminal program." ] InstallKeyHandler -> - command 2 Intangible . doc "Install a keyboard input handler." $ + command 2 Intangible . doc (Set.singleton $ Mutation $ RobotChange BehaviorChange) "Install a keyboard input handler." $ [ "The first argument is a hint line that will be displayed when the input handler is active." , "The second argument is a function to handle keyboard inputs." ] - Teleport -> command 2 short "Teleport a robot to the given location." - As -> command 2 Intangible "Hypothetically run a command as if you were another robot." - RobotNamed -> command 1 Intangible "Find an actor by name." - RobotNumbered -> command 1 Intangible "Find an actor by number." - Knows -> command 1 Intangible "Check if the robot knows about an entity." + Teleport -> command 2 short $ shortDoc (Set.singleton $ Mutation $ RobotChange PositionChange) "Teleport a robot to the given location." + As -> command 2 Intangible $ shortDoc (Set.singleton $ Mutation $ RobotChange BehaviorChange) "Hypothetically run a command as if you were another robot." + RobotNamed -> command 1 Intangible $ shortDoc (Set.singleton $ Query $ Sensing RobotSensing) "Find an actor by name." + RobotNumbered -> command 1 Intangible $ shortDoc (Set.singleton $ Query $ Sensing RobotSensing) "Find an actor by number." + Knows -> command 1 Intangible $ shortDoc (Set.singleton $ Query $ Sensing RobotSensing) "Check if the robot knows about an entity." where - doc b ls = ConstDoc b (T.unlines ls) + doc e b ls = ConstDoc e b (T.unlines ls) + shortDoc e b = ConstDoc e b "" unaryOp s p side d = ConstInfo { syntax = s diff --git a/src/swarm-lang/Swarm/Language/Syntax/CommandMetadata.hs b/src/swarm-lang/Swarm/Language/Syntax/CommandMetadata.hs new file mode 100644 index 00000000..550c31c8 --- /dev/null +++ b/src/swarm-lang/Swarm/Language/Syntax/CommandMetadata.hs @@ -0,0 +1,43 @@ +-- | +-- SPDX-License-Identifier: BSD-3-Clause +-- +-- Command metadata for documentation +module Swarm.Language.Syntax.CommandMetadata where + +import Data.Aeson (ToJSON) +import GHC.Generics (Generic) + +data SensingType + = RobotSensing + | EntitySensing + | WorldCondition + deriving (Eq, Ord, Enum, Bounded, Show, Generic, ToJSON) + +data QueryType + = -- | empirical knowledge + Sensing SensingType + | -- | The random number generator + PRNG + | -- | a priori knowledge + APriori + deriving (Eq, Ord, Show, Generic, ToJSON) + +data RobotChangeType + = PositionChange + | InventoryChange + | ExistenceChange + | BehaviorChange + deriving (Eq, Ord, Enum, Bounded, Show, Generic, ToJSON) + +data MutationType + = Cosmetic + | LogEmission + | EntityChange + | RobotChange RobotChangeType + deriving (Eq, Ord, Show, Generic, ToJSON) + +data CommandEffect + = Query QueryType + | MetaEffect + | Mutation MutationType + deriving (Eq, Ord, Show, Generic, ToJSON) diff --git a/src/swarm-web/Swarm/Web.hs b/src/swarm-web/Swarm/Web.hs index 74bfde88..3443e0d5 100644 --- a/src/swarm-web/Swarm/Web.hs +++ b/src/swarm-web/Swarm/Web.hs @@ -63,6 +63,7 @@ import Servant import Servant.Docs (ToCapture) import Servant.Docs qualified as SD import Servant.Docs.Internal qualified as SD (renderCurlBasePath) +import Swarm.Doc.Command import Swarm.Game.Robot import Swarm.Game.Scenario.Objective import Swarm.Game.Scenario.Objective.Graph @@ -109,6 +110,7 @@ type SwarmAPI = :<|> "code" :> "render" :> ReqBody '[PlainText] T.Text :> Post '[PlainText] T.Text :<|> "code" :> "run" :> ReqBody '[PlainText] T.Text :> Post '[PlainText] T.Text :<|> "paths" :> "log" :> Get '[JSON] (RingBuffer CacheLogEntry) + :<|> "commands" :> Get '[JSON] CommandCatalog :<|> "repl" :> "history" :> "full" :> Get '[JSON] [REPLHistItem] :<|> "map" :> Capture "size" AreaDimensions :> Get '[JSON] GridResponse @@ -164,6 +166,7 @@ mkApp state events = :<|> codeRenderHandler :<|> codeRunHandler events :<|> pathsLogHandler state + :<|> cmdMatrixHandler state :<|> replHandler state :<|> mapViewHandler state @@ -241,6 +244,9 @@ pathsLogHandler appStateRef = do appState <- liftIO (readIORef appStateRef) pure $ appState ^. gameState . pathCaching . pathCachingLog +cmdMatrixHandler :: ReadableIORef AppState -> Handler CommandCatalog +cmdMatrixHandler _ = pure getCatalog + replHandler :: ReadableIORef AppState -> Handler [REPLHistItem] replHandler appStateRef = do appState <- liftIO (readIORef appStateRef) diff --git a/swarm.cabal b/swarm.cabal index d1205722..7c856aba 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -115,6 +115,7 @@ library swarm-lang Swarm.Language.Pretty Swarm.Language.Requirement Swarm.Language.Syntax + Swarm.Language.Syntax.CommandMetadata Swarm.Language.Text.Markdown Swarm.Language.Typecheck Swarm.Language.Typecheck.Unify @@ -422,7 +423,8 @@ library swarm-util library import: stan-config, common, ghc2021-extensions - exposed-modules: Swarm.Doc.Keyword + exposed-modules: Swarm.Doc.Command + Swarm.Doc.Keyword Swarm.Doc.Pedagogy Swarm.Doc.Util Swarm.TUI.Border @@ -566,6 +568,7 @@ library , Swarm.Language.Pretty , Swarm.Language.Requirement , Swarm.Language.Syntax + , Swarm.Language.Syntax.CommandMetadata , Swarm.Language.Text.Markdown , Swarm.Language.Typecheck , Swarm.Language.Typecheck.Unify @@ -675,6 +678,8 @@ executable swarm-docs Swarm.Doc.Schema.Render Swarm.Doc.Schema.SchemaType Swarm.Doc.Wiki.Cheatsheet + Swarm.Doc.Wiki.Matrix + Swarm.Doc.Wiki.Util build-depends: optparse-applicative >= 0.16 && < 0.19, dotgen >= 0.4 && < 0.5, pandoc >= 3.0 && < 3.2, diff --git a/web/command-matrix.html b/web/command-matrix.html new file mode 100644 index 00000000..fb88b7ef --- /dev/null +++ b/web/command-matrix.html @@ -0,0 +1,37 @@ + + + + Command matrix + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandExplicit robot targetPure computationModifies environmentModifies robotMoves robotReturns value
+ + \ No newline at end of file diff --git a/web/script/command-matrix.js b/web/script/command-matrix.js new file mode 100644 index 00000000..68ca33c8 --- /dev/null +++ b/web/script/command-matrix.js @@ -0,0 +1,63 @@ +function mkLink(text, url) { + const anchor = document.createElement("a"); + anchor.href = url + anchor.textContent = text; + return anchor; +} + +function insertTableRows(myTableBody, entries) { + for (const entry of entries) { + const rowItem = document.createElement("tr"); + + const fieldVals = [ + entry.derivedAttrs.hasActorTarget, + entry.derivedAttrs.pureComputation, + entry.derivedAttrs.modifiesEnvironment, + entry.derivedAttrs.modifiesRobot, + entry.derivedAttrs.movesRobot, + entry.derivedAttrs.returnsValue, + ] + + const cellVals = [ + mkLink(entry.cmd, "https://github.com/swarm-game/swarm/wiki/Commands-Cheat-Sheet#" + entry.cmd), + ]; + + for (const val of fieldVals) { + const span = document.createElement("span"); + span.className = val ? "trueValue" : "falseValue"; + span.appendChild(document.createTextNode(val)); + cellVals.push(span); + } + + // cellVals.push(document.createTextNode(entry.derivedAttrs.outputType)); + + for (const val of cellVals) { + const cellElement = document.createElement("td"); + cellElement.appendChild(val); + rowItem.append(cellElement); + } + + myTableBody.appendChild(rowItem); + } +} + +function doFetch(myTable) { + fetch("commands") + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error, status = ${response.status}`); + } + return response.json(); + }) + .then((data) => { + const myTableBody = myTable.querySelector("tbody"); + insertTableRows(myTableBody, data.entries); + // Documentation: http://tristen.ca/tablesort/demo/ + new Tablesort(document.getElementById('my-table')); + }) + .catch((error) => { + const p = document.createElement("p"); + p.appendChild(document.createTextNode(`Error: ${error.message}`)); + document.body.insertBefore(p, myTable); + }); +} \ No newline at end of file diff --git a/web/style/command-matrix.css b/web/style/command-matrix.css new file mode 100644 index 00000000..00bc4d34 --- /dev/null +++ b/web/style/command-matrix.css @@ -0,0 +1,34 @@ +body { + font-family: sans-serif; +} + +.trueValue { + color: #00a000; +} + +.falseValue { + color: #a00000; +} + +a:link { + color: lavender; + text-decoration: none; +} + +/* visited link */ +a:visited { + color: lightblue; + text-decoration: none; +} + +/* mouse over link */ +a:hover { + color: hotpink; + text-decoration: underline; +} + +/* selected link */ +a:active { + color: blue; + text-decoration: underline; +} \ No newline at end of file diff --git a/web/style/tablesort.css b/web/style/tablesort.css new file mode 100644 index 00000000..8510f7df --- /dev/null +++ b/web/style/tablesort.css @@ -0,0 +1,48 @@ +body { + color: white; + background-color: #212121; + } + + table { + margin-left: auto; + margin-right: auto; + } + + th, td { + padding: 0.25em; + } + + th[role=columnheader]:not(.no-sort) { + cursor: pointer; + } + + th[role=columnheader]:not(.no-sort):after { + content: ''; + float: right; + margin-top: 7px; + margin-left: 5px; + border-width: 0 4px 4px; + border-style: solid; + border-color: white transparent; + visibility: hidden; + opacity: 0; + -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + } + + th[aria-sort=ascending]:not(.no-sort):after { + border-bottom: none; + border-width: 4px 4px 0; + } + + th[aria-sort]:not(.no-sort):after { + visibility: visible; + opacity: 0.4; + } + + th[role=columnheader]:not(.no-sort):hover:after { + visibility: visible; + opacity: 1; + } \ No newline at end of file