Skip to content

Commit

Permalink
Dump a list of tutorials and commands they first introduce
Browse files Browse the repository at this point in the history
  • Loading branch information
kostmo committed Apr 1, 2023
1 parent 650bb1c commit 71444bc
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 9 deletions.
3 changes: 2 additions & 1 deletion app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Data.Text.IO qualified as Text
import GitHash (GitInfo, giBranch, giHash, tGitInfoCwdTry)
import Options.Applicative
import Swarm.App (appMain)
import Swarm.DocGen (EditorType (..), GenerateDocs (..), PageAddress (..), SheetType (..), generateDocs)
import Swarm.Docs.Gen (EditorType (..), GenerateDocs (..), PageAddress (..), SheetType (..), generateDocs)
import Swarm.Language.LSP (lspMain)
import Swarm.Language.Pipeline (processTerm)
import Swarm.TUI.Model (AppOpts (..), ColorMode (..))
Expand Down Expand Up @@ -71,6 +71,7 @@ cliParser =
[ command "recipes" (info (pure RecipeGraph) $ progDesc "Output graphviz dotfile of entity dependencies based on recipes")
, command "editors" (info (EditorKeywords <$> editor <**> helper) $ progDesc "Output editor keywords")
, command "cheatsheet" (info (CheatSheet <$> address <*> cheatsheet <**> helper) $ progDesc "Output nice Wiki tables")
, command "pedagogy" (info (pure TutorialCoverage) $ progDesc "Output tutorial coverage")
]
editor :: Parser (Maybe EditorType)
editor =
Expand Down
6 changes: 5 additions & 1 deletion src/Swarm/DocGen.hs → src/Swarm/Docs/Gen.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

-- |
-- SPDX-License-Identifier: BSD-3-Clause
module Swarm.DocGen (
module Swarm.Docs.Gen (
generateDocs,
GenerateDocs (..),
EditorType (..),
Expand Down Expand Up @@ -43,6 +43,7 @@ import Data.Text.IO qualified as T
import Data.Tuple (swap)
import Data.Yaml (decodeFileEither)
import Data.Yaml.Aeson (prettyPrintParseException)
import Swarm.Docs.Pedagogy
import Swarm.Game.Display (displayChar)
import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities)
import Swarm.Game.Entity qualified as E
Expand Down Expand Up @@ -78,6 +79,8 @@ data GenerateDocs where
-- | Keyword lists for editors.
EditorKeywords :: Maybe EditorType -> GenerateDocs
CheatSheet :: PageAddress -> Maybe SheetType -> GenerateDocs
-- | List command introductions by tutorial
TutorialCoverage :: GenerateDocs
deriving (Eq, Show)

data EditorType = Emacs | VSCode
Expand Down Expand Up @@ -129,6 +132,7 @@ generateDocs = \case
entities <- ExceptT loadEntities
recipes <- withExceptT F.prettyFailure $ loadRecipes entities
liftIO $ T.putStrLn $ recipePage address recipes
TutorialCoverage -> renderTutorialProgression >>= putStrLn . T.unpack

-- ----------------------------------------------------------------------------
-- GENERATE KEYWORDS: LIST OF WORDS TO BE HIGHLIGHTED
Expand Down
200 changes: 200 additions & 0 deletions src/Swarm/Docs/Pedagogy.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Assess pedagogical soundness of the tutorials.
--
-- Approach:
-- 1. Obtain a list of all of the tutorial scenarios, in order
-- 2. Search their "solution" code for `commands`
-- 3. "fold" over the tutorial list, noting which tutorial was first to introduce each command
module Swarm.Docs.Pedagogy (
renderTutorialProgression,
generateIntroductionsSequence,
CoverageInfo (..),
TutorialInfo (..),
) where

import Control.Arrow ((&&&))
import Control.Lens (universe, view)
import Control.Monad (guard)
import Control.Monad.Except (ExceptT (..), liftIO)
import Data.Char (isLetter)
import Data.List (foldl', sort)
import Data.List.Split (wordsBy)
import Data.Map qualified as M
import Data.Maybe (mapMaybe)
import Data.Set (Set)
import Data.Set qualified as S
import Data.Text (Text)
import Data.Text qualified as T
import Swarm.Game.Entity (loadEntities)
import Swarm.Game.Scenario (Scenario, scenarioDescription, scenarioName, scenarioObjectives, scenarioSolution)
import Swarm.Game.Scenario.Objective (objectiveGoal)
import Swarm.Game.ScenarioInfo (ScenarioCollection, ScenarioInfoPair, flatten, loadScenariosWithWarnings, scenarioCollectionToList, scenarioPath)
import Swarm.Language.Module (Module (..))
import Swarm.Language.Pipeline (ProcessedTerm (..))
import Swarm.Language.Syntax
import Swarm.Language.Types (Polytype)
import Swarm.TUI.Controller (getTutorials)
import Swarm.Util (commaList, simpleErrorHandle)

-- * Constants

wikiPrefix :: Text
wikiPrefix = "https://github.com/swarm-game/swarm/wiki/"

commandsWikiPrefix :: Text
commandsWikiPrefix = wikiPrefix <> "Commands-Cheat-Sheet#"

-- * Types

-- | Tutorials augmented by the set of
-- commands that they introduce.
-- Generated by folding over all of the
-- tutorials in sequence.
data CoverageInfo = CoverageInfo
{ tutInfo :: TutorialInfo
, novelSolutionCommands :: Set Const
}

-- | Tutorial scenarios with the set of commands
-- introduced in their solution and descriptions
-- having been extracted
data TutorialInfo = TutorialInfo
{ scenarioPair :: ScenarioInfoPair
, solutionCommands :: Set Const
, descriptionCommands :: Set Const
}

-- | A private type used by the fold
data CommandAccum = CommandAccum
{ _encounteredCmds :: Set Const
, tuts :: [CoverageInfo]
}

-- * Functions

-- | Extract commands from both goal descriptions and solution code.
extractCommandUsages :: ScenarioInfoPair -> TutorialInfo
extractCommandUsages siPair@(s, _si) =
TutorialInfo siPair solnCommands $ getDescCommands s
where
solnCommands = S.fromList $ maybe [] getCommands maybeSoln
maybeSoln = view scenarioSolution s

-- | Obtain the set of all commands mentioned by
-- name in the tutorial's goal descriptions.
--
-- NOTE: It may be more robust to require that a command reference
-- be surrounded by backticks and parse for that accordingly.
getDescCommands :: Scenario -> Set Const
getDescCommands s =
S.fromList $ mapMaybe (`M.lookup` txtLookups) allWords
where
goalTextParagraphs = concatMap (view objectiveGoal) $ view scenarioObjectives s
allWords = concatMap (wordsBy (not . isLetter) . T.unpack . T.toLower) goalTextParagraphs

commandConsts = filter isCmd allConst
txtLookups = M.fromList $ map (T.unpack . syntax . constInfo &&& id) commandConsts

-- | Extract the command names from the source code of the solution.
--
-- NOTE: The processed solution stored in the scenario has been "elaborated";
-- e.g. `noop` gets inserted for an empty `build {}` command.
-- So we explicitly ignore `noop`.
--
-- Also, the code from `run` is not parsed transitively yet.
getCommands :: ProcessedTerm -> [Const]
getCommands (ProcessedTerm (Module stx _) _ _) =
mapMaybe isCommand nodelist
where
ignoredCommands = S.fromList [Run, Noop]

nodelist :: [Syntax' Polytype]
nodelist = universe stx
isCommand (Syntax' _ t _) = case t of
TConst c -> guard (isCmd c && c `S.notMember` ignoredCommands) >> Just c
_ -> Nothing

-- | "fold" over the tutorials in sequence to determine which
-- commands are novel to each tutorial's solution.
computeCommandIntroductions :: [ScenarioInfoPair] -> [CoverageInfo]
computeCommandIntroductions =
reverse . tuts . foldl' f initial
where
initial = CommandAccum mempty mempty

f :: CommandAccum -> ScenarioInfoPair -> CommandAccum
f (CommandAccum encounteredPreviously xs) siPair =
CommandAccum updatedEncountered $ CoverageInfo usages novelCommands : xs
where
usages = extractCommandUsages siPair
usedCmdsForTutorial = solutionCommands usages

updatedEncountered = encounteredPreviously `S.union` usedCmdsForTutorial
novelCommands = usedCmdsForTutorial `S.difference` encounteredPreviously

-- | Extract the tutorials from the complete scenario collection
-- and derive their command coverage info.
generateIntroductionsSequence :: ScenarioCollection -> [CoverageInfo]
generateIntroductionsSequence =
computeCommandIntroductions . getTuts
where
getTuts =
concatMap flatten
. scenarioCollectionToList
. getTutorials

-- * Rendering functions

-- | Helper for standalone rendering.
-- For unit tests, can instead access the scenarios via the GameState.
loadScenarioCollection :: IO ScenarioCollection
loadScenarioCollection = simpleErrorHandle $ do
entities <- ExceptT loadEntities
(_, loadedScenarios) <- liftIO $ loadScenariosWithWarnings entities
return loadedScenarios

renderUsagesMarkdown :: Int -> CoverageInfo -> Text
renderUsagesMarkdown idx (CoverageInfo (TutorialInfo (s, si) _sCmds dCmds) novelCmds) =
T.unlines $
""
: firstLine
: "================"
: otherLines
where
otherLines =
concat
[ pure $ "`" <> T.pack (view scenarioPath si) <> "`"
, [""]
, pure $ "*" <> T.strip (view scenarioDescription s) <> "*"
, [""]
, renderSection "Commands introduced in this solution" $ renderCmds novelCmds
, [""]
, renderSection "Commands found in description" $ renderCmds dCmds
]

renderSection title content =
[title, "----------------"] <> content

renderCmds cmds =
pure $
if null cmds
then "<none>"
else commaList . map linkifyCommand . sort . map (T.pack . show) . S.toList $ cmds

linkifyCommand c = "[" <> c <> "](" <> commandsWikiPrefix <> c <> ")"

firstLine =
T.unwords
[ T.pack $ show idx <> ":"
, view scenarioName s
]

renderTutorialProgression :: IO Text
renderTutorialProgression =
render . generateIntroductionsSequence <$> loadScenarioCollection
where
render = T.unlines . zipWith renderUsagesMarkdown [0 ..]
5 changes: 1 addition & 4 deletions src/Swarm/Game/State.hs
Original file line number Diff line number Diff line change
Expand Up @@ -863,10 +863,7 @@ initGameState :: ExceptT Text IO ([SystemFailure], GameState)
initGameState = do
entities <- ExceptT loadEntities
recipes <- withExceptT prettyFailure $ loadRecipes entities
eitherLoadedScenarios <- liftIO $ runExceptT $ loadScenarios entities
let (scenarioWarnings, loadedScenarios) = case eitherLoadedScenarios of
Left xs -> (xs, SC mempty mempty)
Right (warnings, x) -> (warnings, x)
(scenarioWarnings, loadedScenarios) <- liftIO $ loadScenariosWithWarnings entities

(adjsFile, namesFile) <- withExceptT prettyFailure $ do
adjsFile <- getDataFileNameSafe NameGeneration "adjectives.txt"
Expand Down
4 changes: 3 additions & 1 deletion swarm.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ library
exposed-modules: Brick.Widget.List.Skippable
Data.BoolExpr.Simplify
Swarm.App
Swarm.DocGen
Swarm.Docs.Gen
Swarm.Docs.Pedagogy
Swarm.Game.Failure
Swarm.Game.Failure.Render
Swarm.Game.Achievement.Attainment
Expand Down Expand Up @@ -246,6 +247,7 @@ test-suite swarm-unit
other-modules: TestEval
TestInventory
TestModel
TestPedagogy
TestNotification
TestLanguagePipeline
TestPretty
Expand Down
4 changes: 2 additions & 2 deletions test/integration/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Data.Yaml (ParseException, prettyPrintParseException)
import Swarm.DocGen (EditorType (..))
import Swarm.DocGen qualified as DocGen
import Swarm.Docs.Gen (EditorType (..))
import Swarm.Docs.Gen qualified as DocGen
import Swarm.Game.CESK (emptyStore, initMachine)
import Swarm.Game.Entity (EntityMap, loadEntities, lookupByName)
import Swarm.Game.Robot (LogEntry, defReqs, equippedDevices, leText, machine, robotContext, robotLog, waitingUntil)
Expand Down
2 changes: 2 additions & 0 deletions test/unit/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import TestLSP (testLSP)
import TestLanguagePipeline (testLanguagePipeline)
import TestModel (testModel)
import TestNotification (testNotification)
import TestPedagogy (testPedagogy)
import TestPretty (testPrettyConst)
import Witch (from)

Expand All @@ -51,6 +52,7 @@ tests g =
, testCommands
, testEval g
, testModel
, testPedagogy g
, testInventory
, testNotification g
, testMisc
Expand Down
44 changes: 44 additions & 0 deletions test/unit/TestPedagogy.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Swarm pedagogical tests
module TestPedagogy where

import Control.Lens (view)
import Data.Set qualified as S
import Swarm.Docs.Pedagogy
import Swarm.Game.ScenarioInfo (scenarioPath)
import Swarm.Game.State
import Test.Tasty
import Test.Tasty.HUnit

testPedagogy :: GameState -> TestTree
testPedagogy gs =
testGroup
"Pedagogical soundness"
[ testGroup
"Introduce new commands in the description"
testList
]
where
tutorialInfos = generateIntroductionsSequence $ view scenarios gs

testFromTut :: Int -> CoverageInfo -> TestTree
testFromTut idx (CoverageInfo (TutorialInfo (_s, si) _ descCommands) novelCommands) =
testCase
(unwords [show idx, scPath])
$ assertBool errMsg allCommandsCovered
where
missingCmds = novelCommands `S.difference` descCommands
errMsg =
unwords
[ "command(s) missing from description:"
, show missingCmds
]

scPath = view scenarioPath si
allCommandsCovered = S.null missingCmds

testList = zipWith testFromTut [0 ..] tutorialInfos

0 comments on commit 71444bc

Please sign in to comment.