packages feed

scripths 0.5.0.0 → 0.5.1.0

raw patch · 12 files changed

+132/−20 lines, 12 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ ScriptHs.Parser: [metaExtraIncludeDirs] :: CabalMeta -> [Text]
+ ScriptHs.Parser: [metaExtraLibDirs] :: CabalMeta -> [Text]
- ScriptHs.Parser: CabalMeta :: [Text] -> [Text] -> [Text] -> [Text] -> [SourceRepoPin] -> [Text] -> CabalMeta
+ ScriptHs.Parser: CabalMeta :: [Text] -> [Text] -> [Text] -> [Text] -> [Text] -> [Text] -> [SourceRepoPin] -> [Text] -> CabalMeta
- ScriptHs.Run: renderCabalProject :: [FilePath] -> [SourceRepoPin] -> Text
+ ScriptHs.Run: renderCabalProject :: [FilePath] -> [SourceRepoPin] -> [Text] -> [Text] -> Text

Files

README.md view
@@ -130,6 +130,44 @@  `OverloadedStrings` is enabled in every scripths repl by default, so string literals work directly as `Text` / `ByteString`; add any further extensions with `default-extensions`. +## The `-- compile` directive++A notebook cell (or script) can be marked **compiled**:++```+-- compile+```++or, naming the generated module explicitly:++```+-- compile: Training.Core+```++The directive declares that the cell's contents are *module-level+declarations* destined for a generated Haskell module rather than the GHCi+prompt — a host environment (such as [Sabela](https://github.com/mchav/sabela))+writes those declarations into a module, loads it with `:load` under+`-fobject-code -O2`, and gets native compiled code instead of interpreted+bytecode. Cells sharing a module name merge into one module; bare+`-- compile` cells share a default module.++What scripths provides:++- `ScriptHs.Parser` recognises the directive (`scriptCompile` on+  `ScriptFile`) and offers `parseScriptNumbered`, a line-number-preserving+  parse.+- `ScriptHs.Compiled` validates that a cell is declarations-only+  (`checkCompilable`) and renders the generated module+  (`renderCompiledModule`), preceding every unit with a+  `{-# LINE n "sabela-cell-<id>" #-}` pragma so GHC diagnostics map back to+  the originating cell and line (`linePragmaTag` / `parseLinePragmaTag`).++Under the **scripths CLI** the directive is currently a graceful no-op: it+parses as an ordinary comment and the cell runs interpreted, producing the+same results (just slower). Standalone compiled execution in the CLI may+come later.+ ## Local packages  By default `build-depends` resolve from Hackage. To build a script or notebook
scripths.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               scripths-version:            0.5.0.0+version:            0.5.1.0 synopsis:           GHCi scripts for standalone execution and Markdown documentation. description:        GHCi scripts for standalone execution (with dependency resolution) and Markdown documentation (produces inline output). homepage:           https://www.datahaskell.org/
src/ScriptHs/Compiled.hs view
@@ -25,7 +25,13 @@ import qualified Data.Text.Read as TR  import ScriptHs.Parser (Line (..))-import ScriptHs.Render (Kind (..), Piece (..), lineText, toPieces, unRewriteSplice)+import ScriptHs.Render (+    Kind (..),+    Piece (..),+    lineText,+    toPieces,+    unRewriteSplice,+ )  -- | One cell's contribution to a generated module. data CellChunk = CellChunk
src/ScriptHs/Notebook.hs view
@@ -68,7 +68,8 @@     let ghciScript0 = generatedMarkedScript "" codeBlocks     nonce <- makeNonce ghciScript0     let ghciScript = generatedMarkedScript nonce codeBlocks-        sf = ScriptFile{scriptMeta = meta, scriptCompile = Nothing, scriptLines = ghciScript}+        sf =+            ScriptFile{scriptMeta = meta, scriptCompile = Nothing, scriptLines = ghciScript}     rawOutput <- runScriptCapture opts notebookPath sf     let indices = map fst codeBlocks         outputs =
src/ScriptHs/Parser.hs view
@@ -72,6 +72,10 @@     -- ^ Extensions from @default-extensions@ directives.     , metaGhcOptions :: [Text]     -- ^ Flags from @ghc-options@ directives.+    , metaExtraLibDirs :: [Text]+    -- ^ Native library search paths from @extra-lib-dirs@ directives.+    , metaExtraIncludeDirs :: [Text]+    -- ^ C header search paths from @extra-include-dirs@ directives.     , metaPackages :: [Text]     {- ^ Extra local package dirs from @packages@ directives, resolved relative     to the script file (lets a notebook depend on a second, non-enclosing local@@ -125,10 +129,10 @@ Example:  >>> parseScript "-- cabal: build-depends: text\nimport Data.Text (Text)\n"-ScriptFile {scriptMeta = CabalMeta {metaDeps = ["text"], metaExts = [], metaGhcOptions = [], metaPackages = [], metaSourceRepos = [], metaUnknownKeys = []}, scriptLines = [Import "import Data.Text (Text)"]}+ScriptFile {scriptMeta = CabalMeta {metaDeps = ["text"], metaExts = [], metaGhcOptions = [], metaExtraLibDirs = [], metaExtraIncludeDirs = [], metaPackages = [], metaSourceRepos = [], metaUnknownKeys = []}, scriptLines = [Import "import Data.Text (Text)"]}  >>> parseScript ""-ScriptFile {scriptMeta = CabalMeta {metaDeps = [], metaExts = [], metaGhcOptions = [], metaPackages = [], metaSourceRepos = [], metaUnknownKeys = []}, scriptLines = []}+ScriptFile {scriptMeta = CabalMeta {metaDeps = [], metaExts = [], metaGhcOptions = [], metaExtraLibDirs = [], metaExtraIncludeDirs = [], metaPackages = [], metaSourceRepos = [], metaUnknownKeys = []}, scriptLines = []} -} parseScript :: Text -> ScriptFile parseScript = fst . parseScriptNumbered@@ -180,6 +184,8 @@         { metaDeps = concatMap metaDeps ms         , metaExts = concatMap metaExts ms         , metaGhcOptions = concatMap metaGhcOptions ms+        , metaExtraLibDirs = concatMap metaExtraLibDirs ms+        , metaExtraIncludeDirs = concatMap metaExtraIncludeDirs ms         , metaPackages = concatMap metaPackages ms         , metaSourceRepos = concatMap metaSourceRepos ms         , metaUnknownKeys = concatMap metaUnknownKeys ms@@ -216,6 +222,8 @@                 "build-depends" -> emptyCabal{metaDeps = items}                 "default-extensions" -> emptyCabal{metaExts = items}                 "ghc-options" -> emptyCabal{metaGhcOptions = items}+                "extra-lib-dirs" -> emptyCabal{metaExtraLibDirs = items}+                "extra-include-dirs" -> emptyCabal{metaExtraIncludeDirs = items}                 "packages" -> emptyCabal{metaPackages = items}                 "source-repository-package" ->                     emptyCabal{metaSourceRepos = maybeToList (parseSourceRepo value)}@@ -227,6 +235,8 @@             { metaDeps = []             , metaExts = []             , metaGhcOptions = []+            , metaExtraLibDirs = []+            , metaExtraIncludeDirs = []             , metaPackages = []             , metaSourceRepos = []             , metaUnknownKeys = []
src/ScriptHs/Render.hs view
@@ -496,11 +496,15 @@         ["{- cabal:", "build-depends: " <> commaList (dedup ("base" : metaDeps meta))]             ++ ["default-extensions: " <> commaList exts' | not (null exts')]             ++ ["ghc-options: " <> T.unwords opts']+            ++ ["extra-lib-dirs: " <> commaList libDirs | not (null libDirs)]+            ++ ["extra-include-dirs: " <> commaList incDirs | not (null incDirs)]             ++ map renderRepo (metaSourceRepos meta)             ++ ["-}"]   where     exts' = dedup (metaExts meta)     opts' = dedup (metaGhcOptions meta ++ ["-Wno-unused-imports"])+    libDirs = dedup (metaExtraLibDirs meta)+    incDirs = dedup (metaExtraIncludeDirs meta)     commaList = T.intercalate ", "     renderRepo r =         T.unwords $
src/ScriptHs/Run.hs view
@@ -176,7 +176,12 @@     let localPkgs = nub (roPackages opts ++ metaPkgDirs ++ enclosing)     writeManagedCabalProject         (projectDir </> "cabal.project")-        (renderCabalProject localPkgs (metaSourceRepos meta))+        ( renderCabalProject+            localPkgs+            (metaSourceRepos meta)+            (metaExtraLibDirs meta)+            (metaExtraIncludeDirs meta)+        )     -- A local package only imports if its name is also a build-depend; collect     -- the names so renderCabalFile can add them (and warn on dirs without one).     localNames <- localPackageNames localPkgs@@ -227,13 +232,29 @@ {- | Render a @cabal.project@ from local package dirs + git source-repo pins. Always includes @.@ (the synthetic script package). -}-renderCabalProject :: [FilePath] -> [SourceRepoPin] -> T.Text-renderCabalProject localPkgs repos =++{- | The @cabal.project@. @extra-lib-dirs@\/@extra-include-dirs@ from cell+metadata go into a @package *@ stanza (not the executable's build-info), because+build-info dirs are not applied when cabal configures a /dependency/ — a native+library like OpenCASCADE is found only when its own package gets the search path.+-}+renderCabalProject ::+    [FilePath] -> [SourceRepoPin] -> [T.Text] -> [T.Text] -> T.Text+renderCabalProject localPkgs repos libDirs incDirs =     T.unlines $         [managedSentinel, "packages: ."]             ++ map (\p -> "          " <> T.pack p) localPkgs             ++ concatMap renderRepo repos+            ++ packageStar   where+    packageStar+        | null libDirs && null incDirs = []+        | otherwise =+            ["package *"]+                ++ ["    extra-lib-dirs: " <> T.intercalate ", " libDirs | not (null libDirs)]+                ++ [ "    extra-include-dirs: " <> T.intercalate ", " incDirs+                   | not (null incDirs)+                   ]     renderRepo r =         [ "source-repository-package"         , "    type: git"
test/Test/Compiled.hs view
@@ -16,8 +16,8 @@  ) import ScriptHs.Parser (     CompileDirective (..),-    ScriptFile (scriptCompile, scriptLines),     Line (..),+    ScriptFile (scriptCompile, scriptLines),     parseScript,     parseScriptNumbered,  )@@ -132,7 +132,8 @@     testGroup         "renderCompiledModule"         [ testCase "module header and decl with LINE pragma" $ do-            let src = renderCompiledModule "Training" [] [] [chunk 7 "f :: Int -> Int\nf x = x + 1\n"]+            let src =+                    renderCompiledModule "Training" [] [] [chunk 7 "f :: Int -> Int\nf x = x + 1\n"]             assertContains src "module Training where"             assertContains src ("{-# LINE 1 \"" <> linePragmaTag 7 <> "\" #-}")             assertContains src "f x = x + 1"
test/Test/Parser.hs view
@@ -12,6 +12,8 @@ import ScriptHs.Parser (     CabalMeta (         metaDeps,+        metaExtraIncludeDirs,+        metaExtraLibDirs,         metaExts,         metaGhcOptions,         metaPackages,@@ -114,6 +116,12 @@             , testCase "packages directive lists extra local package dirs" $ do                 let sf = parseScript "-- cabal: packages: ../th, ../persistent\n"                 (metaPackages . scriptMeta) sf @?= ["../th", "../persistent"]+            , testCase "extra-lib-dirs directive lists native library search paths" $ do+                let sf = parseScript "-- cabal: extra-lib-dirs: /opt/homebrew/lib, /usr/local/lib\n"+                (metaExtraLibDirs . scriptMeta) sf @?= ["/opt/homebrew/lib", "/usr/local/lib"]+            , testCase "extra-include-dirs directive lists C header search paths" $ do+                let sf = parseScript "-- cabal: extra-include-dirs: /opt/homebrew/include\n"+                (metaExtraIncludeDirs . scriptMeta) sf @?= ["/opt/homebrew/include"]             , testCase "metadata stripped from lines" $ do                 let input =                         T.unlines
test/Test/Render.hs view
@@ -407,7 +407,8 @@             assertBool "has main = do" (T.isInfixOf "main = do" txt)             assertBool "indents print 1" (T.isInfixOf "    print 1" txt)         , testCase "cabal-script header includes base + Wno-unused-imports" $ do-            let hdr = renderCabalScriptHeader (CabalMeta ["dataframe", "text"] [] [] [] [] [])+            let hdr =+                    renderCabalScriptHeader (CabalMeta ["dataframe", "text"] [] [] [] [] [] [] [])             assertBool "opens block" (T.isInfixOf "{- cabal:" hdr)             assertBool                 "base + deps"
test/Test/Run.hs view
@@ -1,5 +1,6 @@ module Test.Run (runTests) where +import Data.Char (isAsciiLower, isDigit) import Data.List (isInfixOf, isPrefixOf) import qualified Data.Text as T import Test.Tasty (TestTree, testGroup)@@ -15,7 +16,7 @@  )  emptyMeta :: CabalMeta-emptyMeta = CabalMeta [] [] [] [] [] []+emptyMeta = CabalMeta [] [] [] [] [] [] [] []  runTests :: TestTree runTests =@@ -36,7 +37,7 @@                 $ do                     let args = cabalArgs "/proj" "/proj/script.ghci"                         eTerm = last (filter ("--repl-option=" `isPrefixOf`) args)-                    assertBool "no space in terminator" (not (' ' `elem` eTerm))+                    assertBool "no space in terminator" (' ' `notElem` eTerm)             , testCase "includes --project-dir" $ do                 let args = cabalArgs "/proj" "/proj/script.ghci"                 assertBool "--project-dir present" $@@ -45,20 +46,40 @@         , testGroup             "renderCabalProject"             [ testCase "base case has the managed sentinel and packages: ." $ do-                let txt = renderCabalProject [] []+                let txt = renderCabalProject [] [] [] []                 assertBool "sentinel" (T.isInfixOf "managed by scripths" txt)                 assertBool "packages ." (T.isInfixOf "packages: ." txt)             , testCase "local package dirs appear as packages entries" $ do-                let txt = renderCabalProject ["/abs/granite"] []+                let txt = renderCabalProject ["/abs/granite"] [] [] []                 assertBool "abs path" (T.isInfixOf "/abs/granite" txt)             , testCase "git pin renders a source-repository-package stanza" $ do                 let pin = SourceRepoPin "https://x/repo" "abc123" (Just "sub")-                    txt = renderCabalProject [] [pin]+                    txt = renderCabalProject [] [pin] [] []                 assertBool "stanza" (T.isInfixOf "source-repository-package" txt)                 assertBool "type" (T.isInfixOf "type: git" txt)                 assertBool "location" (T.isInfixOf "location: https://x/repo" txt)                 assertBool "tag" (T.isInfixOf "tag: abc123" txt)                 assertBool "subdir" (T.isInfixOf "subdir: sub" txt)+            , testCase "no package * stanza when no extra dirs are declared" $ do+                let txt = renderCabalProject [] [] [] []+                assertBool "no package *" (not (T.isInfixOf "package *" txt))+            , testCase "extra dirs render a package * stanza (applies to deps)" $ do+                let txt =+                        renderCabalProject+                            []+                            []+                            ["/opt/homebrew/opt/opencascade/lib"]+                            ["/opt/homebrew/opt/opencascade/include/opencascade"]+                assertBool "package * header" (T.isInfixOf "package *" txt)+                assertBool+                    "lib dir"+                    (T.isInfixOf "    extra-lib-dirs: /opt/homebrew/opt/opencascade/lib" txt)+                assertBool+                    "inc dir"+                    ( T.isInfixOf+                        "    extra-include-dirs: /opt/homebrew/opt/opencascade/include/opencascade"+                        txt+                    )             ]         , testGroup             "deriveProjectName"@@ -80,7 +101,7 @@                 let n = deriveProjectName "/проект/данные.md"                 assertBool                     "ASCII only"-                    (all (\c -> c == '-' || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) n)+                    (all (\c -> c == '-' || isAsciiLower c || isDigit c) n)             , testCase "an all-symbol path still yields a valid name" $                 assertBool                     "fallback stem"
test/Test/Version.hs view
@@ -1,5 +1,6 @@ module Test.Version (versionTests) where +import Data.Maybe (isJust) import qualified Data.Text as T import Data.Version (makeVersion) import Test.Tasty (TestTree, testGroup)@@ -83,7 +84,7 @@             parseTagLine NotebookTag "# Title" @?= Nothing         , testCase "an over-long component clamps instead of silently overflowing Int" $ do             let huge = parseTagLine NotebookTag "<!-- scripths: 99999999999999999999999999.0 -->"-            assertBool "still parses" (huge /= Nothing)+            assertBool "still parses" (isJust huge)             assertBool "stays larger than a normal version (no wrap)" $                 maybe False (> makeVersion [1, 0]) huge         , testCase "leading zeros in a component are tolerated" $@@ -185,7 +186,7 @@             assertBool "body intact" ("# Body" `T.isInfixOf` out)             assertBool "single tag" (T.count "<!-- scripths:" out == 1)             assertBool "tag is the first line" $-                maybe False (const True) (T.stripPrefix (versionTag NotebookTag) out)+                isJust (T.stripPrefix (versionTag NotebookTag) out)         ]   where     -- Text after the second "---" line, where the tag must live.@@ -200,7 +201,7 @@         [ testCase "newer => warns" $             assertBool                 "warns"-                (maybe False (const True) (newerVersionWarning (makeVersion [99, 0])))+                (isJust (newerVersionWarning (makeVersion [99, 0])))         , testCase "current => silent" $             newerVersionWarning scripthsVersion @?= Nothing         , testCase "older => silent" $