diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for scripths
 
+## 0.3.1.0 -- 2026-04-23
+
+* Better block parsing when there are function signatures and comments.
+
 ## 0.3.0.1 -- 2026-03-14
 
 * improve block parsing
diff --git a/scripths.cabal b/scripths.cabal
--- a/scripths.cabal
+++ b/scripths.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               scripths
-version:            0.3.0.1
+version:            0.3.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/
diff --git a/src/ScriptHs/Render.hs b/src/ScriptHs/Render.hs
--- a/src/ScriptHs/Render.hs
+++ b/src/ScriptHs/Render.hs
@@ -1,15 +1,19 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 {- | Rendering 'ScriptHs.Parser.Line' sequences into GHCi-compatible scripts.
 
 GHCi imposes constraints that raw Haskell source does not: multi-line
-definitions must be wrapped in @:{@ \/ @:}@ blocks, and monadic binds
-(@<-@) or Template Haskell splices (@$(@) must be issued as individual
-statements rather than grouped with pure definitions. 'toGhciScript'
-handles all of this automatically
+definitions must be wrapped in @:{@ \/ @:}@ blocks, and each
+expression-statement or monadic bind (@<-@) must be a separate GHCi
+statement. 'toGhciScript' groups input lines into logical units
+(a lead non-indented line plus its indented continuations), classifies
+each unit, then assembles blocks accordingly.
 -}
 module ScriptHs.Render (
     toGhciScript,
 ) where
 
+import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
 import Data.Text (Text)
 import qualified Data.Text as T
 import ScriptHs.Parser (Line (..))
@@ -21,142 +25,250 @@
 
 {- | Render a list of 'Line's as a GHCi script.
 
-Lines are grouped into blocks and wrapped in @:{@ \/ @:}@ where necessary.
-Monadic bind expressions (@<-@) and Template Haskell splices (@$(@) are
-always emitted as individual GHCi statements, since GHCi does not allow
-them inside multi-line blocks.
+Classification rules:
 
-Example — a multi-line definition is wrapped in a single block:
+* A run of @--@ comment lines forms a 'KComment' unit that attaches
+  forward to the next non-comment unit.
+* A type signature, value binding, or clause-head-with-guards forms a
+  'KDeclaration' unit; consecutive declarations merge into a single
+  @:{ … :}@ block.
+* A monadic bind (@<-@ on the lead line) forms a 'KIOBind' unit; each
+  such unit is its own block.
+* A Template Haskell splice (@$(…)@ or the rewritten @_ = (); …@ form)
+  forms a 'KTHSplice' unit; each is its own block.
+* Anything else is a 'KAction' (expression-statement, @do@-block, etc.);
+  each action unit becomes its own block.
+-}
+toGhciScript :: [Line] -> Text
+toGhciScript = T.unlines . concatMap renderBlock . piecesToBlocks . toPieces
 
-@
-toGhciScript
-  [ HaskellLine "double :: Int -> Int"
-  , HaskellLine "double = (*2)"
-  ]
--- :{
--- double :: Int -> Int
--- double = (*2)
--- :}
-@
+---------------------------------------------------------------
+-- Kind and Piece
+---------------------------------------------------------------
 
-Example — an IO bind is kept as a standalone statement:
+data Kind
+    = KComment
+    | KDeclaration
+    | KAction
+    | KIOBind
+    | KTHSplice
+    deriving (Show, Eq)
 
-@
-toGhciScript
-  [ HaskellLine "x <- getLine"
-  , HaskellLine "putStrLn x"
-  ]
--- x <- getLine
--- putStrLn x
-@
+data Piece
+    = PBlank
+    | PGhciCommand Text
+    | PPragma Text
+    | PImport Text
+    | PUnit Kind [Line]
+    deriving (Show)
+
+---------------------------------------------------------------
+-- Step 1: [Line] -> [Piece]
+---------------------------------------------------------------
+
+toPieces :: [Line] -> [Piece]
+toPieces [] = []
+toPieces (Blank : rest) = PBlank : toPieces rest
+toPieces (GhciCommand t : rest) = PGhciCommand t : toPieces rest
+toPieces (Pragma t : rest) = PPragma t : toPieces rest
+toPieces (Import t : rest) = PImport t : toPieces rest
+toPieces (HaskellLine t : rest)
+    | isCommentText t =
+        let (more, rest') = spanComments rest
+            unit = HaskellLine t : more
+         in PUnit KComment unit : toPieces rest'
+    | otherwise =
+        let (cont, rest') = takeContinuations rest
+            unit = HaskellLine t : cont
+            k = classify t (map lineText cont)
+         in PUnit k unit : toPieces rest'
+
+spanComments :: [Line] -> ([Line], [Line])
+spanComments (HaskellLine t : rest)
+    | isCommentText t =
+        let (more, rest') = spanComments rest
+         in (HaskellLine t : more, rest')
+spanComments xs = ([], xs)
+
+{- | Take indented continuations after a lead, absorbing interior blank
+lines only when followed by more indented non-blank content. Trailing
+blanks are left behind so they can serve as unit separators (which is
+what makes the "double blank still breaks" case work).
 -}
-toGhciScript :: [Line] -> Text
-toGhciScript = T.unlines . concatMap renderBlock . groupBlocks
+takeContinuations :: [Line] -> ([Line], [Line])
+takeContinuations [] = ([], [])
+takeContinuations xs@(l : rest)
+    | isIndentedNonBlank l =
+        let (more, rest') = takeContinuations rest
+         in (l : more, rest')
+    | isBlankLine l =
+        let (blanks, afterBlanks) = span isBlankLine xs
+         in case afterBlanks of
+                (x : _)
+                    | isIndentedNonBlank x ->
+                        let (more, rest') = takeContinuations afterBlanks
+                         in (blanks ++ more, rest')
+                _ -> ([], xs)
+    | otherwise = ([], xs)
 
-groupBlocks :: [Line] -> [Block]
-groupBlocks = concatMap splitIOBinds . groupRaw
+isIndentedNonBlank :: Line -> Bool
+isIndentedNonBlank (HaskellLine t) = T.isPrefixOf " " t || T.isPrefixOf "\t" t
+isIndentedNonBlank _ = False
 
-groupRaw :: [Line] -> [Block]
-groupRaw [] = []
-groupRaw (Blank : rest) = SingleLine Blank : groupRaw rest
-groupRaw (GhciCommand t : rest) = SingleLine (GhciCommand t) : groupRaw rest
-groupRaw ls =
-    let (block, rest) = span isBlockLine ls
-        (block', rest') = takeIfIndented block rest
-     in classifyBlock block' : groupRaw rest'
+isBlankLine :: Line -> Bool
+isBlankLine Blank = True
+isBlankLine _ = False
 
-takeIfIndented :: [Line] -> [Line] -> ([Line], [Line])
-takeIfIndented block rest = (block ++ takeWhile isIndented rest, dropWhile isIndented rest)
+---------------------------------------------------------------
+-- Classification
+---------------------------------------------------------------
 
-isIndented :: Line -> Bool
-isIndented Blank = True
-isIndented (HaskellLine t) = T.isPrefixOf " " t || T.isPrefixOf "\t" t
-isIndented _ = False
+classify :: Text -> [Text] -> Kind
+classify leadText contTexts
+    | isTHSplice leadText = KTHSplice
+    | isDeclaration leadText contTexts = KDeclaration
+    | isIOBindLead leadText = KIOBind
+    | otherwise = KAction
 
-splitIOBinds :: Block -> [Block]
-splitIOBinds (MultiLine ls) =
-    concatMap (splitDefIO . classifyBlock) (splitOn isIOLine ls)
-  where
-    isIOLine l = isIOorTH (lineText l)
-splitIOBinds b = [b]
+isTHSplice :: Text -> Bool
+isTHSplice t =
+    let s = T.stripStart t
+     in "$(" `T.isPrefixOf` s || "_ = ();" `T.isPrefixOf` s
 
-{- | Split a block that mixes definitions and IO actions.
-  Only splits when there's a clear boundary: a non-indented line
-  without @=@ or @::@ following a line that has @=@ or @::@, or vice versa.
-  Indented lines always attach to the preceding line's group.
+isIOBindLead :: Text -> Bool
+isIOBindLead = T.isInfixOf "<-"
+
+isDeclaration :: Text -> [Text] -> Bool
+isDeclaration leadText contTexts =
+    startsWithDeclKeyword leadText
+        || isTypeSig leadText
+        || isValueBinding leadText
+        || isClauseHead leadText && any contHasBinding contTexts
+
+startsWithDeclKeyword :: Text -> Bool
+startsWithDeclKeyword t =
+    any
+        (`T.isPrefixOf` T.stripStart t)
+        ["data ", "newtype ", "type ", "class ", "instance ", "default "]
+
+isTypeSig :: Text -> Bool
+isTypeSig t = case afterLeadIdent t of
+    Just rest -> "::" `T.isPrefixOf` T.stripStart rest
+    Nothing -> False
+
+isValueBinding :: Text -> Bool
+isValueBinding t = maybe False hasTopLevelEquals (afterLeadIdent t)
+
+{- | A line like @isPrime n@ — identifier + args, no @=@ / @::@ / @<-@ on
+this line. Only counts as a declaration when accompanied by indented
+continuations that supply the RHS (guards, a @where@ clause, etc.).
 -}
-splitDefIO :: Block -> [Block]
-splitDefIO (MultiLine ls)
-    | hasMix ls = map classifyBlock (groupByKind ls)
-    | otherwise = [MultiLine ls]
-splitDefIO b = [b]
+isClauseHead :: Text -> Bool
+isClauseHead t = case afterLeadIdent t of
+    Just rest ->
+        let rest' = T.stripEnd (T.stripStart rest)
+         in not (T.null rest')
+                && not (hasTopLevelEquals rest)
+                && not ("::" `T.isInfixOf` rest)
+                && not ("<-" `T.isInfixOf` rest)
+    Nothing -> False
 
--- | Check if a block has both definitions and IO actions at the top level.
-hasMix :: [Line] -> Bool
-hasMix ls =
-    let topLevel = filter (not . isIndented) ls
-        defs = filter (isDef . lineText) topLevel
-        actions = filter (not . isDef . lineText) topLevel
-     in not (null defs) && not (null actions)
+contHasBinding :: Text -> Bool
+contHasBinding t =
+    let s = T.stripStart t
+     in "| " `T.isPrefixOf` s && hasTopLevelEquals s
+            || hasTopLevelEquals t
+            || "where" `T.isPrefixOf` s
 
-groupByKind :: [Line] -> [[Line]]
-groupByKind [] = []
-groupByKind (l : ls) =
-    let kind = lineKindOf l
-        -- Collect lines of the same kind, plus any indented continuations
-        (same, rest) = spanSameKind kind ls
-     in (l : same) : groupByKind rest
+{- | Parse a leading lowercase identifier. Returns the text that follows
+it (with any leading whitespace) or 'Nothing' if the stripped line does
+not begin with a lowercase identifier, or begins with a Haskell keyword.
+-}
+afterLeadIdent :: Text -> Maybe Text
+afterLeadIdent t =
+    let s = T.stripStart t
+     in case T.uncons s of
+            Just (c, _)
+                | isIdentStart c ->
+                    let (ident, rest) = T.span isIdentCont s
+                     in if T.null ident || isHaskellKeyword ident
+                            then Nothing
+                            else Just rest
+            _ -> Nothing
 
-spanSameKind :: Bool -> [Line] -> ([Line], [Line])
-spanSameKind _ [] = ([], [])
-spanSameKind kind (l : ls)
-    | isIndented l =
-        -- Indented lines attach to the current group
-        let (more, rest) = spanSameKind kind ls
-         in (l : more, rest)
-    | lineKindOf l == kind =
-        let (more, rest) = spanSameKind kind ls
-         in (l : more, rest)
-    | otherwise = ([], l : ls)
+isIdentStart :: Char -> Bool
+isIdentStart c = c == '_' || isAsciiLower c
 
--- | True if the line looks like a definition (has @=@ or @::@).
-lineKindOf :: Line -> Bool
-lineKindOf l = isDef (lineText l)
+isIdentCont :: Char -> Bool
+isIdentCont c =
+    isIdentStart c
+        || isAsciiUpper c
+        || isDigit c
+        || c == '\''
 
-isDef :: Text -> Bool
-isDef t =
-    hasTopLevelEquals t || " :: " `T.isInfixOf` t
+isHaskellKeyword :: Text -> Bool
+isHaskellKeyword t =
+    t
+        `elem` [ "do"
+               , "let"
+               , "in"
+               , "if"
+               , "then"
+               , "else"
+               , "case"
+               , "of"
+               , "where"
+               , "data"
+               , "newtype"
+               , "type"
+               , "class"
+               , "instance"
+               , "module"
+               , "import"
+               , "default"
+               , "deriving"
+               , "infix"
+               , "infixl"
+               , "infixr"
+               ]
 
 hasTopLevelEquals :: Text -> Bool
-hasTopLevelEquals t =
-    " = " `T.isInfixOf` t || T.isSuffixOf " =" t
+hasTopLevelEquals t = " = " `T.isInfixOf` t || T.isSuffixOf " =" t
 
-splitOn :: (a -> Bool) -> [a] -> [[a]]
-splitOn _ [] = []
-splitOn p (x : xs)
-    | p x = [x] : splitOn p xs
-    | otherwise =
-        let (run, rest) = break p xs
-         in (x : run) : splitOn p rest
+isCommentText :: Text -> Bool
+isCommentText t = "--" `T.isPrefixOf` T.stripStart t
 
-isBlockLine :: Line -> Bool
-isBlockLine Blank = False
-isBlockLine (GhciCommand _) = False
-isBlockLine _ = True
+---------------------------------------------------------------
+-- Step 2: [Piece] -> [Block]
+---------------------------------------------------------------
 
-classifyBlock :: [Line] -> Block
-classifyBlock [l] = SingleLine l
-classifyBlock ls = MultiLine ls
+piecesToBlocks :: [Piece] -> [Block]
+piecesToBlocks [] = []
+piecesToBlocks (PBlank : rest) = SingleLine Blank : piecesToBlocks rest
+piecesToBlocks (PGhciCommand t : rest) = SingleLine (GhciCommand t) : piecesToBlocks rest
+piecesToBlocks (PPragma t : rest) = SingleLine (Pragma t) : piecesToBlocks rest
+piecesToBlocks (PImport t : rest) = SingleLine (Import t) : piecesToBlocks rest
+piecesToBlocks (PUnit KComment lines1 : PUnit k lines2 : rest)
+    | k /= KComment =
+        piecesToBlocks (PUnit k (lines1 ++ lines2) : rest)
+piecesToBlocks (PUnit KDeclaration lines1 : PUnit KDeclaration lines2 : rest) =
+    piecesToBlocks (PUnit KDeclaration (lines1 ++ lines2) : rest)
+piecesToBlocks (PUnit _ ls : rest) = toBlock ls : piecesToBlocks rest
+  where
+    toBlock [l] = SingleLine l
+    toBlock xs = MultiLine xs
 
+---------------------------------------------------------------
+-- Rendering
+---------------------------------------------------------------
+
 renderBlock :: Block -> [Text]
 renderBlock (SingleLine Blank) = [""]
 renderBlock (SingleLine (GhciCommand t)) = [t]
 renderBlock (SingleLine (Pragma t)) = [t]
 renderBlock (SingleLine (Import t)) = [t]
 renderBlock (SingleLine (HaskellLine t)) = wrapMulti [t]
-renderBlock (MultiLine ls)
-    | allIOorTH ls = concatMap (\l -> wrapMulti [lineText l]) ls
-    | otherwise = wrapMulti (map lineText ls)
+renderBlock (MultiLine ls) = wrapMulti (map lineText ls)
 
 wrapMulti :: [Text] -> [Text]
 wrapMulti ls = [":{"] ++ ls ++ [":}"]
@@ -167,14 +279,3 @@
 lineText (Pragma t) = t
 lineText (Import t) = t
 lineText (HaskellLine t) = t
-
-isIOorTH :: Text -> Bool
-isIOorTH t =
-    not (T.isPrefixOf " " t)
-        && ( T.isInfixOf "<-" t
-                || T.isInfixOf "$(" t
-                || T.isPrefixOf "_ = ();" (T.stripStart t)
-           )
-
-allIOorTH :: [Line] -> Bool
-allIOorTH = all (isIOorTH . lineText)
diff --git a/test/Test/Render.hs b/test/Test/Render.hs
--- a/test/Test/Render.hs
+++ b/test/Test/Render.hs
@@ -13,7 +13,7 @@
     testGroup
         "Render"
         [ testGroup
-            "Single lines"
+            "Line-level rendering"
             [ testCase "plain expression stays unwrapped" $ do
                 let result = toGhciScript [HaskellLine "print 42"]
                 assertWrapped result ["print 42"]
@@ -21,12 +21,11 @@
                 let result = toGhciScript [Import "import Data.Text"]
                 assertNotWrapped result
             , testCase "pragma stays unwrapped" $ do
-                let result = toGhciScript [Import "{-# LANGUAGE GADTs #-}"]
+                let result = toGhciScript [Pragma "{-# LANGUAGE GADTs #-}"]
                 assertNotWrapped result
             , testCase "ghci command stays unwrapped" $ do
                 let result = toGhciScript [GhciCommand ":set -XOverloadedStrings"]
-                let ls = nonEmpty result
-                ls @?= [":set -XOverloadedStrings"]
+                nonEmpty result @?= [":set -XOverloadedStrings"]
             , testCase "IO bind gets wrapped" $ do
                 let result = toGhciScript [HaskellLine "x <- getLine"]
                 assertWrapped result ["x <- getLine"]
@@ -35,17 +34,37 @@
                 assertBool "has blank" (T.isInfixOf "\n\n" result || result == "\n")
             ]
         , testGroup
-            "IO line isolation"
+            "Isolation: one block per statement"
             [ testCase "consecutive IO binds each get own block" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "x <- getLine"
                             , HaskellLine "y <- getLine"
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ("expected 2 blocks, got " ++ show (length blocks))
-                    (length blocks == 2)
+                length (splitBlocks result) @?= 2
+            , testCase "consecutive IO expression-statements each get own block" $ do
+                let result =
+                        toGhciScript
+                            [ HaskellLine "print 1"
+                            , HaskellLine "putStrLn \"hi\""
+                            , HaskellLine "displayLatex \"hi\""
+                            , HaskellLine "displayLatex \"hi\""
+                            ]
+                length (splitBlocks result) @?= 4
+            , testCase "inline type annotation does not flip statement into declaration" $ do
+                let result =
+                        toGhciScript
+                            [ HaskellLine "print (1 :: Int)"
+                            , HaskellLine "print \"x\""
+                            ]
+                length (splitBlocks result) @?= 2
+            , testCase "TH splice gets its own block" $ do
+                let result =
+                        toGhciScript
+                            [ HaskellLine "$(deriveJSON defaultOptions ''Foo)"
+                            , HaskellLine "print \"after\""
+                            ]
+                length (splitBlocks result) @?= 2
             , testCase "IO bind between pure code splits correctly" $ do
                 let result =
                         toGhciScript
@@ -67,8 +86,7 @@
                             , HaskellLine "  x = 5"
                             , HaskellLine "  y = 10"
                             ]
-                let blocks = splitBlocks result
-                length blocks @?= 1
+                length (splitBlocks result) @?= 1
             , testCase "consecutive do-notation lines grouped" $ do
                 let result =
                         toGhciScript
@@ -77,21 +95,18 @@
                             , HaskellLine "  y <- pure 10"
                             , HaskellLine "  pure $ x + y"
                             ]
-                let blocks = splitBlocks result
-                length blocks @?= 1
+                length (splitBlocks result) @?= 1
             , testCase "consecutive do-notation lines with space are grouped" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "do"
                             , HaskellLine "  x <- pure 5"
-                            , -- indentation should ignore these blank lines.
-                              Blank
                             , Blank
+                            , Blank
                             , HaskellLine "  y <- pure 10"
                             , HaskellLine "  pure $ x + y"
                             ]
-                let blocks = splitBlocks result
-                length blocks @?= 1
+                length (splitBlocks result) @?= 1
             , testCase "blank separates blocks" $ do
                 let result =
                         toGhciScript
@@ -121,7 +136,7 @@
                 assertBool "has print" (T.isInfixOf "print iris" result)
             ]
         , testGroup
-            "Continuation across blanks (don't break)"
+            "Continuation across blanks"
             [ testCase "blank between independent IO statements still separates" $ do
                 let result =
                         toGhciScript
@@ -129,10 +144,7 @@
                             , Blank
                             , HaskellLine "putStrLn \"b\""
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ("expected 2 blocks, got " ++ show (length blocks))
-                    (length blocks == 2)
+                length (splitBlocks result) @?= 2
             , testCase "blank between imports still separates" $ do
                 let result =
                         toGhciScript
@@ -140,13 +152,9 @@
                             , Blank
                             , Import "import Data.Map"
                             ]
-                -- Both imports should appear
                 assertBool "has Text" (T.isInfixOf "Data.Text" result)
                 assertBool "has Map" (T.isInfixOf "Data.Map" result)
-            ]
-        , testGroup
-            "Continuation across blanks (new behavior)"
-            [ testCase "blank before where stays in same block" $ do
+            , testCase "blank before where stays in same block" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "foo x = bar x"
@@ -154,14 +162,7 @@
                             , HaskellLine "  where"
                             , HaskellLine "    bar = id"
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ( "expected 1 block for where clause, got "
-                        ++ show (length blocks)
-                        ++ ": "
-                        ++ show blocks
-                    )
-                    (length blocks == 1)
+                length (splitBlocks result) @?= 1
             , testCase "blank before guards stays in same block" $ do
                 let result =
                         toGhciScript
@@ -170,14 +171,7 @@
                             , HaskellLine "  | n > 0 = 1"
                             , HaskellLine "  | otherwise = 0"
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ( "expected 1 block for guards, got "
-                        ++ show (length blocks)
-                        ++ ": "
-                        ++ show blocks
-                    )
-                    (length blocks == 1)
+                length (splitBlocks result) @?= 1
             , testCase "blank before deriving stays in same block" $ do
                 let result =
                         toGhciScript
@@ -185,14 +179,7 @@
                             , Blank
                             , HaskellLine "  deriving (Show, Eq)"
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ( "expected 1 block for deriving, got "
-                        ++ show (length blocks)
-                        ++ ": "
-                        ++ show blocks
-                    )
-                    (length blocks == 1)
+                length (splitBlocks result) @?= 1
             , testCase "double blank still breaks (intentional separation)" $ do
                 let result =
                         toGhciScript
@@ -201,35 +188,25 @@
                             , Blank
                             , HaskellLine "y = 2"
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ("expected 2 blocks for double blank, got " ++ show (length blocks))
-                    (length blocks == 2)
+                length (splitBlocks result) @?= 2
             ]
         , testGroup
-            "Mixed block splitting (don't break)"
+            "Mixed block splitting"
             [ testCase "pure definitions stay grouped" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "f x = x + 1"
                             , HaskellLine "g y = y * 2"
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ("expected 1 block for definitions, got " ++ show (length blocks))
-                    (length blocks == 1)
+                length (splitBlocks result) @?= 1
             , testCase "indented continuation stays grouped" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "f x ="
                             , HaskellLine "  x + 1"
                             ]
-                let blocks = splitBlocks result
-                length blocks @?= 1
-            ]
-        , testGroup
-            "Mixed block splitting (new behavior)"
-            [ testCase "definition then IO splits into two blocks" $ do
+                length (splitBlocks result) @?= 1
+            , testCase "definition then IO splits into two blocks" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "x = 5"
@@ -274,29 +251,77 @@
                             [ HaskellLine "f :: Int -> Int"
                             , HaskellLine "f x = x + 1"
                             ]
+                length (splitBlocks result) @?= 1
+            , testCase "comment + type sig + guarded body + where stays together" $ do
+                let result =
+                        toGhciScript
+                            [ HaskellLine "-- Helper function to check if a number is prime"
+                            , HaskellLine "isPrime :: Int -> Bool"
+                            , HaskellLine "isPrime n"
+                            , HaskellLine "  | n < 2 = False"
+                            , HaskellLine "  | n == 2 = True"
+                            , HaskellLine "  | even n = False"
+                            , HaskellLine "  | otherwise = all (\\k -> n `mod` k /= 0) [3, 5 .. isqrt n]"
+                            , HaskellLine "  where isqrt = floor . sqrt . fromIntegral"
+                            ]
+                length (splitBlocks result) @?= 1
+            , testCase "multi-section script: commented + typed functions + do block" $ do
+                let result =
+                        toGhciScript
+                            [ HaskellLine "-- Helper function to check if a number is prime"
+                            , HaskellLine "isPrime :: Int -> Bool"
+                            , HaskellLine "isPrime n"
+                            , HaskellLine "  | n < 2 = False"
+                            , HaskellLine "  | n == 2 = True"
+                            , HaskellLine "  | even n = False"
+                            , HaskellLine "  | otherwise = all (\\k -> n `mod` k /= 0) [3, 5 .. isqrt n]"
+                            , HaskellLine "  where isqrt = floor . sqrt . fromIntegral"
+                            , Blank
+                            , HaskellLine "-- Generate list of primes up to n"
+                            , HaskellLine "primesUpTo :: Int -> [Int]"
+                            , HaskellLine "primesUpTo n = filter isPrime [2 .. n]"
+                            , Blank
+                            , HaskellLine "-- Compute gaps between consecutive primes"
+                            , HaskellLine "primeGaps :: [Int] -> [Int]"
+                            , HaskellLine "primeGaps ps = zipWith (-) (tail ps) ps"
+                            , Blank
+                            , HaskellLine "-- Test"
+                            , HaskellLine "do"
+                            , HaskellLine "  let testPrimes = primesUpTo 10"
+                            ]
                 let blocks = splitBlocks result
                 assertBool
-                    ("expected 1 block for sig+def, got " ++ show (length blocks))
-                    (length blocks == 1)
+                    ( "expected 4 blocks (one per logical section), got "
+                        ++ show (length blocks)
+                        ++ ": "
+                        ++ show blocks
+                    )
+                    (length blocks == 4)
             ]
         , testGroup
-            "Bracket counting (don't break)"
+            "Comments"
+            [ testCase "comment attaches to following declaration" $ do
+                let result =
+                        toGhciScript
+                            [ HaskellLine "-- doc comment"
+                            , HaskellLine "f x = x + 1"
+                            ]
+                length (splitBlocks result) @?= 1
+                assertWrapped result ["-- doc comment", "f x = x + 1"]
+            ]
+        , testGroup
+            "Bracket counting"
             [ testCase "complete expression in parens" $ do
                 let result = toGhciScript [HaskellLine "f x = (x + 1)"]
-                let blocks = splitBlocks result
-                length blocks @?= 1
+                length (splitBlocks result) @?= 1
             , testCase "balanced multiline already grouped by indentation" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "f x ="
                             , HaskellLine "  (x + 1)"
                             ]
-                let blocks = splitBlocks result
-                length blocks @?= 1
-            ]
-        , testGroup
-            "Bracket counting (new behavior)"
-            [ testCase "unclosed bracket extends block" $ do
+                length (splitBlocks result) @?= 1
+            , testCase "unclosed bracket extends block" $ do
                 let result =
                         toGhciScript
                             [ HaskellLine "xs = ["
@@ -305,14 +330,7 @@
                             , HaskellLine "  3"
                             , HaskellLine "  ]"
                             ]
-                let blocks = splitBlocks result
-                assertBool
-                    ( "expected 1 block for list literal, got "
-                        ++ show (length blocks)
-                        ++ ": "
-                        ++ show blocks
-                    )
-                    (length blocks == 1)
+                length (splitBlocks result) @?= 1
             ]
         ]
 
