tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

Spec.hs (162216B)


      1 module Main where
      2 
      3 import Eval
      4 import FileEval
      5 import Lexer
      6 import Parser
      7 import REPL
      8 import Research
      9 import Wire
     10 import ContentStore
     11 import ContentStore.Bundle
     12 import Module.Manifest
     13 import Module.Resolver
     14 import IODriver (IOPermissions(..), checkIOSentinel, runIO, runIOWithEnv, runIOWith, unsafePerms, defaultPerms)
     15 
     16 import Control.Exception      (bracket, evaluate, try, SomeException)
     17 import System.IO.Unsafe       (unsafePerformIO)
     18 import qualified Network.Socket as NS
     19 import Control.Monad         (forM, forM_)
     20 import Control.Monad.IO.Class (liftIO)
     21 import System.IO.Temp         (withSystemTempDirectory)
     22 import System.Directory      (createDirectory, doesFileExist, doesDirectoryExist, listDirectory, getCurrentDirectory)
     23 import System.FilePath       ((</>))
     24 import Data.Bits              (xor)
     25 import Data.Char              (digitToInt)
     26 import Data.List              (find, isInfixOf)
     27 import Data.Text              (Text, unpack, pack)
     28 import Data.Word              (Word8)
     29 import Test.Tasty
     30 import Test.Tasty.HUnit
     31 import Text.Megaparsec        (runParser)
     32 
     33 import Data.ByteString (ByteString)
     34 import qualified Data.Foldable as Foldable
     35 import qualified Data.ByteString as BS
     36 import qualified Data.Map as Map
     37 import qualified Data.Sequence as Seq
     38 import qualified Data.Set as Set
     39 import qualified Data.Vector as V
     40 
     41 main :: IO ()
     42 main = defaultMain tests
     43 
     44 tricuTestString :: String -> String
     45 tricuTestString s = show $ result (evalTricu Map.empty $ parseTricu s)
     46 
     47 testStore :: StorePath
     48 testStore = StorePath "/tmp/tricu-test-store"
     49 {-# NOINLINE testStore #-}
     50 
     51 allTestLibsEnv :: Env
     52 allTestLibsEnv = unsafePerformIO $ do
     53   base    <- evaluateFile "./lib/base.tri"
     54   bytes   <- evaluateFile "./lib/bytes.tri"
     55   bin     <- evaluateFile "./lib/binary.tri"
     56   http    <- evaluateFile "./lib/http.tri"
     57   arbor   <- evaluateFile "./lib/arboricx/arboricx.tri"
     58   io      <- evaluateFile "./lib/io.tri"
     59   sock    <- evaluateFile "./lib/socket.tri"
     60   intensional   <- evaluateFile "./lib/intensionalContracts.tri"
     61   pure (injectKernel (Map.unions [base, bytes, bin, http, arbor, io, sock, intensional]))
     62 {-# NOINLINE allTestLibsEnv #-}
     63 
     64 tests :: TestTree
     65 tests = testGroup "Tricu Tests"
     66   [ lexer
     67   , parser
     68   , simpleEvaluation
     69   , lambdas
     70   , arithmetic
     71   , providedLibraries
     72   , contractsTests
     73   , maybeTests
     74   , fileEval
     75   , demos
     76   , decoding
     77   , elimLambdaSingle
     78   , stressElimLambda
     79   , byteMarshallingTests
     80   , wireTests
     81   , tricuReaderTests
     82   , byteListUtilities
     83   , binaryParserTests
     84   , httpParsingTests
     85   , contentStoreTests
     86   , ioDriverTests
     87   ]
     88 
     89 lexer :: TestTree
     90 lexer = testGroup "Lexer Tests"
     91   [ testCase "Lex simple identifiers" $ do
     92       let input = "x a b = a"
     93           expect = Right [LIdentifier "x", LIdentifier "a", LIdentifier "b", LAssign, LIdentifier "a"]
     94       runParser tricuLexer "" input @?= expect
     95 
     96   , testCase "Lex Tree Calculus terms" $ do
     97       let input = "t t t"
     98           expect = Right [LKeywordT, LKeywordT, LKeywordT]
     99       runParser tricuLexer "" input @?= expect
    100 
    101   , testCase "Lex escaped characters in strings" $ do
    102       let input = "\"hello\\nworld\""
    103           expect = Right [LStringLiteral "hello\nworld"]
    104       runParser tricuLexer "" input @?= expect
    105 
    106   , testCase "Lex multiple escaped characters in strings" $ do
    107       let input = "\"tab:\\t newline:\\n quote:\\\" backslash:\\\\\""
    108           expect = Right [LStringLiteral "tab:\t newline:\n quote:\" backslash:\\"]
    109       runParser tricuLexer "" input @?= expect
    110 
    111   , testCase "Lex escaped characters in string literals" $ do
    112       let input = "x = \"line1\\nline2\\tindented\""
    113           expect = Right [LIdentifier "x", LAssign, LStringLiteral "line1\nline2\tindented"]
    114       runParser tricuLexer "" input @?= expect
    115 
    116   , testCase "Lex empty string with escape sequence" $ do
    117       let input = "\"\\\"\""
    118           expect = Right [LStringLiteral "\""]
    119       runParser tricuLexer "" input @?= expect
    120 
    121   , testCase "Lex mixed literals" $ do
    122       let input = "t \"string\" 42"
    123           expect = Right [LKeywordT, LStringLiteral "string", LIntegerLiteral 42]
    124       runParser tricuLexer "" input @?= expect
    125 
    126   , testCase "Lex invalid token" $ do
    127       let input = "&invalid"
    128       case runParser tricuLexer "" input of
    129         Left _ -> return ()
    130         Right _ -> assertFailure "Expected lexer to fail on invalid token"
    131 
    132   , testCase "Drop trailing whitespace in definitions" $ do
    133       let input = "x = 5 "
    134           expect = [LIdentifier "x",LAssign,LIntegerLiteral 5]
    135       case (runParser tricuLexer "" input) of
    136         Left _ -> assertFailure "Failed to lex input"
    137         Right i -> i @?= expect
    138 
    139   , testCase "Error when using invalid characters in identifiers" $ do
    140         case (runParser tricuLexer "" "!result = 5") of
    141           Left _ -> return ()
    142           Right _ -> assertFailure "Expected failure when trying to assign the value of !result"
    143 
    144   , testCase "Lex <| as arrow-left token" $ do
    145       let input = "f <| g"
    146           expect = Right [LIdentifier "f", LArrowLeft, LIdentifier "g"]
    147       runParser tricuLexer "" input @?= expect
    148 
    149   , testCase "Lex <| without surrounding spaces" $ do
    150       let input = "a<|b"
    151           expect = Right [LIdentifier "a", LArrowLeft, LIdentifier "b"]
    152       runParser tricuLexer "" input @?= expect
    153 
    154   , testCase "Lex |> as arrow-right token" $ do
    155       let input = "f |> g"
    156           expect = Right [LIdentifier "f", LArrowRight, LIdentifier "g"]
    157       runParser tricuLexer "" input @?= expect
    158 
    159   , testCase "Lex |> without surrounding spaces" $ do
    160       let input = "a|>b"
    161           expect = Right [LIdentifier "a", LArrowRight, LIdentifier "b"]
    162       runParser tricuLexer "" input @?= expect
    163 
    164   , testCase "Lex <- as bind arrow token" $ do
    165       let input = "x <- action"
    166           expect = Right [LIdentifier "x", LBindArrow, LIdentifier "action"]
    167       runParser tricuLexer "" input @?= expect
    168 
    169   , testCase "Lex $ remains legal identifier char" $ do
    170       let input = "foo$bar = 1"
    171           expect = Right [LIdentifier "foo$bar", LAssign, LIntegerLiteral 1]
    172       runParser tricuLexer "" input @?= expect
    173 
    174   , testCase "Lex @ and =@ as annotation tokens" $ do
    175       let input = "f x@Bool =@String x"
    176           expect = Right
    177             [ LIdentifier "f"
    178             , LIdentifier "x"
    179             , LAt
    180             , LIdentifier "Bool"
    181             , LAssignAt
    182             , LIdentifier "String"
    183             , LIdentifier "x"
    184             ]
    185       runParser tricuLexer "" input @?= expect
    186   ]
    187 
    188 parser :: TestTree
    189 parser = testGroup "Parser Tests"
    190   [ testCase "Error when assigning a value to T" $ do
    191       let tokens = lexTricu "t = x" 
    192       case parseSingleExpr tokens of
    193         Left  _ -> return ()
    194         Right _ -> assertFailure "Expected failure when trying to assign the value of T"
    195 
    196   , testCase "Parse function definitions" $ do
    197       let input = "x = (a b c : a)"
    198           expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SLambda ["c"] (SVar "a" Nothing))))
    199       parseSingle input @?= expect
    200 
    201   , testCase "Parse nested Tree Calculus terms" $ do
    202       let input = "t (t t) t"
    203           expect = SApp (SApp TLeaf (SApp TLeaf TLeaf)) TLeaf
    204       parseSingle input @?= expect
    205 
    206   , testCase "Parse sequential Tree Calculus terms" $ do
    207       let input = "t t t"
    208           expect = SApp (SApp TLeaf TLeaf) TLeaf
    209       parseSingle input @?= expect
    210 
    211   , testCase "Parse mixed list literals" $ do
    212       let input = "[t (\"hello\") t]"
    213           expect = SList [TLeaf, SStr "hello", TLeaf]
    214       parseSingle input @?= expect
    215 
    216   , testCase "Parse function with applications" $ do
    217       let input  = "f = (x : t x)"
    218           expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SVar "x" Nothing)))
    219       parseSingle input @?= expect
    220 
    221   , testCase "Parse nested lists" $ do
    222       let input  = "[t [(t t)]]"
    223           expect = SList [TLeaf,SList [SApp TLeaf TLeaf]]
    224       parseSingle input @?= expect
    225 
    226   , testCase "Parse complex parentheses" $ do
    227       let input  = "t (t t (t t))"
    228           expect = SApp TLeaf (SApp (SApp TLeaf TLeaf) (SApp TLeaf TLeaf))
    229       parseSingle input @?= expect
    230 
    231   , testCase "Parse empty list" $ do
    232       let input  = "[]"
    233           expect = SList []
    234       parseSingle input @?= expect
    235 
    236   , testCase "Parse multiple nested lists" $ do
    237       let input  = "[[t t] [t (t t)]]"
    238           expect = SList [SList [TLeaf,TLeaf],SList [TLeaf,SApp TLeaf TLeaf]]
    239       parseSingle input @?= expect
    240 
    241   , testCase "Parse whitespace variance" $ do
    242       let input1 = "[t t]"
    243       let input2 = "[ t t ]"
    244           expect = SList [TLeaf, TLeaf]
    245       parseSingle input1 @?= expect
    246       parseSingle input2 @?= expect
    247 
    248   , testCase "Parse string in list" $ do
    249       let input  = "[(\"hello\")]"
    250           expect = SList [SStr "hello"]
    251       parseSingle input @?= expect
    252 
    253   , testCase "Parse parentheses inside list" $ do
    254       let input  = "[t (t t)]"
    255           expect = SList [TLeaf,SApp TLeaf TLeaf]
    256       parseSingle input @?= expect
    257 
    258   , testCase "Parse nested parentheses in function body" $ do
    259       let input  = "f = (x : t (t (t t)))"
    260           expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SApp TLeaf (SApp TLeaf TLeaf))))
    261       parseSingle input @?= expect
    262 
    263   , testCase "Parse lambda abstractions" $ do
    264       let input  = "(a : a)"
    265           expect = (SLambda ["a"] (SVar "a" Nothing))
    266       parseSingle input @?= expect
    267 
    268   , testCase "Parse multiple arguments to lambda abstractions" $ do
    269       let input  = "x = (a b : a)"
    270           expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SVar "a" Nothing)))
    271       parseSingle input @?= expect
    272 
    273   , testCase "Parse top-level definition arguments" $ do
    274       let input  = "const a b = a"
    275           expect = SDef "const" ["a", "b"] (SVar "a" Nothing)
    276       parseSingle input @?= expect
    277 
    278   , testCase "Evaluate top-level definition arguments" $ do
    279       tricuTestString "const a b = a\nconst 1 2" @?= "Fork (Stem Leaf) Leaf"
    280 
    281   , testCase "Parse annotated definition binders" $ do
    282       let input = "foo x@Bool xs@(List Bool) =@String x"
    283           expect = SDefAnn
    284             "foo"
    285             [DefBinder "x" (Just (VEName "Bool")), DefBinder "xs" (Just (VEApp (VEName "List") (VEName "Bool")))]
    286             (Just (VEName "String"))
    287             (SVar "x" Nothing)
    288       parseSingle input @?= expect
    289 
    290   , testCase "Parse phantom tail annotations" $ do
    291       let input = "foo x@Bool @(List Bool) =@String x"
    292           expect = SDefAnn
    293             "foo"
    294             [DefBinder "x" (Just (VEName "Bool")), DefPhantom (VEApp (VEName "List") (VEName "Bool"))]
    295             (Just (VEName "String"))
    296             (SVar "x" Nothing)
    297       parseSingle input @?= expect
    298 
    299   , testCase "Parse pure phantom function annotation" $ do
    300       let input = "foo @Bool @(Fn [Bool] String) =@Unit (x : x)"
    301           expect = SDefAnn
    302             "foo"
    303             [DefPhantom (VEName "Bool"), DefPhantom (VEApp (VEApp (VEName "Fn") (VEList [VEName "Bool"])) (VEName "String"))]
    304             (Just (VEName "Unit"))
    305             (SLambda ["x"] (SVar "x" Nothing))
    306       parseSingle input @?= expect
    307 
    308   , testCase "Evaluate annotated definition applies runtime contract boundary" $ do
    309       let input = unlines
    310             [ "bool? = guardC \"not a boolean\" (b : or? (equal? b true) (equal? b false))"
    311             , "id x@bool? =@bool? x"
    312             , "main = id true"
    313             ]
    314           env = evalTricu allTestLibsEnv (parseTricu input)
    315       result env @?= trueT
    316 
    317   , testCase "Reject named binders after phantom annotations" $ do
    318       let tokens = lexTricu "foo @Bool x@Bool =@Bool x"
    319       case parseSingleExpr tokens of
    320         Left _ -> return ()
    321         Right ast -> assertFailure $ "Expected parse failure, got " ++ show ast
    322 
    323   , testCase "Unparenthesized annotation names remain ordinary aliases" $ do
    324       let input = "foo x@List Bool =@Bool x"
    325           expect = SDefAnn "foo"
    326             [DefBinder "x" (Just (VEName "List")), DefBinder "Bool" Nothing]
    327             (Just (VEName "Bool"))
    328             (SVar "x" Nothing)
    329       parseSingle input @?= expect
    330 
    331   , testCase "Parse let expression" $ do
    332       let input  = "let x = t t in x"
    333           expect = SLet "x" (SApp TLeaf TLeaf) (SVar "x" Nothing)
    334       parseSingle input @?= expect
    335 
    336   , testCase "Evaluate let expression" $ do
    337       tricuTestString "let x = 1 in x" @?= "Fork (Stem Leaf) Leaf"
    338 
    339   , testCase "Parse let function binding" $ do
    340       let input  = "let f x = x in f t"
    341           expect = SLet "f" (SLambda ["x"] (SVar "x" Nothing))
    342                         (SApp (SVar "f" Nothing) TLeaf)
    343       parseSingle input @?= expect
    344 
    345   , testCase "Parse where expression" $ do
    346       let input  = "x where x = t t"
    347           expect = SLet "x" (SApp TLeaf TLeaf) (SVar "x" Nothing)
    348       parseSingle input @?= expect
    349 
    350   , testCase "Evaluate where expression" $ do
    351       tricuTestString "x where x = 1" @?= "Fork (Stem Leaf) Leaf"
    352 
    353   , testCase "Parse where binding with arguments (SLet)" $ do
    354       let input  = "f 3 where f x = x"
    355           expect = SLet "f" (SLambda ["x"] (SVar "x" Nothing))
    356                         (SApp (SVar "f" Nothing) (SInt 3))
    357       parseSingle input @?= expect
    358 
    359   , testCase "Evaluate where binding with arguments matches applied lambda" $ do
    360       tricuTestString "f (t t) where f x = t x x"
    361         @?= tricuTestString "(f : f (t t)) (x : t x x)"
    362 
    363   , testCase "Evaluate nested let bindings" $ do
    364       tricuTestString "let a = t t in let b = t in t a b"
    365         @?= tricuTestString "t (t t) t"
    366 
    367   , testCase "Inner let binding shadows outer binding" $ do
    368       tricuTestString "let x = t in let x = t t in x"
    369         @?= tricuTestString "t t"
    370 
    371   , testCase "Parse indented multiline definition body" $ do
    372       let input  = "x =\n  t\n    t"
    373           expect = SDef "x" [] (SApp TLeaf TLeaf)
    374       parseSingle input @?= expect
    375 
    376   , testCase "Evaluate indented multiline let" $ do
    377       tricuTestString "let\n  x =\n    1\nin\n  x" @?= "Fork (Stem Leaf) Leaf"
    378 
    379   , testCase "Evaluate indented multiline where" $ do
    380       tricuTestString "x\n  where x =\n    1" @?= "Fork (Stem Leaf) Leaf"
    381 
    382   , testCase "Parse explicit custom-bind do" $ do
    383       let input = "do bind\n  x <- pure t\n  pure x"
    384           expect = SApp
    385             (SApp (SVar "bind" Nothing) (SApp (SVar "pure" Nothing) TLeaf))
    386             (SLambda ["x"] (SApp (SVar "pure" Nothing) (SVar "x" Nothing)))
    387       parseSingle input @?= expect
    388 
    389   , testCase "Parse do statement without binder" $ do
    390       let input = "do bind\n  pure t\n  pure t"
    391           expect = SApp
    392             (SApp (SVar "bind" Nothing) (SApp (SVar "pure" Nothing) TLeaf))
    393             (SLambda ["_"] (SApp (SVar "pure" Nothing) TLeaf))
    394       parseSingle input @?= expect
    395 
    396   , testCase "Reject bare do without explicit bind operator" $ do
    397       parsed <- try (evaluate (parseSingle "do\n  x <- pure t\n  pure x")) :: IO (Either SomeException TricuAST)
    398       case parsed of
    399         Left _  -> pure ()
    400         Right _ -> assertFailure "Expected bare do to fail"
    401 
    402   , testCase "Grouping T terms with parentheses in function application" $ do
    403       let input  = "x = (a : a)\nx (t)"
    404           expect = [SDef "x" [] (SLambda ["a"] (SVar "a" Nothing)),SApp (SVar "x" Nothing) TLeaf]
    405       parseTricu input @?= expect
    406 
    407   , testCase "Comments 1" $ do
    408       let input = "(t) (t) -- (t)"
    409           expect = [SApp TLeaf TLeaf]
    410       parseTricu input @?= expect
    411 
    412   , testCase "Comments 2" $ do
    413       let input = "(t) -- (t) -- (t)"
    414           expect = [TLeaf]
    415       parseTricu input @?= expect
    416 
    417   , testCase "Parse <| as low-precedence application" $ do
    418       let input = "f x <| g y"
    419           expect = SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing))
    420                         (SApp (SVar "g" Nothing) (SVar "y" Nothing))
    421       parseSingle input @?= expect
    422 
    423   , testCase "Parse chained <| as left-associative" $ do
    424       let input = "f <| g <| h"
    425           expect = SApp (SApp (SVar "f" Nothing) (SVar "g" Nothing))
    426                         (SVar "h" Nothing)
    427       parseSingle input @?= expect
    428 
    429   , testCase "Parse <| after newline inside parens" $ do
    430       let input = "(f x <|\n  g y)"
    431           expect = SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing))
    432                         (SApp (SVar "g" Nothing) (SVar "y" Nothing))
    433       parseSingle input @?= expect
    434 
    435   , testCase "Parse <| in lambda body" $ do
    436       let input = "(x : f x <| g)"
    437           expect = SLambda ["x"] (SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing))
    438                                       (SVar "g" Nothing))
    439       parseSingle input @?= expect
    440 
    441   , testCase "Parse |> as low-precedence application" $ do
    442       let input = "f x |> g y"
    443           expect = SApp (SApp (SVar "g" Nothing) (SVar "y" Nothing))
    444                         (SApp (SVar "f" Nothing) (SVar "x" Nothing))
    445       parseSingle input @?= expect
    446 
    447   , testCase "Parse chained |> as left-associative" $ do
    448       let input = "f |> g |> h"
    449           expect = SApp (SVar "h" Nothing)
    450                         (SApp (SVar "g" Nothing) (SVar "f" Nothing))
    451       parseSingle input @?= expect
    452 
    453   , testCase "Parse |> after newline inside parens" $ do
    454       let input = "(f x |>\n  g y)"
    455           expect = SApp (SApp (SVar "g" Nothing) (SVar "y" Nothing))
    456                         (SApp (SVar "f" Nothing) (SVar "x" Nothing))
    457       parseSingle input @?= expect
    458 
    459   , testCase "Parse |> in lambda body" $ do
    460       let input = "(x : f x |> g)"
    461           expect = SLambda ["x"] (SApp (SVar "g" Nothing)
    462                                       (SApp (SVar "f" Nothing) (SVar "x" Nothing)))
    463       parseSingle input @?= expect
    464 
    465   , testCase "Parse mixed <| and |>" $ do
    466       let input = "f |> g <| h"
    467           expect = SApp (SApp (SVar "g" Nothing) (SVar "f" Nothing))
    468                         (SVar "h" Nothing)
    469       parseSingle input @?= expect
    470 
    471   , testCase "Parse forward pipe chain" $ do
    472       let input = "x |> f |> g"
    473           expect = SApp (SVar "g" Nothing)
    474                     (SApp (SVar "f" Nothing) (SVar "x" Nothing))
    475       parseSingle input @?= expect
    476 
    477   , testCase "Parse backward pipe" $ do
    478       let input = "f <| x"
    479           expect = SApp (SVar "f" Nothing) (SVar "x" Nothing)
    480       parseSingle input @?= expect
    481 
    482   , testCase "Parse backward pipe chain left associative" $ do
    483       let input = "f <| x <| y"
    484           expect = SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing))
    485                     (SVar "y" Nothing)
    486       parseSingle input @?= expect
    487 
    488   , testCase "Parse newline after forward pipe" $ do
    489       let input = "x |>\nf"
    490           expect = SApp (SVar "f" Nothing) (SVar "x" Nothing)
    491       parseSingle input @?= expect
    492 
    493   , testCase "Parse newline after backward pipe" $ do
    494       let input = "f <|\nx"
    495           expect = SApp (SVar "f" Nothing) (SVar "x" Nothing)
    496       parseSingle input @?= expect
    497   ]
    498 
    499 simpleEvaluation :: TestTree
    500 simpleEvaluation = testGroup "Evaluation Tests"
    501   [ testCase "Evaluate single Leaf" $ do
    502       let input = "t"
    503       let ast = parseSingle input
    504       (result $ evalSingle Map.empty ast) @?= Leaf
    505 
    506   , testCase "Evaluate single Stem" $ do
    507       let input = "t t"
    508       let ast = parseSingle input
    509       (result $ evalSingle Map.empty ast) @?= Stem Leaf
    510 
    511   , testCase "Evaluate single Fork" $ do
    512       let input = "t t t"
    513       let ast = parseSingle input
    514       (result $ evalSingle Map.empty ast) @?= Fork Leaf Leaf
    515 
    516   , testCase "Evaluate nested Fork and Stem" $ do
    517       let input = "t (t t) t"
    518       let ast = parseSingle input
    519       (result $ evalSingle Map.empty ast) @?= Fork (Stem Leaf) Leaf
    520 
    521   , testCase "Evaluate `not` function" $ do
    522       let input = "t (t (t t) (t t t)) t"
    523       let ast = parseSingle input
    524       (result $ evalSingle Map.empty ast) @?=
    525         Fork (Fork (Stem Leaf) (Fork Leaf Leaf)) Leaf
    526 
    527   , testCase "Environment updates with definitions" $ do
    528       let input = "x = t\ny = x"
    529           env = evalTricu Map.empty (parseTricu input)
    530       Map.lookup "x" env @?= Just Leaf
    531       Map.lookup "y" env @?= Just Leaf
    532 
    533   , testCase "Variable substitution" $ do
    534       let input = "x = t t\ny = t x\ny"
    535           env = evalTricu Map.empty (parseTricu input)
    536       (result env) @?= Stem (Stem Leaf)
    537 
    538   , testCase "Multiline input evaluation" $ do
    539       let input = "x = t\ny = t t\nx"
    540           env = evalTricu Map.empty (parseTricu input)
    541       (result env) @?= Leaf
    542 
    543   , testCase "Evaluate string literal" $ do
    544       let input = "\"hello\""
    545       let ast = parseSingle input
    546       (result $ evalSingle Map.empty ast) @?= ofString "hello"
    547 
    548   , testCase "Evaluate list literal" $ do
    549       let input = "[t (t t)]"
    550       let ast = parseSingle input
    551       (result $ evalSingle Map.empty ast) @?= ofList [Leaf, Stem Leaf]
    552 
    553   , testCase "Evaluate empty list" $ do
    554       let input = "[]"
    555       let ast = parseSingle input
    556       (result $ evalSingle Map.empty ast) @?= ofList []
    557 
    558   , testCase "Evaluate variable dependency chain" $ do
    559       let input = "x = t (t t)\n \
    560                   \ y = x\n \
    561                   \ z = y\n \
    562                   \ variablewithamuchlongername = z\n \
    563                   \ variablewithamuchlongername"
    564           env = evalTricu Map.empty (parseTricu input)
    565       (result env) @?= (Stem (Stem Leaf))
    566 
    567 
    568   , testCase "Immutable definitions" $ do
    569       let input = "x = t t\nx = t\nx"
    570           env = evalTricu Map.empty (parseTricu input)
    571       result <- try (evaluate (tricuTestString input)) :: IO (Either SomeException String)
    572       case result of
    573         Left  _ -> return ()
    574         Right _ -> assertFailure "Expected evaluation error"
    575 
    576 
    577   , testCase "Apply identity to Boolean Not" $ do
    578       let not = "(t (t (t t) (t t t)) t)"
    579       let input = "x = (a : a)\nx " ++ not
    580           env = evalTricu Map.empty (parseTricu input)
    581       result env @?= Fork (Fork (Stem Leaf) (Fork Leaf Leaf)) Leaf
    582   ]
    583 
    584 lambdas :: TestTree
    585 lambdas = testGroup "Lambda Evaluation Tests"
    586   [ testCase "Lambda Identity Function" $ do
    587       let input = "id = (x : x)\nid t"
    588       tricuTestString input @?= "Leaf"
    589 
    590   , testCase "Lambda Constant Function (K combinator)" $ do
    591       let input = "k = (x y : x)\nk t (t t)"
    592       tricuTestString input @?= "Leaf"
    593 
    594   , testCase "Lambda Application with Variable" $ do
    595       let input = "id = (x : x)\nval = t t\nid val"
    596       tricuTestString input @?= "Stem Leaf"
    597 
    598   , testCase "Lambda Application with Multiple Arguments" $ do
    599       let input = "apply = (f x y : f x y)\nk = (a b : a)\napply k t (t t)"
    600       tricuTestString input @?= "Leaf"
    601 
    602    , testCase "Nested Lambda Application" $ do
    603       let input = "apply = (f x y : f x y)\nid = (x : x)\napply (f x : f x) id t"
    604       tricuTestString input @?= "Leaf"
    605 
    606   , testCase "Lambda with a complex body" $ do
    607       let input = "f = (x : t (t x))\nf t"
    608       tricuTestString input @?= "Stem (Stem Leaf)"
    609 
    610   , testCase "Lambda returning a function" $ do
    611       let input = "f = (x : (y : x))\ng = f t\ng (t t)"
    612       tricuTestString input @?= "Leaf"
    613 
    614   , testCase "Lambda with Shadowing" $ do
    615       let input = "f = (x : (x : x))\nf t (t t)"
    616       tricuTestString input @?= "Stem Leaf"
    617 
    618    , testCase "Lambda returning another lambda" $ do
    619       let input = "k = (x : (y : x))\nk_app = k t\nk_app (t t)"
    620       tricuTestString input @?= "Leaf"
    621 
    622    , testCase "Lambda with free variables" $ do
    623       let input = "y = t t\nf = (x : y)\nf t"
    624       tricuTestString input @?= "Stem Leaf"
    625 
    626    , testCase "SKI Composition" $ do
    627       let input = "s = (x y z : x z (y z))\nk = (x y : x)\ni = (x : x)\ncomp = s k i\ncomp t (t t)"
    628       tricuTestString input @?= "Stem (Stem Leaf)"
    629 
    630    , testCase "Lambda with multiple parameters and application" $ do
    631       let input = "f = (a b c : t a b c)\nf t (t t) (t t t)"
    632       tricuTestString input @?= "Stem Leaf"
    633 
    634    , testCase "Lambda with nested application in the body" $ do
    635       let input = "f = (x : t (t (t x)))\nf t"
    636       tricuTestString input @?= "Stem (Stem (Stem Leaf))"
    637 
    638     , testCase "Lambda returning a function and applying it" $ do
    639         let input = "f = (x : (y : t x y))\ng = f t\ng (t t)"
    640         tricuTestString input @?= "Fork Leaf (Stem Leaf)"
    641 
    642     , testCase "Lambda applying a variable" $ do
    643         let input = "id = (x : x)\na = t t\nid a"
    644         tricuTestString input @?= "Stem Leaf"
    645 
    646     , testCase "Nested lambda abstractions in the same expression" $ do
    647         let input = "f = (x : (y : x y))\ng = (z : z)\nf g t"
    648         tricuTestString input @?= "Leaf"
    649 
    650   , testCase "Lambda applied to string literal" $ do
    651         let input = "f = (x : x)\nf \"hello\""
    652         tricuTestString input @?= "Fork (Fork Leaf (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) Leaf))))"
    653 
    654 
    655    , testCase "Lambda applied to integer literal" $ do
    656         let input = "f = (x : x)\nf 42"
    657         tricuTestString input @?= "Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) Leaf)))))"
    658 
    659    , testCase "Lambda applied to list literal" $ do
    660         let input = "f = (x : x)\nf [t (t t)]"
    661         tricuTestString input @?= "Fork Leaf (Fork (Stem Leaf) Leaf)"
    662 
    663   , testCase "Lambda containing list literal" $ do
    664       let input = "(a : [(a)]) 1"
    665       tricuTestString input @?= "Fork (Fork (Stem Leaf) Leaf) Leaf"
    666   ]
    667 
    668 maybeTests :: TestTree
    669 maybeTests = testGroup "Maybe Tests"
    670   [ testCase "nothing is Leaf" $ do
    671       let input = "nothing"
    672           env = evalTricu allTestLibsEnv (parseTricu input)
    673       result env @?= Leaf
    674 
    675   , testCase "just wraps value in Stem" $ do
    676       let input = "just (t t)"
    677           env = evalTricu allTestLibsEnv (parseTricu input)
    678       result env @?= Stem (Stem Leaf)
    679 
    680   , testCase "matchMaybe on nothing returns default" $ do
    681       let input = "matchMaybe \"empty\" (x : x) nothing"
    682           env = evalTricu allTestLibsEnv (parseTricu input)
    683       result env @?= ofString "empty"
    684 
    685   , testCase "matchMaybe on just extracts value" $ do
    686       let input = "matchMaybe \"empty\" (x : x) (just (t t))"
    687           env = evalTricu allTestLibsEnv (parseTricu input)
    688       result env @?= Stem Leaf
    689 
    690   , testCase "maybe applies f inside just" $ do
    691       let input = "maybe 0 (x : succ x) (just 5)"
    692           env = evalTricu allTestLibsEnv (parseTricu input)
    693       result env @?= ofNumber 6
    694 
    695   , testCase "maybe returns default on nothing" $ do
    696       let input = "maybe 0 (x : succ x) nothing"
    697           env = evalTricu allTestLibsEnv (parseTricu input)
    698       result env @?= ofNumber 0
    699 
    700   , testCase "maybeMap transforms just value" $ do
    701       let input = "maybeMap (x : succ x) (just 3)"
    702           env = evalTricu allTestLibsEnv (parseTricu input)
    703       result env @?= justT (ofNumber 4)
    704 
    705   , testCase "maybeMap returns nothing on nothing" $ do
    706       let input = "maybeMap (x : succ x) nothing"
    707           env = evalTricu allTestLibsEnv (parseTricu input)
    708       result env @?= nothingT
    709 
    710   , testCase "maybeBind flattens just" $ do
    711       let input = "maybeBind (just 3) (x : just (succ x))"
    712           env = evalTricu allTestLibsEnv (parseTricu input)
    713       result env @?= justT (ofNumber 4)
    714 
    715   , testCase "maybeBind returns nothing on nothing" $ do
    716       let input = "maybeBind nothing (x : just (succ x))"
    717           env = evalTricu allTestLibsEnv (parseTricu input)
    718       result env @?= Leaf
    719 
    720   , testCase "maybeOr returns just value" $ do
    721       let input = "maybeOr 99 (just 5)"
    722           env = evalTricu allTestLibsEnv (parseTricu input)
    723       result env @?= ofNumber 5
    724 
    725   , testCase "maybeOr returns default on nothing" $ do
    726       let input = "maybeOr 99 nothing"
    727           env = evalTricu allTestLibsEnv (parseTricu input)
    728       result env @?= ofNumber 99
    729 
    730   , testCase "maybe? on just is true" $ do
    731       let input = "maybe? (just t)"
    732           env = evalTricu allTestLibsEnv (parseTricu input)
    733       result env @?= trueT
    734 
    735   , testCase "maybe? on nothing is false" $ do
    736       let input = "maybe? nothing"
    737           env = evalTricu allTestLibsEnv (parseTricu input)
    738       result env @?= falseT
    739   ]
    740 
    741 providedLibraries :: TestTree
    742 providedLibraries = testGroup "Library Tests"
    743   [ testCase "Triage test Leaf" $ do
    744       let input = "test t"
    745           env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input)
    746       env @?= "\"Leaf\""
    747 
    748   , testCase "Triage test (Stem Leaf)" $ do
    749       let input = "test (t t)"
    750           env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input)
    751       env @?= "\"Stem\""
    752 
    753   , testCase "Triage test (Fork Leaf Leaf)" $ do
    754       let input = "test (t t t)"
    755           env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input)
    756       env @?= "\"Fork\""
    757 
    758   , testCase "Boolean NOT: true" $ do
    759       let input = "not? true"
    760           env = result $ evalTricu allTestLibsEnv (parseTricu input)
    761       env @?= Leaf
    762 
    763   , testCase "Boolean NOT: false" $ do
    764       let input = "not? false"
    765           env = result $ evalTricu allTestLibsEnv (parseTricu input)
    766       env @?= Stem Leaf
    767 
    768 
    769   , testCase "Boolean AND TF" $ do
    770       let input = "and? (t t) (t)"
    771           env = evalTricu allTestLibsEnv (parseTricu input)
    772       result env @?= Leaf
    773 
    774   , testCase "Boolean AND FT" $ do
    775       let input = "and? (t) (t t)"
    776           env = evalTricu allTestLibsEnv (parseTricu input)
    777       result env @?= Leaf
    778 
    779   , testCase "Boolean AND FF" $ do
    780       let input = "and? (t) (t)"
    781           env = evalTricu allTestLibsEnv (parseTricu input)
    782       result env @?= Leaf
    783 
    784   , testCase "Boolean AND TT" $ do
    785       let input = "and? (t t) (t t)"
    786           env = evalTricu allTestLibsEnv (parseTricu input)
    787       result env @?= Stem Leaf
    788 
    789   , testCase "List head" $ do
    790       let input = "head [(t) (t t) (t t t)]"
    791           env = evalTricu allTestLibsEnv (parseTricu input)
    792       result env @?= Leaf
    793 
    794   , testCase "List tail" $ do
    795       let input = "head (tail (tail [(t) (t t) (t t t)]))"
    796           env = evalTricu allTestLibsEnv (parseTricu input)
    797       result env @?= Fork Leaf Leaf
    798 
    799   , testCase "List map" $ do
    800       let input = "head (tail (map (a : (t t t)) [(t) (t) (t)]))"
    801           env = evalTricu allTestLibsEnv (parseTricu input)
    802       result env @?= Fork Leaf Leaf
    803 
    804   , testCase "Empty list check" $ do
    805       let input = "emptyList? []"
    806           env = evalTricu allTestLibsEnv (parseTricu input)
    807       result env @?= Stem Leaf
    808 
    809   , testCase "Non-empty list check" $ do
    810       let input = "not? (emptyList? [(1) (2) (3)])"
    811           env = evalTricu allTestLibsEnv (parseTricu input)
    812       result env @?= Stem Leaf
    813 
    814   , testCase "Concatenate strings" $ do
    815       let input = "append \"Hello, \" \"world!\""
    816           env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input)
    817       env @?= "\"Hello, world!\""
    818 
    819   , testCase "Verifying Equality" $ do
    820       let input = "equal? (t t t) (t t t)"
    821           env = evalTricu allTestLibsEnv (parseTricu input)
    822       result env @?= Stem Leaf
    823 
    824   , testCase "headMaybe on empty list" $ do
    825       let input = "headMaybe []"
    826           env = evalTricu allTestLibsEnv (parseTricu input)
    827       result env @?= nothingT
    828 
    829   , testCase "headMaybe on non-empty list" $ do
    830       let input = "headMaybe [(t) (t t)]"
    831           env = evalTricu allTestLibsEnv (parseTricu input)
    832       result env @?= justT Leaf
    833 
    834   , testCase "lastMaybe on empty list" $ do
    835       let input = "lastMaybe []"
    836           env = evalTricu allTestLibsEnv (parseTricu input)
    837       result env @?= nothingT
    838 
    839   , testCase "lastMaybe on single element" $ do
    840       let input = "lastMaybe [(t t)]"
    841           env = evalTricu allTestLibsEnv (parseTricu input)
    842       result env @?= justT (Stem Leaf)
    843 
    844   , testCase "lastMaybe on multi-element list" $ do
    845       let input = "lastMaybe [(t) (t t) (t t t)]"
    846           env = evalTricu allTestLibsEnv (parseTricu input)
    847       result env @?= justT (Fork Leaf Leaf)
    848 
    849   , testCase "nthMaybe first element" $ do
    850       let input = "nthMaybe 0 [(t) (t t)]"
    851           env = evalTricu allTestLibsEnv (parseTricu input)
    852       result env @?= justT Leaf
    853 
    854   , testCase "nthMaybe middle element" $ do
    855       let input = "nthMaybe 1 [(t) (t t) (t t t)]"
    856           env = evalTricu allTestLibsEnv (parseTricu input)
    857       result env @?= justT (Stem Leaf)
    858 
    859   , testCase "nthMaybe out of bounds" $ do
    860       let input = "nthMaybe 5 [(t) (t t)]"
    861           env = evalTricu allTestLibsEnv (parseTricu input)
    862       result env @?= nothingT
    863 
    864   , testCase "reverse empty list" $ do
    865       let input = "reverse []"
    866           env = evalTricu allTestLibsEnv (parseTricu input)
    867       result env @?= ofList []
    868 
    869   , testCase "reverse non-empty list" $ do
    870       let input = "reverse [(1) (2) (3)]"
    871           env = evalTricu allTestLibsEnv (parseTricu input)
    872       result env @?= ofList [ofNumber 3, ofNumber 2, ofNumber 1]
    873 
    874   , testCase "take 0 any list = empty" $ do
    875       let input = "take 0 [(1) (2) (3)]"
    876           env = evalTricu allTestLibsEnv (parseTricu input)
    877       result env @?= ofList []
    878 
    879   , testCase "take 2 [1,2,3] = [1,2]" $ do
    880       let input = "take 2 [(1) (2) (3)]"
    881           env = evalTricu allTestLibsEnv (parseTricu input)
    882       result env @?= ofList [ofNumber 1, ofNumber 2]
    883 
    884   , testCase "take overlong returns whole list" $ do
    885       let input = "take 5 [(1) (2)]"
    886           env = evalTricu allTestLibsEnv (parseTricu input)
    887       result env @?= ofList [ofNumber 1, ofNumber 2]
    888 
    889   , testCase "drop 0 any list = list" $ do
    890       let input = "drop 0 [(1) (2) (3)]"
    891           env = evalTricu allTestLibsEnv (parseTricu input)
    892       result env @?= ofList [ofNumber 1, ofNumber 2, ofNumber 3]
    893 
    894   , testCase "drop 2 [1,2,3] = [3]" $ do
    895       let input = "drop 2 [(1) (2) (3)]"
    896           env = evalTricu allTestLibsEnv (parseTricu input)
    897       result env @?= ofList [ofNumber 3]
    898 
    899   , testCase "drop overlong returns empty" $ do
    900       let input = "drop 5 [(1) (2)]"
    901           env = evalTricu allTestLibsEnv (parseTricu input)
    902       result env @?= ofList []
    903 
    904   , testCase "splitAt 0 [1,2] = pair [] [1,2]" $ do
    905       let input = "splitAt 0 [(1) (2)]"
    906           env = evalTricu allTestLibsEnv (parseTricu input)
    907       result env @?= pairT (ofList []) (ofList [ofNumber 1, ofNumber 2])
    908 
    909   , testCase "splitAt 2 [1,2,3] = pair [1,2] [3]" $ do
    910       let input = "splitAt 2 [(1) (2) (3)]"
    911           env = evalTricu allTestLibsEnv (parseTricu input)
    912       result env @?= pairT (ofList [ofNumber 1, ofNumber 2]) (ofList [ofNumber 3])
    913 
    914   , testCase "splitAt overlong = pair [1,2] []" $ do
    915       let input = "splitAt 5 [(1) (2)]"
    916           env = evalTricu allTestLibsEnv (parseTricu input)
    917       result env @?= pairT (ofList [ofNumber 1, ofNumber 2]) (ofList [])
    918 
    919   , testCase "concatMap on empty list" $ do
    920       let input = "concatMap (x : [(x) (x)]) []"
    921           env = evalTricu allTestLibsEnv (parseTricu input)
    922       result env @?= ofList []
    923 
    924   , testCase "concatMap doubles elements" $ do
    925       let input = "concatMap (x : [(x) (x)]) [(1) (2)]"
    926           env = evalTricu allTestLibsEnv (parseTricu input)
    927       result env @?= ofList [ofNumber 1, ofNumber 1, ofNumber 2, ofNumber 2]
    928 
    929   , testCase "find on empty list" $ do
    930       let input = "find (x : equal? x 2) []"
    931           env = evalTricu allTestLibsEnv (parseTricu input)
    932       result env @?= nothingT
    933 
    934   , testCase "find finds element" $ do
    935       let input = "find (x : equal? x 2) [(1) (2) (3)]"
    936           env = evalTricu allTestLibsEnv (parseTricu input)
    937       result env @?= justT (ofNumber 2)
    938 
    939   , testCase "find missing element" $ do
    940       let input = "find (x : equal? x 9) [(1) (2) (3)]"
    941           env = evalTricu allTestLibsEnv (parseTricu input)
    942       result env @?= nothingT
    943 
    944   , testCase "partition empty list" $ do
    945       let input = "partition (x : equal? x 2) []"
    946           env = evalTricu allTestLibsEnv (parseTricu input)
    947       result env @?= pairT (ofList []) (ofList [])
    948 
    949   , testCase "partition splits list" $ do
    950       let input = "partition (x : lt? 2 x) [(1) (2) (3) (4)]"
    951           env = evalTricu allTestLibsEnv (parseTricu input)
    952       result env @?= pairT (ofList [ofNumber 3, ofNumber 4]) (ofList [ofNumber 1, ofNumber 2])
    953 
    954   , testCase "zipWith on empty lists" $ do
    955       let input = "zipWith add [] []"
    956           env = evalTricu allTestLibsEnv (parseTricu input)
    957       result env @?= ofList []
    958 
    959   , testCase "zipWith adds pairwise" $ do
    960       let input = "zipWith add [(1) (2)] [(10) (20)]"
    961           env = evalTricu allTestLibsEnv (parseTricu input)
    962       result env @?= ofList [ofNumber 11, ofNumber 22]
    963 
    964   , testCase "zipWith truncates to shorter list" $ do
    965       let input = "zipWith add [(1) (2)] [(10)]"
    966           env = evalTricu allTestLibsEnv (parseTricu input)
    967       result env @?= ofList [ofNumber 11]
    968 
    969   , testCase "strLength" $ do
    970       let input = "strLength \"hello\""
    971           env = evalTricu allTestLibsEnv (parseTricu input)
    972       result env @?= ofNumber 5
    973 
    974   , testCase "strAppend" $ do
    975       let input = "strAppend \"hello\" \" world\""
    976           env = evalTricu allTestLibsEnv (parseTricu input)
    977       result env @?= ofString "hello world"
    978 
    979   , testCase "equal? equal strings" $ do
    980       let input = "equal? \"abc\" \"abc\""
    981           env = evalTricu allTestLibsEnv (parseTricu input)
    982       result env @?= trueT
    983 
    984   , testCase "equal? different strings" $ do
    985       let input = "equal? \"abc\" \"def\""
    986           env = evalTricu allTestLibsEnv (parseTricu input)
    987       result env @?= falseT
    988 
    989   , testCase "strEmpty? on empty" $ do
    990       let input = "strEmpty? \"\""
    991           env = evalTricu allTestLibsEnv (parseTricu input)
    992       result env @?= trueT
    993 
    994   , testCase "strEmpty? on non-empty" $ do
    995       let input = "strEmpty? \"a\""
    996           env = evalTricu allTestLibsEnv (parseTricu input)
    997       result env @?= falseT
    998 
    999   , testCase "startsWith? prefix matches" $ do
   1000       let input = "startsWith? \"he\" \"hello\""
   1001           env = evalTricu allTestLibsEnv (parseTricu input)
   1002       result env @?= trueT
   1003 
   1004   , testCase "startsWith? prefix too long" $ do
   1005       let input = "startsWith? \"hello\" \"he\""
   1006           env = evalTricu allTestLibsEnv (parseTricu input)
   1007       result env @?= falseT
   1008 
   1009   , testCase "startsWith? empty prefix" $ do
   1010       let input = "startsWith? \"\" \"hello\""
   1011           env = evalTricu allTestLibsEnv (parseTricu input)
   1012       result env @?= trueT
   1013 
   1014   , testCase "endsWith? suffix matches" $ do
   1015       let input = "endsWith? \"lo\" \"hello\""
   1016           env = evalTricu allTestLibsEnv (parseTricu input)
   1017       result env @?= trueT
   1018 
   1019   , testCase "endsWith? suffix too long" $ do
   1020       let input = "endsWith? \"hello\" \"lo\""
   1021           env = evalTricu allTestLibsEnv (parseTricu input)
   1022       result env @?= falseT
   1023 
   1024   , testCase "endsWith? empty suffix" $ do
   1025       let input = "endsWith? \"\" \"hello\""
   1026           env = evalTricu allTestLibsEnv (parseTricu input)
   1027       result env @?= trueT
   1028 
   1029   , testCase "contains? substring found" $ do
   1030       let input = "contains? \"ell\" \"hello\""
   1031           env = evalTricu allTestLibsEnv (parseTricu input)
   1032       result env @?= trueT
   1033 
   1034   , testCase "contains? substring missing" $ do
   1035       let input = "contains? \"xyz\" \"hello\""
   1036           env = evalTricu allTestLibsEnv (parseTricu input)
   1037       result env @?= falseT
   1038 
   1039   , testCase "contains? empty needle" $ do
   1040       let input = "contains? \"\" \"hello\""
   1041           env = evalTricu allTestLibsEnv (parseTricu input)
   1042       result env @?= trueT
   1043 
   1044   , testCase "lines splits on newline" $ do
   1045       let input = "lines \"a\\nb\\nc\""
   1046           env = evalTricu allTestLibsEnv (parseTricu input)
   1047       result env @?= ofList [ofString "a", ofString "b", ofString "c"]
   1048 
   1049   , testCase "lines single line" $ do
   1050       let input = "lines \"hello\""
   1051           env = evalTricu allTestLibsEnv (parseTricu input)
   1052       result env @?= ofList [ofString "hello"]
   1053 
   1054   , testCase "lines empty string" $ do
   1055       let input = "lines \"\""
   1056           env = evalTricu allTestLibsEnv (parseTricu input)
   1057       result env @?= ofList [ofString ""]
   1058 
   1059   , testCase "lines trailing newline" $ do
   1060       let input = "lines \"a\\n\""
   1061           env = evalTricu allTestLibsEnv (parseTricu input)
   1062       result env @?= ofList [ofString "a", ofString ""]
   1063 
   1064   , testCase "unlines joins with newline" $ do
   1065       let input = "unlines [(\"a\") (\"b\")]"
   1066           env = evalTricu allTestLibsEnv (parseTricu input)
   1067       result env @?= ofString "a\nb\n"
   1068 
   1069   , testCase "unlines empty list" $ do
   1070       let input = "unlines []"
   1071           env = evalTricu allTestLibsEnv (parseTricu input)
   1072       result env @?= ofString ""
   1073 
   1074   , testCase "words splits on space" $ do
   1075       let input = "words \"hello world\""
   1076           env = evalTricu allTestLibsEnv (parseTricu input)
   1077       result env @?= ofList [ofString "hello", ofString "world"]
   1078 
   1079   , testCase "words empty string" $ do
   1080       let input = "words \"\""
   1081           env = evalTricu allTestLibsEnv (parseTricu input)
   1082       result env @?= ofList []
   1083 
   1084   , testCase "words multiple spaces" $ do
   1085       let input = "words \"  hello  world  \""
   1086           env = evalTricu allTestLibsEnv (parseTricu input)
   1087       result env @?= ofList [ofString "hello", ofString "world"]
   1088 
   1089   , testCase "unwords joins with space" $ do
   1090       let input = "unwords [(\"hello\") (\"world\")]"
   1091           env = evalTricu allTestLibsEnv (parseTricu input)
   1092       result env @?= ofString "hello world"
   1093 
   1094   , testCase "unwords single word" $ do
   1095       let input = "unwords [(\"hello\")]"
   1096           env = evalTricu allTestLibsEnv (parseTricu input)
   1097       result env @?= ofString "hello"
   1098 
   1099   , testCase "unwords empty list" $ do
   1100       let input = "unwords []"
   1101           env = evalTricu allTestLibsEnv (parseTricu input)
   1102       result env @?= ofString ""
   1103 
   1104   , testCase "intercalate joins fields" $ do
   1105       let input = "intercalate \", \" [(\"a\") (\"b\") (\"c\")]"
   1106           env = evalTricu allTestLibsEnv (parseTricu input)
   1107       result env @?= ofString "a, b, c"
   1108 
   1109   , testCase "intercalate leaves a lone field alone" $ do
   1110       let input = "intercalate \", \" [(\"a\")]"
   1111           env = evalTricu allTestLibsEnv (parseTricu input)
   1112       result env @?= ofString "a"
   1113 
   1114   , testCase "intercalate empty list" $ do
   1115       let input = "intercalate \", \" []"
   1116           env = evalTricu allTestLibsEnv (parseTricu input)
   1117       result env @?= ofString ""
   1118 
   1119   , testCase "joinSuffix terminates every field" $ do
   1120       let input = "joinSuffix \"-\" [(\"a\") (\"b\")]"
   1121           env = evalTricu allTestLibsEnv (parseTricu input)
   1122       result env @?= ofString "a-b-"
   1123 
   1124   , testCase "splitOnByte splits on a byte" $ do
   1125       let input = "splitOnByte 58 \"a:b:c\""
   1126           env = evalTricu allTestLibsEnv (parseTricu input)
   1127       result env @?= ofList [ofString "a", ofString "b", ofString "c"]
   1128 
   1129   , testCase "splitOnByte keeps empty fields" $ do
   1130       let input = "splitOnByte 58 \"a::b\""
   1131           env = evalTricu allTestLibsEnv (parseTricu input)
   1132       result env @?= ofList [ofString "a", ofString "", ofString "b"]
   1133 
   1134   , testCase "splitOnByte trailing separator leaves an empty field" $ do
   1135       let input = "splitOnByte 58 \"a:\""
   1136           env = evalTricu allTestLibsEnv (parseTricu input)
   1137       result env @?= ofList [ofString "a", ofString ""]
   1138 
   1139   , testCase "splitOnByte without a match" $ do
   1140       let input = "splitOnByte 58 \"abc\""
   1141           env = evalTricu allTestLibsEnv (parseTricu input)
   1142       result env @?= ofList [ofString "abc"]
   1143 
   1144   , testCase "splitOnByte empty input" $ do
   1145       let input = "splitOnByte 58 \"\""
   1146           env = evalTricu allTestLibsEnv (parseTricu input)
   1147       result env @?= ofList [ofString ""]
   1148 
   1149   , testCase "intercalate round trips splitOnByte" $ do
   1150       let input = "equal? (intercalate \":\" (splitOnByte 58 \"a:b:c\")) \"a:b:c\""
   1151           env = evalTricu allTestLibsEnv (parseTricu input)
   1152       result env @?= trueT
   1153 
   1154   , testCase "takeWhile keeps the matching prefix" $ do
   1155       let input = "takeWhile (n : lt? n 3) [(1) (2) (3) (1)]"
   1156           env = evalTricu allTestLibsEnv (parseTricu input)
   1157       result env @?= ofList [ofNumber 1, ofNumber 2]
   1158 
   1159   , testCase "takeWhile stops at the first mismatch" $ do
   1160       let input = "takeWhile (n : lt? n 3) [(3) (1)]"
   1161           env = evalTricu allTestLibsEnv (parseTricu input)
   1162       result env @?= ofList []
   1163 
   1164   , testCase "dropWhile drops the matching prefix" $ do
   1165       let input = "dropWhile (n : lt? n 3) [(1) (2) (3) (1)]"
   1166           env = evalTricu allTestLibsEnv (parseTricu input)
   1167       result env @?= ofList [ofNumber 3, ofNumber 1]
   1168 
   1169   , testCase "dropWhile on an all matching list" $ do
   1170       let input = "dropWhile (n : lt? n 3) [(1) (2)]"
   1171           env = evalTricu allTestLibsEnv (parseTricu input)
   1172       result env @?= ofList []
   1173 
   1174   , testCase "trim strips surrounding spaces and tabs" $ do
   1175       let input = "trim \"  \\ttrimmed  \\t\""
   1176           env = evalTricu allTestLibsEnv (parseTricu input)
   1177       result env @?= ofString "trimmed"
   1178 
   1179   , testCase "trim leaves interior bytes alone" $ do
   1180       let input = "trim \" a b \""
   1181           env = evalTricu allTestLibsEnv (parseTricu input)
   1182       result env @?= ofString "a b"
   1183 
   1184   , testCase "trim all whitespace is empty" $ do
   1185       let input = "trim \" \\t \""
   1186           env = evalTricu allTestLibsEnv (parseTricu input)
   1187       result env @?= ofString ""
   1188   ]
   1189 
   1190 contractsTests :: TestTree
   1191 contractsTests = testGroup "Contracts library tests"
   1192   [ testCase "anyC passes any value" $ do
   1193       let input = "main = resultIsOk (checkContract anyC 7)"
   1194           env = evalTricu allTestLibsEnv (parseTricu input)
   1195       result env @?= trueT
   1196 
   1197   , testCase "guardC rejects failures" $ do
   1198       let input = "main = resultIsErr (checkContract (guardC \"odd\" even?) 5)"
   1199           env = evalTricu allTestLibsEnv (parseTricu input)
   1200       result env @?= trueT
   1201 
   1202   , testCase "andC combines contracts" $ do
   1203       let input = "main = resultIsOk (checkContract (andC (guardC \"odd\" odd?) (guardC \"gt0\" (n : gt? n 0))) 5)"
   1204           env = evalTricu allTestLibsEnv (parseTricu input)
   1205       result env @?= trueT
   1206 
   1207   , testCase "listOf checks every element" $ do
   1208       let input = "main = resultIsOk (checkContract (listOf (guardC \"odd\" odd?)) [1 3 5])"
   1209           env = evalTricu allTestLibsEnv (parseTricu input)
   1210       result env @?= trueT
   1211 
   1212   , testCase "fnContract wraps a function" $ do
   1213       let input = "main = resultIsOk (checkContract (fnContract (guardC \"odd\" odd?) anyC) (x : x))"
   1214           env = evalTricu allTestLibsEnv (parseTricu input)
   1215       result env @?= trueT
   1216 
   1217   , testCase "runM reduces pureM" $ do
   1218       let input = "main = resultIsOk (runM (pureM 42))"
   1219           env = evalTricu allTestLibsEnv (parseTricu input)
   1220       result env @?= trueT
   1221 
   1222   , testCase "do notation chains checkM and pureM" $ do
   1223       let input = unlines
   1224             [ "program = (xs :"
   1225             , "  do bindM"
   1226             , "    n <- pureM (length xs)"
   1227             , "    _ <- checkM (guardC \"positive\" (n : gte? n 1)) n"
   1228             , "    pureM n)"
   1229             , "main = runM (program [1 2 3 4])"
   1230             ]
   1231           env = evalTricu allTestLibsEnv (parseTricu input)
   1232       decodeResult (result env) @?= "[t t, 4]"
   1233 
   1234   , testCase "handleM replaces exception with a new action" $ do
   1235       let input = unlines
   1236             [ "lookupConfig = (key defaultValue :"
   1237             , "  exceptE \"missing\" key (resume : pureM defaultValue))"
   1238             , "pipeline = (xs :"
   1239             , "  do bindM"
   1240             , "    divisor <- lookupConfig \"divisor\" 1"
   1241             , "    total <- pureM (sum xs)"
   1242             , "    scaled <- liftM (x : div x divisor) total"
   1243             , "    pureM scaled)"
   1244             , "handled = handleM \"missing\" (key k : pureM 2) (pipeline [10 20 30])"
   1245             , "main = runM handled"
   1246             ]
   1247           env = evalTricu allTestLibsEnv (parseTricu input)
   1248       decodeResult (result env) @?= "[t t, 30]"
   1249 
   1250   , testCase "=@ return annotation passes" $ do
   1251       let input = unlines
   1252             [ "five =@anyC 5"
   1253             , "main = five"
   1254             ]
   1255           env = evalTricu allTestLibsEnv (parseTricu input)
   1256       result env @?= ofNumber 5
   1257 
   1258   , testCase "=@ return annotation fails" $ do
   1259       let input = unlines
   1260             [ "boom =@(neverC \"boom\") 5"
   1261             , "main = boom"
   1262             ]
   1263           env = evalTricu allTestLibsEnv (parseTricu input)
   1264       decodeResult (result env) @?= "[t, \"boom\"]"
   1265 
   1266   , testCase "@ argument annotation passes" $ do
   1267       let input = unlines
   1268             [ "idNat x@anyC =@anyC x"
   1269             , "main = idNat 5"
   1270             ]
   1271           env = evalTricu allTestLibsEnv (parseTricu input)
   1272       result env @?= ofNumber 5
   1273 
   1274   , testCase "@ argument annotation fails" $ do
   1275       let input = unlines
   1276             [ "idNat x@(neverC \"bad\") =@anyC x"
   1277             , "main = idNat 5"
   1278             ]
   1279           env = evalTricu allTestLibsEnv (parseTricu input)
   1280       decodeResult (result env) @?= "[t, \"bad\"]"
   1281   ]
   1282 
   1283 arithmetic :: TestTree
   1284 arithmetic = testGroup "Arithmetic Tests"
   1285   [ testCase "isZero? on 0" $ do
   1286       let input = "isZero? 0"
   1287           env = evalTricu allTestLibsEnv (parseTricu input)
   1288       result env @?= trueT
   1289 
   1290   , testCase "isZero? on 5" $ do
   1291       let input = "isZero? 5"
   1292           env = evalTricu allTestLibsEnv (parseTricu input)
   1293       result env @?= falseT
   1294 
   1295   , testCase "add 0 3 = 3" $ do
   1296       let input = "add 0 3"
   1297           env = evalTricu allTestLibsEnv (parseTricu input)
   1298       result env @?= ofNumber 3
   1299 
   1300   , testCase "add 3 0 = 3" $ do
   1301       let input = "add 3 0"
   1302           env = evalTricu allTestLibsEnv (parseTricu input)
   1303       result env @?= ofNumber 3
   1304 
   1305   , testCase "add 2 3 = 5" $ do
   1306       let input = "add 2 3"
   1307           env = evalTricu allTestLibsEnv (parseTricu input)
   1308       result env @?= ofNumber 5
   1309 
   1310   , testCase "sub 5 2 = 3" $ do
   1311       let input = "sub 5 2"
   1312           env = evalTricu allTestLibsEnv (parseTricu input)
   1313       result env @?= ofNumber 3
   1314 
   1315   , testCase "sub 2 5 = 0 (saturated)" $ do
   1316       let input = "sub 2 5"
   1317           env = evalTricu allTestLibsEnv (parseTricu input)
   1318       result env @?= ofNumber 0
   1319 
   1320   , testCase "sub 5 5 = 0" $ do
   1321       let input = "sub 5 5"
   1322           env = evalTricu allTestLibsEnv (parseTricu input)
   1323       result env @?= ofNumber 0
   1324 
   1325   , testCase "lt? 2 3 = true" $ do
   1326       let input = "lt? 2 3"
   1327           env = evalTricu allTestLibsEnv (parseTricu input)
   1328       result env @?= trueT
   1329 
   1330   , testCase "lt? 3 2 = false" $ do
   1331       let input = "lt? 3 2"
   1332           env = evalTricu allTestLibsEnv (parseTricu input)
   1333       result env @?= falseT
   1334 
   1335   , testCase "lt? 2 2 = false" $ do
   1336       let input = "lt? 2 2"
   1337           env = evalTricu allTestLibsEnv (parseTricu input)
   1338       result env @?= falseT
   1339 
   1340   , testCase "lte? 2 3 = true" $ do
   1341       let input = "lte? 2 3"
   1342           env = evalTricu allTestLibsEnv (parseTricu input)
   1343       result env @?= trueT
   1344 
   1345   , testCase "lte? 3 2 = false" $ do
   1346       let input = "lte? 3 2"
   1347           env = evalTricu allTestLibsEnv (parseTricu input)
   1348       result env @?= falseT
   1349 
   1350   , testCase "lte? 2 2 = true" $ do
   1351       let input = "lte? 2 2"
   1352           env = evalTricu allTestLibsEnv (parseTricu input)
   1353       result env @?= trueT
   1354 
   1355   , testCase "mul 0 5 = 0" $ do
   1356       let input = "mul 0 5"
   1357           env = evalTricu allTestLibsEnv (parseTricu input)
   1358       result env @?= ofNumber 0
   1359 
   1360   , testCase "mul 5 0 = 0" $ do
   1361       let input = "mul 5 0"
   1362           env = evalTricu allTestLibsEnv (parseTricu input)
   1363       result env @?= ofNumber 0
   1364 
   1365   , testCase "mul 2 3 = 6" $ do
   1366       let input = "mul 2 3"
   1367           env = evalTricu allTestLibsEnv (parseTricu input)
   1368       result env @?= ofNumber 6
   1369 
   1370   , testCase "mul 3 3 = 9" $ do
   1371       let input = "mul 3 3"
   1372           env = evalTricu allTestLibsEnv (parseTricu input)
   1373       result env @?= ofNumber 9
   1374 
   1375   , testCase "pred 0 = 0" $ do
   1376       let input = "pred 0"
   1377           env = evalTricu allTestLibsEnv (parseTricu input)
   1378       result env @?= ofNumber 0
   1379 
   1380   , testCase "pred 1 = 0" $ do
   1381       let input = "pred 1"
   1382           env = evalTricu allTestLibsEnv (parseTricu input)
   1383       result env @?= ofNumber 0
   1384 
   1385   , testCase "pred 5 = 4" $ do
   1386       let input = "pred 5"
   1387           env = evalTricu allTestLibsEnv (parseTricu input)
   1388       result env @?= ofNumber 4
   1389 
   1390   , testCase "add is commutative" $ do
   1391       let input = "equal? (add 4 7) (add 7 4)"
   1392           env = evalTricu allTestLibsEnv (parseTricu input)
   1393       result env @?= trueT
   1394 
   1395   , testCase "add is associative" $ do
   1396       let input = "equal? (add (add 2 3) 4) (add 2 (add 3 4))"
   1397           env = evalTricu allTestLibsEnv (parseTricu input)
   1398       result env @?= trueT
   1399 
   1400   , testCase "sub x 0 = x" $ do
   1401       let input = "sub 7 0"
   1402           env = evalTricu allTestLibsEnv (parseTricu input)
   1403       result env @?= ofNumber 7
   1404 
   1405   , testCase "sub chained" $ do
   1406       let input = "sub (sub 10 3) 2"
   1407           env = evalTricu allTestLibsEnv (parseTricu input)
   1408       result env @?= ofNumber 5
   1409 
   1410   , testCase "mul identity 1" $ do
   1411       let input = "mul 1 5"
   1412           env = evalTricu allTestLibsEnv (parseTricu input)
   1413       result env @?= ofNumber 5
   1414 
   1415   , testCase "mul identity 2" $ do
   1416       let input = "mul 5 1"
   1417           env = evalTricu allTestLibsEnv (parseTricu input)
   1418       result env @?= ofNumber 5
   1419 
   1420   , testCase "mul is commutative" $ do
   1421       let input = "equal? (mul 3 4) (mul 4 3)"
   1422           env = evalTricu allTestLibsEnv (parseTricu input)
   1423       result env @?= trueT
   1424 
   1425   , testCase "mul is associative" $ do
   1426       let input = "equal? (mul (mul 2 3) 4) (mul 2 (mul 3 4))"
   1427           env = evalTricu allTestLibsEnv (parseTricu input)
   1428       result env @?= trueT
   1429 
   1430   , testCase "mul distributes over add" $ do
   1431       let input = "equal? (mul 2 (add 3 4)) (add (mul 2 3) (mul 2 4))"
   1432           env = evalTricu allTestLibsEnv (parseTricu input)
   1433       result env @?= trueT
   1434 
   1435   , testCase "lt? reflexive is false" $ do
   1436       let input = "lt? 5 5"
   1437           env = evalTricu allTestLibsEnv (parseTricu input)
   1438       result env @?= falseT
   1439 
   1440   , testCase "lte? reflexive is true" $ do
   1441       let input = "lte? 5 5"
   1442           env = evalTricu allTestLibsEnv (parseTricu input)
   1443       result env @?= trueT
   1444 
   1445   , testCase "lt? transitivity" $ do
   1446       let input = "and? (lt? 2 5) (lt? 5 7)"
   1447           env = evalTricu allTestLibsEnv (parseTricu input)
   1448       result env @?= trueT
   1449 
   1450   , testCase "add larger numbers" $ do
   1451       let input = "add 12 15"
   1452           env = evalTricu allTestLibsEnv (parseTricu input)
   1453       result env @?= ofNumber 27
   1454 
   1455   , testCase "mul larger numbers" $ do
   1456       let input = "mul 5 6"
   1457           env = evalTricu allTestLibsEnv (parseTricu input)
   1458       result env @?= ofNumber 30
   1459 
   1460   , testCase "isZero? on add 0 0" $ do
   1461       let input = "isZero? (add 0 0)"
   1462           env = evalTricu allTestLibsEnv (parseTricu input)
   1463       result env @?= trueT
   1464 
   1465   , testCase "div 10 3 = 3" $ do
   1466       let input = "div 10 3"
   1467           env = evalTricu allTestLibsEnv (parseTricu input)
   1468       result env @?= ofNumber 3
   1469 
   1470   , testCase "div 12 4 = 3 (exact)" $ do
   1471       let input = "div 12 4"
   1472           env = evalTricu allTestLibsEnv (parseTricu input)
   1473       result env @?= ofNumber 3
   1474 
   1475   , testCase "div 3 5 = 0 (divisor larger)" $ do
   1476       let input = "div 3 5"
   1477           env = evalTricu allTestLibsEnv (parseTricu input)
   1478       result env @?= ofNumber 0
   1479 
   1480   , testCase "div 7 1 = 7 (identity)" $ do
   1481       let input = "div 7 1"
   1482           env = evalTricu allTestLibsEnv (parseTricu input)
   1483       result env @?= ofNumber 7
   1484 
   1485   , testCase "div 0 5 = 0" $ do
   1486       let input = "div 0 5"
   1487           env = evalTricu allTestLibsEnv (parseTricu input)
   1488       result env @?= ofNumber 0
   1489 
   1490   , testCase "div 5 0 = 0 (div by zero)" $ do
   1491       let input = "div 5 0"
   1492           env = evalTricu allTestLibsEnv (parseTricu input)
   1493       result env @?= ofNumber 0
   1494 
   1495   , testCase "mod 10 3 = 1" $ do
   1496       let input = "mod 10 3"
   1497           env = evalTricu allTestLibsEnv (parseTricu input)
   1498       result env @?= ofNumber 1
   1499 
   1500   , testCase "mod 12 4 = 0 (exact)" $ do
   1501       let input = "mod 12 4"
   1502           env = evalTricu allTestLibsEnv (parseTricu input)
   1503       result env @?= ofNumber 0
   1504 
   1505   , testCase "mod 3 5 = 3 (divisor larger)" $ do
   1506       let input = "mod 3 5"
   1507           env = evalTricu allTestLibsEnv (parseTricu input)
   1508       result env @?= ofNumber 3
   1509 
   1510   , testCase "mod 7 1 = 0" $ do
   1511       let input = "mod 7 1"
   1512           env = evalTricu allTestLibsEnv (parseTricu input)
   1513       result env @?= ofNumber 0
   1514 
   1515   , testCase "mod 5 0 = 0 (mod by zero)" $ do
   1516       let input = "mod 5 0"
   1517           env = evalTricu allTestLibsEnv (parseTricu input)
   1518       result env @?= ofNumber 0
   1519 
   1520   , testCase "div mod consistency" $ do
   1521       let input = "equal? (add (mul 3 7) 4) 25"
   1522           env = evalTricu allTestLibsEnv (parseTricu input)
   1523       result env @?= trueT
   1524 
   1525   , testCase "pow 2 0 = 1" $ do
   1526       let input = "pow 2 0"
   1527           env = evalTricu allTestLibsEnv (parseTricu input)
   1528       result env @?= ofNumber 1
   1529 
   1530   , testCase "pow 2 3 = 8" $ do
   1531       let input = "pow 2 3"
   1532           env = evalTricu allTestLibsEnv (parseTricu input)
   1533       result env @?= ofNumber 8
   1534 
   1535   , testCase "pow 3 2 = 9" $ do
   1536       let input = "pow 3 2"
   1537           env = evalTricu allTestLibsEnv (parseTricu input)
   1538       result env @?= ofNumber 9
   1539 
   1540   , testCase "pow 0 0 = 1" $ do
   1541       let input = "pow 0 0"
   1542           env = evalTricu allTestLibsEnv (parseTricu input)
   1543       result env @?= ofNumber 1
   1544 
   1545   , testCase "pow 0 5 = 0" $ do
   1546       let input = "pow 0 5"
   1547           env = evalTricu allTestLibsEnv (parseTricu input)
   1548       result env @?= ofNumber 0
   1549 
   1550   , testCase "pow 1 10 = 1" $ do
   1551       let input = "pow 1 10"
   1552           env = evalTricu allTestLibsEnv (parseTricu input)
   1553       result env @?= ofNumber 1
   1554 
   1555   , testCase "pow 5 1 = 5" $ do
   1556       let input = "pow 5 1"
   1557           env = evalTricu allTestLibsEnv (parseTricu input)
   1558       result env @?= ofNumber 5
   1559 
   1560   , testCase "min 3 7 = 3" $ do
   1561       let input = "min 3 7"
   1562           env = evalTricu allTestLibsEnv (parseTricu input)
   1563       result env @?= ofNumber 3
   1564 
   1565   , testCase "min 7 3 = 3" $ do
   1566       let input = "min 7 3"
   1567           env = evalTricu allTestLibsEnv (parseTricu input)
   1568       result env @?= ofNumber 3
   1569 
   1570   , testCase "min 5 5 = 5" $ do
   1571       let input = "min 5 5"
   1572           env = evalTricu allTestLibsEnv (parseTricu input)
   1573       result env @?= ofNumber 5
   1574 
   1575   , testCase "min 0 5 = 0" $ do
   1576       let input = "min 0 5"
   1577           env = evalTricu allTestLibsEnv (parseTricu input)
   1578       result env @?= ofNumber 0
   1579 
   1580   , testCase "max 3 7 = 7" $ do
   1581       let input = "max 3 7"
   1582           env = evalTricu allTestLibsEnv (parseTricu input)
   1583       result env @?= ofNumber 7
   1584 
   1585   , testCase "max 7 3 = 7" $ do
   1586       let input = "max 7 3"
   1587           env = evalTricu allTestLibsEnv (parseTricu input)
   1588       result env @?= ofNumber 7
   1589 
   1590   , testCase "max 5 5 = 5" $ do
   1591       let input = "max 5 5"
   1592           env = evalTricu allTestLibsEnv (parseTricu input)
   1593       result env @?= ofNumber 5
   1594 
   1595   , testCase "max 0 5 = 5" $ do
   1596       let input = "max 0 5"
   1597           env = evalTricu allTestLibsEnv (parseTricu input)
   1598       result env @?= ofNumber 5
   1599 
   1600   , testCase "even? 0 = true" $ do
   1601       let input = "even? 0"
   1602           env = evalTricu allTestLibsEnv (parseTricu input)
   1603       result env @?= trueT
   1604 
   1605   , testCase "even? 1 = false" $ do
   1606       let input = "even? 1"
   1607           env = evalTricu allTestLibsEnv (parseTricu input)
   1608       result env @?= falseT
   1609 
   1610   , testCase "even? 2 = true" $ do
   1611       let input = "even? 2"
   1612           env = evalTricu allTestLibsEnv (parseTricu input)
   1613       result env @?= trueT
   1614 
   1615   , testCase "even? 7 = false" $ do
   1616       let input = "even? 7"
   1617           env = evalTricu allTestLibsEnv (parseTricu input)
   1618       result env @?= falseT
   1619 
   1620   , testCase "odd? 0 = false" $ do
   1621       let input = "odd? 0"
   1622           env = evalTricu allTestLibsEnv (parseTricu input)
   1623       result env @?= falseT
   1624 
   1625   , testCase "odd? 1 = true" $ do
   1626       let input = "odd? 1"
   1627           env = evalTricu allTestLibsEnv (parseTricu input)
   1628       result env @?= trueT
   1629 
   1630   , testCase "odd? 2 = false" $ do
   1631       let input = "odd? 2"
   1632           env = evalTricu allTestLibsEnv (parseTricu input)
   1633       result env @?= falseT
   1634 
   1635   , testCase "odd? 7 = true" $ do
   1636       let input = "odd? 7"
   1637           env = evalTricu allTestLibsEnv (parseTricu input)
   1638       result env @?= trueT
   1639   ]
   1640 
   1641 fileEval :: TestTree
   1642 fileEval = testGroup "File evaluation tests"
   1643   [ testCase "Forks" $ do
   1644       res <- liftIO $ evaluateFileResult "./test/fork.tri"
   1645       res @?= Fork Leaf Leaf
   1646 
   1647   , testCase "File ends with comment" $ do
   1648       res <- liftIO $ evaluateFileResult "./test/comments-1.tri"
   1649       res @?= Fork (Stem Leaf) Leaf
   1650 
   1651   , testCase "Mapping and Equality" $ do
   1652       fEnv    <- liftIO $ evaluateFileWithContext allTestLibsEnv "./test/map.tri"
   1653       (mainResult fEnv) @?= Stem Leaf
   1654 
   1655   , testCase "Eval and decoding string" $ do
   1656       res <- liftIO $ evaluateFileWithContext allTestLibsEnv "./test/string.tri"
   1657       decodeResult (result res) @?= "\"String test!\""
   1658   ]
   1659 
   1660 -- All of our demo tests are also module tests
   1661 demos :: TestTree
   1662 demos = testGroup "Test provided demo functionality"
   1663   [ testCase "Structural equality demo" $ do
   1664       res     <- liftIO $ evaluateFileResult "./demos/equality.tri"
   1665       decodeResult res @?= "t t"
   1666   , testCase "Convert values back to source code demo" $ do
   1667       res     <- liftIO $ evaluateFileResult "./demos/toSource.tri"
   1668       decodeResult res @?= "\"(t (t (t t) (t t t)) (t t (t t t)))\""
   1669   , testCase "Determining the size of functions" $ do
   1670       res     <- liftIO $ evaluateFileResult "./demos/size.tri"
   1671       decodeResult res @?= "321"
   1672   , testCase "Level Order Traversal demo" $ do
   1673       res     <- liftIO $ evaluateFileResult "./demos/levelOrderTraversal.tri"
   1674       decodeResult res @?= "\"\n1 \n2 3 \n4 5 6 7 \n8 11 10 9 12 \""
   1675   , testCase "Contract effect demo with do notation" $ do
   1676       res     <- liftIO $ evaluateFileResult "./demos/contractEffects.tri"
   1677       decodeResult res @?= "[t t, 10]"
   1678   , testCase "Safe base wrappers demo" $ do
   1679       res     <- liftIO $ evaluateFileResult "./demos/contractBasics.tri"
   1680       decodeResult res @?= "[3, t t, t, t t]"
   1681   ]
   1682 
   1683 decoding :: TestTree
   1684 decoding = testGroup "Decoding Tests"
   1685   [ testCase "Decode Leaf" $ do
   1686       decodeResult Leaf @?= "t"
   1687 
   1688   , testCase "Decode list of non-ASCII numbers" $ do
   1689       let input = ofList [ofNumber 1, ofNumber 14, ofNumber 6]
   1690       decodeResult input @?= "[1, 14, 6]"
   1691 
   1692   , testCase "Decode list of ASCII numbers as a string" $ do
   1693       let input = ofList [ofNumber 97, ofNumber 98, ofNumber 99]
   1694       decodeResult input @?= "\"abc\""
   1695 
   1696   , testCase "Decode small number" $ do
   1697       decodeResult (ofNumber 42) @?= "42"
   1698 
   1699   , testCase "Decode large number" $ do
   1700       decodeResult (ofNumber 9999) @?= "9999"
   1701 
   1702   , testCase "Decode string in list" $ do
   1703       let input = ofList [ofString "hello", ofString "world"]
   1704       decodeResult input @?= "[\"hello\", \"world\"]"
   1705 
   1706   , testCase "Decode mixed list with strings" $ do
   1707       let input = ofList [ofString "hello", ofNumber 42, ofString "world"]
   1708       decodeResult input @?= "[\"hello\", 42, \"world\"]"
   1709 
   1710   , testCase "Decode nested lists with strings" $ do
   1711       let input = ofList [ofList [ofString "nested"], ofString "string"]
   1712       decodeResult input @?= "[[\"nested\"], \"string\"]"
   1713   ]
   1714 
   1715 elimLambdaSingle :: TestTree
   1716 elimLambdaSingle = testCase "elimLambda preserves eval, fires eta, and SDef binds" $ do
   1717   -- 1) eta reduction, purely structural and parsed from source
   1718   let [etaIn] = parseTricu "x : f x"
   1719       [fRef ] = parseTricu "f"
   1720   elimLambda etaIn @?= fRef
   1721 
   1722   -- 2) SDef binds its own name and parameters
   1723   let [defFXY] = parseTricu "f x y : f x"
   1724       fv       = freeVars defFXY
   1725   assertBool "f should be bound in SDef" ("f" `Set.notMember` fv)
   1726   assertBool "x should be bound in SDef" ("x" `Set.notMember` fv)
   1727   assertBool "y should be bound in SDef" ("y" `Set.notMember` fv)
   1728 
   1729   -- 3) semantics preserved on a small program that exercises compose and triage
   1730   let src =
   1731         unlines
   1732           [ "false = t"
   1733           , "_     = t"
   1734           , "true  = t t"
   1735           , "id    = a : a"
   1736           , "const = a b : a"
   1737           , "compose = f g x : f (g x)"
   1738           , "triage = leaf stem fork : t (t leaf stem) fork"
   1739           , "test   = triage \"Leaf\" (_ : \"Stem\") (_ _ : \"Fork\")"
   1740           , "main   = compose id id test"
   1741           ]
   1742       prog        = parseTricu src
   1743       progElim    = map elimLambda prog
   1744       evalBefore  = result (evalTricu Map.empty prog)
   1745       evalAfter   = result (evalTricu Map.empty progElim)
   1746   evalAfter @?= evalBefore
   1747 
   1748 stressElimLambda :: TestTree
   1749 stressElimLambda = testCase "stress elimLambda on wide list under deep curried lambda" $ do
   1750   let numVars = 200
   1751       numBody = 800
   1752       vars    = [ "x" ++ show i | i <- [1..numVars] ]
   1753       body    = "(" ++ unwords (replicate numBody "t") ++ ")"
   1754       etaOne  = "h : f h"
   1755       etaTwo  = "k : id k"
   1756       defId   = "id = a : a"
   1757       lambda  = unwords vars ++ " : " ++ body
   1758       src     = unlines
   1759                   [ defId
   1760                   , etaOne
   1761                   , "compose = f g x : f (g x)"
   1762                   , "f = t t"
   1763                   , etaTwo
   1764                   , lambda
   1765                   , "main = compose id id (" ++ head vars ++ " : f " ++ head vars ++ ")"
   1766                   ]
   1767       prog    = parseTricu src
   1768 
   1769   let out = map elimLambda prog
   1770   let noLambda term = case term of
   1771         SLambda _ _ -> False
   1772         SApp f g    -> noLambda f && noLambda g
   1773         SList xs    -> all noLambda xs
   1774         TFork l r   -> noLambda l && noLambda r
   1775         TStem u     -> noLambda u
   1776         _           -> True
   1777 
   1778   assertBool "all lambdas eliminated" (all noLambda out)
   1779 
   1780   let before = result (evalTricu Map.empty prog)
   1781       after  = result (evalTricu Map.empty out)
   1782   after @?= before
   1783 
   1784 -- --------------------------------------------------------------------------
   1785 -- Byte marshalling tests
   1786 -- --------------------------------------------------------------------------
   1787 
   1788 byteMarshallingTests :: TestTree
   1789 byteMarshallingTests = testGroup "Byte Marshalling Tests"
   1790   [ testCase "ofByte / toByte round-trip: 0" $ do
   1791       let w8 = (0 :: Word8)
   1792       toByte (ofByte w8) @?= Right w8
   1793 
   1794   , testCase "ofByte / toByte round-trip: 1" $ do
   1795       let w8 = (1 :: Word8)
   1796       toByte (ofByte w8) @?= Right w8
   1797 
   1798   , testCase "ofByte / toByte round-trip: 127" $ do
   1799       let w8 = (127 :: Word8)
   1800       toByte (ofByte w8) @?= Right w8
   1801 
   1802   , testCase "ofByte / toByte round-trip: 128" $ do
   1803       let w8 = (128 :: Word8)
   1804       toByte (ofByte w8) @?= Right w8
   1805 
   1806   , testCase "ofByte / toByte round-trip: 255" $ do
   1807       let w8 = (255 :: Word8)
   1808       toByte (ofByte w8) @?= Right w8
   1809 
   1810   , testCase "toByte rejects value > 255" $ do
   1811       -- ofNumber 256 = Fork Leaf (Fork Leaf Leaf) — value 256
   1812       toByte (ofNumber 256) @?= Left "Byte value out of range: 256"
   1813 
   1814   , testCase "toByte accepts Leaf" $ do
   1815       toByte (Leaf) @?= Right 0
   1816 
   1817   , testCase "toByte rejects non-number tree" $ do
   1818       toByte (Stem Leaf) @?= Left "Invalid Tree Calculus number"
   1819       toByte (Stem (Stem Leaf)) @?= Left "Invalid Tree Calculus number"
   1820 
   1821   , testCase "ofBytes / toBytes round-trip: empty ByteString" $ do
   1822       toBytes (ofBytes BS.empty) @?= Right BS.empty
   1823 
   1824   , testCase "ofBytes / toBytes round-trip: [0x00]" $ do
   1825       toBytes (ofBytes (BS.pack [0x00])) @?= Right (BS.pack [0x00])
   1826 
   1827   , testCase "ofBytes / toBytes round-trip: [0xff]" $ do
   1828       toBytes (ofBytes (BS.pack [0xff])) @?= Right (BS.pack [0xff])
   1829 
   1830   , testCase "ofBytes / toBytes round-trip: mixed bytes" $ do
   1831       let bytes = BS.pack [0x00, 0x01, 0x7f, 0x80, 0xff, 0x41, 0x42, 0x43]
   1832       toBytes (ofBytes bytes) @?= Right bytes
   1833 
   1834   , testCase "toBytes rejects non-list tree" $ do
   1835       -- Leaf is a valid list (empty), so this won't work.
   1836       -- Stem Leaf is not a list.
   1837       toBytes (Stem Leaf) @?= Left "Invalid Tree Calculus list"
   1838 
   1839   , testCase "toBytes rejects list containing invalid byte (>255)" $ do
   1840       -- [ofNumber 256, ofNumber 1] — first element is > 255
   1841       let badList = ofList [ofNumber 256, ofNumber 1]
   1842       toBytes badList @?= Left "Byte value out of range: 256"
   1843 
   1844   , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Leaf payload" $ do
   1845       -- Leaf payload is 0x00 (1 byte)
   1846       let payload = BS.pack [0x00]
   1847       treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload
   1848 
   1849   , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Stem payload" $ do
   1850       -- Stem payload: 0x01 || 32-byte hash = 33 bytes
   1851       let payload = BS.pack (0x01 : replicate 32 0x42)
   1852       treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload
   1853 
   1854   , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Fork payload" $ do
   1855       -- Fork payload: 0x02 || 32-byte hash || 32-byte hash = 65 bytes
   1856       let payload = BS.pack (0x02 : replicate 64 0x42)
   1857       treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload
   1858 
   1859   , testCase "hashToTreeBytes / treeBytesToHash round-trip" $ do
   1860       -- Use a known 32-byte hash (SHA256 of "")
   1861       let hashStr :: MerkleHash
   1862           hashStr = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
   1863       case hashToTreeBytes hashStr of
   1864         Left err -> assertFailure $ "hashToTreeBytes failed: " ++ err
   1865         Right tree -> treeBytesToHash tree @?= Right hashStr
   1866 
   1867   , testCase "hashToTreeBytes rejects invalid hex hash" $ do
   1868       hashToTreeBytes "not-a-hash" @?= Left "Invalid hex MerkleHash"
   1869 
   1870   , testCase "hashToTreeBytes rejects non-32-byte hash" $ do
   1871       -- "00" decodes to 1 byte, not 32
   1872       hashToTreeBytes "00" @?= Left "Hash raw bytes must be 32 bytes"
   1873 
   1874   , testCase "treeBytesToHash rejects wrong byte count" $ do
   1875       -- Only 16 bytes, not 32
   1876       let t16 = ofBytes (BS.pack [0x41 | _ <- [1..16]])
   1877       treeBytesToHash t16 @?= Left "Expected exactly 32 byte elements for hash"
   1878   ]
   1879 
   1880 -- --------------------------------------------------------------------------
   1881 -- Content store tests
   1882 -- --------------------------------------------------------------------------
   1883 
   1884 contentStoreTests :: TestTree
   1885 contentStoreTests = testGroup "Content Store Tests"
   1886   [ testCase "Filesystem CAS: put/get object and sharded path" $
   1887       withSystemTempDirectory "tricu-store" $ \dir -> do
   1888         let store = StorePath dir
   1889             domain = Domain "test.object.v1"
   1890             payload = BS.pack [1, 2, 3, 4]
   1891         h <- putObject store domain payload
   1892         shardForHash h @?= take 3 (unpack h)
   1893         objectPath store h @?= dir </> "objects" </> take 3 (unpack h) </> unpack h
   1894         doesFileExist (objectPath store h) >>= (@?= True)
   1895         getObject store h >>= (@?= Just payload)
   1896 
   1897   , testCase "Filesystem CAS: idempotent object writes" $
   1898       withSystemTempDirectory "tricu-store" $ \dir -> do
   1899         let store = StorePath dir
   1900             domain = Domain "test.object.v1"
   1901             payload = BS.pack [9, 8, 7]
   1902         h1 <- putObject store domain payload
   1903         h2 <- putObject store domain payload
   1904         h1 @?= h2
   1905         countStoredObjects store >>= (@?= 1)
   1906 
   1907   , testCase "Filesystem CAS: putTree/getTree round trip" $
   1908       withSystemTempDirectory "tricu-store" $ \dir -> do
   1909         let store = StorePath dir
   1910             term = Fork (Stem Leaf) (Fork Leaf (Stem Leaf))
   1911             leafHash = nodeHash NLeaf
   1912             stemHash = nodeHash (NStem leafHash)
   1913             rightHash = nodeHash (NFork leafHash stemHash)
   1914             expectedRoot = nodeHash (NFork stemHash rightHash)
   1915         root <- putTree store term
   1916         root @?= expectedRoot
   1917         getTree store root >>= (@?= Just term)
   1918 
   1919   , testCase "Filesystem CAS: shared subtrees are deduplicated" $
   1920       withSystemTempDirectory "tricu-store" $ \dir -> do
   1921         let store = StorePath dir
   1922             shared = Stem Leaf
   1923             term = Fork shared shared
   1924         _ <- putTree store term
   1925         countStoredObjects store >>= (@?= 3)
   1926 
   1927   , testCase "Workspace aliases: write/read/list object refs" $
   1928       withSystemTempDirectory "tricu-store" $ \dir -> do
   1929         let store = StorePath dir
   1930             ref = ObjectRef "arboricx.tree-root.v1" "abc123"
   1931         writeAlias store NameAlias "main" ref
   1932         readAlias store NameAlias "main" >>= (@?= Just ref)
   1933         listAliases store NameAlias >>= (@?= [("main", ref)])
   1934 
   1935   , testCase "Module manifests: put/get round trip through CAS" $
   1936       withSystemTempDirectory "tricu-store" $ \dir -> do
   1937         let store = StorePath dir
   1938             term = Fork Leaf (Stem Leaf)
   1939             manifestFor root = ModuleManifest []
   1940               [ ModuleExport
   1941                   "main"
   1942                   (ObjectRef (unDomain treeTermDomain) root)
   1943                   "arboricx.abi.tree.v1"
   1944               ]
   1945         root <- putTreeTerm store term
   1946         h <- putManifest store (manifestFor root)
   1947         getManifest store h >>= (@?= Just (manifestFor root))
   1948 
   1949   , testCase "ObjectResolver: resolves manifests and trees without filesystem coupling" $ do
   1950       let term = Fork Leaf (Stem Leaf)
   1951           leafH = nodeHash NLeaf
   1952           stemH = nodeHash (NStem leafH)
   1953           rootH = nodeHash (NFork leafH stemH)
   1954           termH = hashObject treeTermDomain (encodeTreeTerm term)
   1955           manifest = ModuleManifest []
   1956             [ ModuleExport
   1957                 "value"
   1958                 (ObjectRef (unDomain treeTermDomain) termH)
   1959                 "arboricx.abi.tree.v1"
   1960               ]
   1961           manifestBytes = encodeManifest manifest
   1962           manifestH = hashObject manifestDomain manifestBytes
   1963           objects = Map.fromList
   1964             [ (("arboricx.merkle.node.v1", leafH), serializeNode NLeaf)
   1965             , (("arboricx.merkle.node.v1", stemH), serializeNode (NStem leafH))
   1966             , (("arboricx.merkle.node.v1", rootH), serializeNode (NFork leafH stemH))
   1967             , ((unDomain treeTermDomain, termH), encodeTreeTerm term)
   1968             , ((unDomain manifestDomain, manifestH), manifestBytes)
   1969             ]
   1970           hydrate objs h = case deserializeNode <$> Map.lookup ("arboricx.merkle.node.v1", h) objs of
   1971             Nothing -> return Nothing
   1972             Just NLeaf -> return (Just Leaf)
   1973             Just (NStem child) -> fmap Stem <$> hydrate objs child
   1974             Just (NFork left right) -> do
   1975               l <- hydrate objs left
   1976               r <- hydrate objs right
   1977               return $ Fork <$> l <*> r
   1978           resolver = ObjectResolver
   1979             { resolverAlias = \kind name -> return $ if kind == ModuleAlias && name == "demo"
   1980                 then Just (ObjectRef (unDomain manifestDomain) manifestH)
   1981                 else Nothing
   1982             , resolverObject = \ref -> return $ Map.lookup (objectRefKind ref, objectRefHash ref) objects
   1983             , resolverManifest = \h -> return $ do
   1984                 bytes <- Map.lookup (unDomain manifestDomain, h) objects
   1985                 either (const Nothing) Just (decodeManifest bytes)
   1986             , resolverTree = hydrate objects
   1987             }
   1988       resolverAlias resolver ModuleAlias "demo" >>= (@?= Just (ObjectRef (unDomain manifestDomain) manifestH))
   1989       resolveManifest resolver manifestH >>= (@?= Just manifest)
   1990       resolveTree resolver rootH >>= (@?= Just term)
   1991 
   1992   , testCase "Workspace modules: exported names are local top-level definitions only" $
   1993       withSystemTempDirectory "tricu-workspace-local-exports" $ \dir -> do
   1994         let store = StorePath (dir </> "store")
   1995             depPath = dir </> "dep.tri"
   1996             libPath = dir </> "util.tri"
   1997             mainPath = dir </> "main.tri"
   1998         writeFile (dir </> "tricu.workspace") "module dep = dep.tri\nmodule util = util.tri\n"
   1999         writeFile depPath "helper = t t\n"
   2000         writeFile libPath "!import \"dep\" !Local\n\nvalue = helper\n"
   2001         writeFile mainPath "!import \"util\" Util\n\nmain = Util.value\n"
   2002         env <- evaluateFileWithStore (Just store) mainPath
   2003         result env @?= Stem Leaf
   2004         mAlias <- readAlias store ModuleAlias "util"
   2005         case mAlias of
   2006           Nothing -> assertFailure "expected workspace build to write util module alias"
   2007           Just ref -> do
   2008             mManifest <- getManifest store (objectRefHash ref)
   2009             case mManifest of
   2010               Nothing -> assertFailure "expected workspace module manifest"
   2011               Just manifest -> map moduleExportName (moduleManifestExports manifest) @?= ["value"]
   2012 
   2013   , testCase "Workspace modules: contract annotations travel with exported definitions" $
   2014       withSystemTempDirectory "tricu-workspace-contract-export" $ \dir -> do
   2015         let store = StorePath (dir </> "store")
   2016             libPath = dir </> "util.tri"
   2017             mainPath = dir </> "main.tri"
   2018         cwd <- getCurrentDirectory
   2019         writeFile (dir </> "tricu.workspace") ("module base = \"" ++ cwd </> "lib/base.tri\"\nmodule util = \"" ++ dir </> "util.tri\"\n")
   2020         writeFile libPath "!import \"base\" !Local\n\nalwaysOk = (value rest : ok value rest)\n\nneverOk = (value rest : err \"nope\" rest)\n\nsafeId n@alwaysOk =@alwaysOk n\n\nbadId n@neverOk =@neverOk n\n"
   2021         writeFile mainPath "!import \"util\" Util\n\nmain = Util.safeId 5\n"
   2022         env <- evaluateFileWithStore (Just store) mainPath
   2023         result env @?= ofNumber 5
   2024         writeFile mainPath "!import \"util\" Util\n\nmain = Util.badId 5\n"
   2025         envFail <- evaluateFileWithStore (Just store) mainPath
   2026         decodeResult (result envFail) @?= "[t, \"nope\"]"
   2027 
   2028   , testCase "Module imports: resolve manifest exports from store" $
   2029       withSystemTempDirectory "tricu-module-import" $ \dir -> do
   2030         let store = StorePath (dir </> "store")
   2031             sourcePath = dir </> "consumer.tri"
   2032             term = Fork Leaf (Stem Leaf)
   2033             manifestFor root = ModuleManifest []
   2034               [ ModuleExport
   2035                   "value"
   2036                   (ObjectRef (unDomain treeTermDomain) root)
   2037                   "arboricx.abi.tree.v1"
   2038               ]
   2039         root <- putTreeTerm store term
   2040         manifestHash <- putManifest store (manifestFor root)
   2041         writeAlias store ModuleAlias "demo" (ObjectRef (unDomain manifestDomain) manifestHash)
   2042         writeFile sourcePath "!import \"demo\" Demo\n\nmain = Demo.value\n"
   2043         env <- evaluateFileWithStore (Just store) sourcePath
   2044         result env @?= term
   2045 
   2046   , testCase "Module resolver diagnostics: missing alias names workspace/module guidance" $ do
   2047       let resolver = filesystemResolver (StorePath "/tmp/tricu-test-missing-module-store")
   2048       outcome <- try (resolveModuleImport resolver "definitely-not-a-module" "Demo") :: IO (Either SomeException ResolvedModule)
   2049       case outcome of
   2050         Right _ -> assertFailure "expected missing module alias failure"
   2051         Left err -> show err `containsAll` ["Module alias not found", "definitely-not-a-module", "tricu.workspace", "ModuleAlias"]
   2052 
   2053   , testCase "Module resolver diagnostics: alias kind mismatch names expected kind" $ do
   2054       let resolver = ObjectResolver
   2055             { resolverAlias = \kind name -> return $ if kind == ModuleAlias && name == "demo"
   2056                 then Just (ObjectRef "arboricx.tree-root.v1" "abc123")
   2057                 else Nothing
   2058             , resolverObject = \_ -> return Nothing
   2059             , resolverManifest = \_ -> return Nothing
   2060             , resolverTree = \_ -> return Nothing
   2061             }
   2062       outcome <- try (resolveModuleImport resolver "demo" "Demo") :: IO (Either SomeException ResolvedModule)
   2063       case outcome of
   2064         Right _ -> assertFailure "expected alias kind mismatch failure"
   2065         Left err -> show err `containsAll` ["Module alias", "demo", "unsupported object kind", "arboricx.tree-root.v1", "arboricx.module-manifest.v1"]
   2066 
   2067   , testCase "Module resolver diagnostics: missing tree term names export and hash" $ do
   2068       let root = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
   2069           manifest = ModuleManifest []
   2070             [ ModuleExport "value" (ObjectRef (unDomain treeTermDomain) root) "arboricx.abi.tree.v1" ]
   2071           resolver = ObjectResolver
   2072             { resolverAlias = \kind name -> return $ if kind == ModuleAlias && name == "demo"
   2073                 then Just (ObjectRef (unDomain manifestDomain) "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
   2074                 else Nothing
   2075             , resolverObject = \_ -> return Nothing
   2076             , resolverManifest = \_ -> return (Just manifest)
   2077             , resolverTree = \_ -> return Nothing
   2078             }
   2079       outcome <- try (resolveModuleImport resolver "demo" "Demo") :: IO (Either SomeException ResolvedModule)
   2080       case outcome of
   2081         Right _ -> assertFailure "expected missing tree term failure"
   2082         Left err -> show err `containsAll` ["Module export", "value", "missing tree term", unpack root]
   2083 
   2084   , testCase "Arboricx bundle: unpack transport bundle into CAS tree terms" $
   2085       withSystemTempDirectory "tricu-store" $ \dir -> do
   2086         let store = StorePath dir
   2087             term = Fork (Stem Leaf) Leaf
   2088             bundle = buildBundle [("main", term)]
   2089         exports <- unpackBundleToStore store (encodeBundle bundle)
   2090         case exports of
   2091           [("main", root)] -> getTreeTerm store root >>= (@?= Just term)
   2092           other -> assertFailure $ "unexpected exports: " ++ show other
   2093 
   2094   , testCase "Arboricx bundle: pack CAS tree terms into transport bundle" $
   2095       withSystemTempDirectory "tricu-store" $ \dir -> do
   2096         let store = StorePath dir
   2097             term = Fork Leaf (Stem Leaf)
   2098         root <- putTreeTerm store term
   2099         bundle <- packBundleFromStore store [("main", root)]
   2100         bundleRoots bundle @?= [2]
   2101         let terms = reconstructBundleTermsForTest (bundleNodes bundle)
   2102         case manifestExports (bundleManifest bundle) of
   2103           [exported] -> do
   2104             exportName exported @?= "main"
   2105             terms V.! fromIntegral (exportRoot exported) @?= term
   2106           other -> assertFailure $ "unexpected exports: " ++ show other
   2107   ]
   2108 
   2109 reconstructBundleTermsForTest :: Seq.Seq BundleNode -> V.Vector T
   2110 reconstructBundleTermsForTest nodes = V.fromList (go <$> Foldable.toList nodes)
   2111   where
   2112     built = V.fromList (go <$> Foldable.toList nodes)
   2113     go BNLeaf = Leaf
   2114     go (BNStem child) = Stem (built V.! fromIntegral child)
   2115     go (BNFork left right) = Fork (built V.! fromIntegral left) (built V.! fromIntegral right)
   2116 
   2117 countStoredObjects :: StorePath -> IO Int
   2118 countStoredObjects store@(StorePath root) = do
   2119   ensureStore store
   2120   shards <- listDirectory (root </> "objects")
   2121   fmap sum $ forM shards $ \shard -> do
   2122     files <- listDirectory (root </> "objects" </> shard)
   2123     return (length files)
   2124 
   2125 -- --------------------------------------------------------------------------
   2126 -- Wire module tests
   2127 -- --------------------------------------------------------------------------
   2128 
   2129 -- | Helper: create a temporary file-backed DB, store a term, return the
   2130 
   2131 wireTests :: TestTree
   2132 wireTests = testGroup "Wire Tests"
   2133   [ testCase "Indexed bundle: header and manifest declare indexed format" $ do
   2134       let term = result $ evalTricu Map.empty $ parseTricu "id = a : a\nmain = id t"
   2135           bundle = buildBundle [("main", term)]
   2136           wireData = encodeBundle bundle
   2137       BS.take 8 wireData @?= BS.pack [0x41, 0x52, 0x42, 0x4f, 0x52, 0x49, 0x43, 0x58]
   2138       case decodeBundle wireData of
   2139         Left err -> assertFailure $ "decodeBundle failed: " ++ err
   2140         Right decoded -> do
   2141           let manifest = bundleManifest decoded
   2142               tree = manifestTree manifest
   2143               hashSpec = treeNodeHash tree
   2144           manifestSchema manifest @?= "arboricx.bundle.manifest.v1"
   2145           manifestBundleType manifest @?= "tree-calculus-executable-object"
   2146           manifestClosure manifest @?= ClosureComplete
   2147           treeCalculus tree @?= "tree-calculus.v1"
   2148           treeNodePayload tree @?= "arboricx.indexed.payload.v1"
   2149           nodeHashAlgorithm hashSpec @?= "indexed"
   2150           nodeHashDomain hashSpec @?= "arboricx.indexed.node.v1"
   2151           bundleRoots decoded @?= bundleRoots bundle
   2152           case manifestExports manifest of
   2153             [exported] -> do
   2154               exportName exported @?= "main"
   2155               exportRoot exported @?= head (bundleRoots bundle)
   2156               exportKind exported @?= "term"
   2157               exportAbi exported @?= "arboricx.abi.tree.v1"
   2158             exports -> assertFailure $ "Expected one export, got: " ++ show exports
   2159 
   2160   , testCase "Indexed bundle: deterministic encoding" $ do
   2161       let term = result $ evalTricu Map.empty $ parseTricu "x = t t\nmain = t x"
   2162           bundle1 = buildBundle [("main", term)]
   2163           bundle2 = buildBundle [("main", term)]
   2164       encodeBundle bundle1 @?= encodeBundle bundle2
   2165 
   2166   , testCase "Indexed bundle: renaming export changes bytes" $ do
   2167       let term = result $ evalTricu Map.empty $ parseTricu "f = a : a\nmain = f t"
   2168           mainBundle = buildBundle [("main", term)]
   2169           renamedBundle = buildBundle [("validate", term)]
   2170       encodeBundle mainBundle /= encodeBundle renamedBundle @? "different export names should produce different bytes"
   2171       -- But nodes are identical
   2172       bundleNodes mainBundle @?= bundleNodes renamedBundle
   2173 
   2174   , testCase "Indexed bundle: verify rejects out-of-bounds root" $ do
   2175       let term = Leaf
   2176           bundle = buildBundle [("main", term)]
   2177           badBundle = bundle { bundleRoots = [99] }
   2178       case verifyBundle badBundle of
   2179         Left err -> assertBool ("Expected bounds error, got: " ++ err) ("out of bounds" `isInfixOf` err)
   2180         Right () -> assertFailure "Expected out-of-bounds root to be rejected"
   2181 
   2182   , testCase "Indexed bundle: verify rejects out-of-bounds child index" $ do
   2183       let bundle = Bundle
   2184             { bundleVersion = 1000
   2185             , bundleRoots = [1]
   2186             , bundleNodes = Seq.fromList [BNLeaf, BNStem 99]
   2187             , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)])
   2188                 { manifestRoots = [BundleRoot 1 "default"]
   2189                 , manifestExports = [BundleExport "main" 1 "term" "arboricx.abi.tree.v1"]
   2190                 }
   2191             , bundleManifestBytes = BS.empty
   2192             }
   2193       case verifyBundle bundle of
   2194         Left err -> assertBool ("Expected bounds error, got: " ++ err) ("references child 99" `isInfixOf` err)
   2195         Right () -> assertFailure "Expected out-of-bounds child to be rejected"
   2196 
   2197   , testCase "Indexed bundle: verify rejects acyclic (forward reference)" $ do
   2198       let bundle = Bundle
   2199             { bundleVersion = 1000
   2200             , bundleRoots = [1]
   2201             , bundleNodes = Seq.fromList [BNStem 1, BNLeaf]  -- index 0 refers to 1 (forward)
   2202             , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)])
   2203                 { manifestRoots = [BundleRoot 1 "default"]
   2204                 , manifestExports = [BundleExport "main" 1 "term" "arboricx.abi.tree.v1"]
   2205                 }
   2206             , bundleManifestBytes = BS.empty
   2207             }
   2208       case verifyBundle bundle of
   2209         Left err -> assertBool ("Expected acyclicity error, got: " ++ err) ("references child 1" `isInfixOf` err)
   2210         Right () -> assertFailure "Expected forward reference to be rejected"
   2211 
   2212   , testCase "Indexed bundle: verify rejects duplicate nodes" $ do
   2213       let bundle = Bundle
   2214             { bundleVersion = 1000
   2215             , bundleRoots = [0]
   2216             , bundleNodes = Seq.fromList [BNLeaf, BNLeaf]
   2217             , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)])
   2218                 { manifestRoots = [BundleRoot 0 "default"]
   2219                 , manifestExports = [BundleExport "main" 0 "term" "arboricx.abi.tree.v1"]
   2220                 }
   2221             , bundleManifestBytes = BS.empty
   2222             }
   2223       case verifyBundle bundle of
   2224         Left err -> assertBool ("Expected duplicate error, got: " ++ err) ("duplicate" `isInfixOf` err)
   2225         Right () -> assertFailure "Expected duplicate nodes to be rejected"
   2226 
   2227   , testCase "Indexed bundle: unpack into filesystem CAS" $
   2228       withSystemTempDirectory "tricu-store" $ \dir -> do
   2229         let term = result $ evalTricu Map.empty $ parseTricu "validateEmail = a : a\nmain = validateEmail t"
   2230             bundle = buildBundle [("validateEmail", term)]
   2231             wireData = encodeBundle bundle
   2232             store = StorePath dir
   2233         roots <- unpackBundleToStore store wireData
   2234         case roots of
   2235           [("validateEmail", root)] -> getTree store root >>= (@?= Just term)
   2236           other -> assertFailure $ "unexpected roots: " ++ show other
   2237 
   2238   , testCase "Indexed bundle: round-trip decode and verify" $ do
   2239       let term = result $ evalTricu Map.empty $ parseTricu "x = t\ny = t x\nz = t y\nmain = z"
   2240           bundle = buildBundle [("main", term)]
   2241           wireData = encodeBundle bundle
   2242       case decodeBundle wireData of
   2243         Left err -> assertFailure $ "decodeBundle failed: " ++ err
   2244         Right decoded -> case verifyBundle decoded of
   2245           Left err -> assertFailure $ "verifyBundle failed: " ++ err
   2246           Right () -> do
   2247             bundleRoots decoded @?= bundleRoots bundle
   2248             Seq.length (bundleNodes decoded) @?= Seq.length (bundleNodes bundle)
   2249 
   2250   , testCase "Indexed bundle: unsupported manifest semantics rejected" $ do
   2251       let term = Leaf
   2252           bundle = buildBundle [("main", term)]
   2253           manifest = bundleManifest bundle
   2254           partialBundle = bundle
   2255             { bundleManifest = manifest { manifestClosure = ClosurePartial }
   2256             , bundleManifestBytes = BS.empty
   2257             }
   2258           capabilityBundle = bundle
   2259             { bundleManifest = manifest
   2260                 { manifestRuntime = (manifestRuntime manifest)
   2261                     { runtimeCapabilities = ["host.io"] }
   2262                 }
   2263             , bundleManifestBytes = BS.empty
   2264             }
   2265           wrongHashBundle = bundle
   2266             { bundleManifest = manifest
   2267                 { manifestTree = (manifestTree manifest)
   2268                     { treeNodeHash = (treeNodeHash $ manifestTree manifest)
   2269                         { nodeHashAlgorithm = "blake3" }
   2270                     }
   2271                 }
   2272             , bundleManifestBytes = BS.empty
   2273             }
   2274       case verifyBundle partialBundle of
   2275         Left err -> assertBool ("Expected closure error, got: " ++ err) ("closure = complete" `isInfixOf` err)
   2276         Right () -> assertFailure "Expected partial closure to be rejected"
   2277       case verifyBundle capabilityBundle of
   2278         Left err -> assertBool ("Expected capability error, got: " ++ err) ("capabilities" `isInfixOf` err)
   2279         Right () -> assertFailure "Expected runtime capabilities to be rejected"
   2280       case verifyBundle wrongHashBundle of
   2281         Left err -> assertBool ("Expected hash algorithm error, got: " ++ err) ("node hash algorithm" `isInfixOf` err)
   2282         Right () -> assertFailure "Expected unsupported node hash algorithm to be rejected"
   2283     ]
   2284 
   2285 -- --------------------------------------------------------------------------
   2286 -- Tricu reader tests
   2287 -- Smoke-test the tricu-native Arboricx reader against indexed bundles.
   2288 -- --------------------------------------------------------------------------
   2289 
   2290 tricuReaderTests :: TestTree
   2291 tricuReaderTests = testGroup "Tricu Reader Tests"
   2292   [ testCase "Tricu reader parses indexed bundle (id fixture)" $ do
   2293       bundleBytes <- BS.readFile "./test/fixtures/id.arboricx"
   2294       let bundleT = ofBytes bundleBytes
   2295       let env = Map.insert "testBundle" bundleT allTestLibsEnv
   2296           tagExpr = parseTricu "pairFirst (runArboricx testBundle t)"
   2297           tag = result (evalTricu env tagExpr)
   2298           codeExpr = parseTricu "pairFirst (pairSecond (runArboricx testBundle t))"
   2299           code = result (evalTricu env codeExpr)
   2300       tag @?= trueT
   2301 
   2302   , testCase "Tricu reader parses indexed bundle (append fixture)" $ do
   2303       bundleBytes <- BS.readFile "./test/fixtures/append.arboricx"
   2304       let bundleT = ofBytes bundleBytes
   2305       let env = Map.insert "testBundle" bundleT allTestLibsEnv
   2306           tagExpr = parseTricu "pairFirst (runArboricx testBundle t)"
   2307           tag = result (evalTricu env tagExpr)
   2308       tag @?= trueT
   2309 
   2310   , testCase "Tricu reader parses indexed bundle (bool fixtures)" $ do
   2311       forM_ ["true", "false"] $ \name -> do
   2312         bundleBytes <- BS.readFile ("./test/fixtures/" ++ name ++ ".arboricx")
   2313         let bundleT = ofBytes bundleBytes
   2314         let env = Map.insert "testBundle" bundleT allTestLibsEnv
   2315             tagExpr = parseTricu "pairFirst (runArboricx testBundle t)"
   2316             tag = result (evalTricu env tagExpr)
   2317         tag @?= trueT
   2318   ]
   2319 
   2320 -- --------------------------------------------------------------------------
   2321 -- Byte-list utility tests
   2322 -- Expected values built with canonical Haskell-side T constructors.
   2323 -- --------------------------------------------------------------------------
   2324 
   2325 -- | Helpers for byte-list test expectations.
   2326 
   2327 trueT  :: T
   2328 trueT  = Stem Leaf
   2329 
   2330 falseT :: T
   2331 falseT = Leaf
   2332 
   2333 nothingT :: T
   2334 nothingT = Leaf
   2335 
   2336 justT :: T -> T
   2337 justT = Stem
   2338 
   2339 pairT :: T -> T -> T
   2340 pairT = Fork
   2341 
   2342 byteT :: Integer -> T
   2343 byteT = ofNumber
   2344 
   2345 bytesT :: [Integer] -> T
   2346 bytesT = ofList . fmap byteT
   2347 
   2348 bytesExpr :: [Integer] -> String
   2349 bytesExpr xs = "[" ++ unwords (map (\n -> "(" ++ show n ++ ")") xs) ++ "]"
   2350 
   2351 u16 :: Integer -> [Integer]
   2352 u16 n = [0,n]
   2353 
   2354 u32 :: Integer -> [Integer]
   2355 u32 n = [0,0,0,n]
   2356 
   2357 u64 :: Integer -> [Integer]
   2358 u64 n = [0,0,0,0,0,0,0,n]
   2359 
   2360 arboricxHeaderBytes :: Integer -> [Integer]
   2361 arboricxHeaderBytes sectionCount =
   2362   [65,82,66,79,82,73,67,88]
   2363   ++ u16 1
   2364   ++ u16 0
   2365   ++ u32 sectionCount
   2366   ++ u64 0
   2367   ++ u64 32
   2368 
   2369 sectionEntryBytes :: [Integer] -> Integer -> Integer -> [Integer]
   2370 sectionEntryBytes sectionType offset lengthBytes =
   2371   sectionType
   2372   ++ u16 1
   2373   ++ u16 1
   2374   ++ u16 0
   2375   ++ u16 1
   2376   ++ u64 offset
   2377   ++ u64 lengthBytes
   2378   ++ replicate 32 0
   2379 
   2380 manifestSectionIdBytes :: [Integer]
   2381 manifestSectionIdBytes = [0,0,0,1]
   2382 
   2383 nodesSectionIdBytes :: [Integer]
   2384 nodesSectionIdBytes = [0,0,0,2]
   2385 
   2386 hexTextBytes :: Text -> [Integer]
   2387 hexTextBytes h = go (unpack h)
   2388   where
   2389     go [] = []
   2390     go (a:b:rest) = toInteger (digitToInt a * 16 + digitToInt b) : go rest
   2391     go _ = error "odd-length hex text"
   2392 
   2393 manifestEntryBytes :: Integer -> Integer -> [Integer]
   2394 manifestEntryBytes = sectionEntryBytes manifestSectionIdBytes
   2395 
   2396 nodesEntryBytes :: Integer -> Integer -> [Integer]
   2397 nodesEntryBytes = sectionEntryBytes nodesSectionIdBytes
   2398 
   2399 simpleContainerBytes :: [Integer] -> [Integer] -> [Integer]
   2400 simpleContainerBytes manifestBytes nodesBytes =
   2401   let manifestOffset = 152
   2402       nodesOffset = manifestOffset + fromIntegral (length manifestBytes)
   2403   in arboricxHeaderBytes 2
   2404      ++ manifestEntryBytes manifestOffset (fromIntegral $ length manifestBytes)
   2405      ++ nodesEntryBytes nodesOffset (fromIntegral $ length nodesBytes)
   2406      ++ manifestBytes
   2407      ++ nodesBytes
   2408 
   2409 singleSectionContainerBytes :: [Integer] -> [Integer] -> [Integer]
   2410 singleSectionContainerBytes sectionType sectionBytes =
   2411   arboricxHeaderBytes 1
   2412   ++ sectionEntryBytes sectionType 92 (fromIntegral $ length sectionBytes)
   2413   ++ sectionBytes
   2414 
   2415 arboricxHeaderT :: Integer -> T
   2416 arboricxHeaderT sectionCount =
   2417   pairT (bytesT [0,1])
   2418     (pairT (bytesT [0,0])
   2419       (pairT (bytesT $ u32 sectionCount)
   2420         (pairT (bytesT $ u64 0)
   2421           (bytesT $ u64 32))))
   2422 
   2423 sectionRecordT :: [Integer] -> Integer -> Integer -> T
   2424 sectionRecordT sectionType offset lengthBytes =
   2425   pairT (bytesT sectionType)
   2426     (pairT (bytesT [0,1])
   2427       (pairT (bytesT [0,1])
   2428         (pairT (bytesT [0,0])
   2429           (pairT (bytesT [0,1])
   2430             (pairT (bytesT $ u64 offset)
   2431               (pairT (bytesT $ u64 lengthBytes)
   2432                 (bytesT $ replicate 32 0)))))))
   2433 
   2434 sectionRecordExpr :: [Integer] -> Integer -> Integer -> String
   2435 sectionRecordExpr sectionType offset lengthBytes =
   2436   "(pair " ++ bytesExpr sectionType
   2437   ++ " (pair " ++ bytesExpr [0,1]
   2438   ++ " (pair " ++ bytesExpr [0,1]
   2439   ++ " (pair " ++ bytesExpr [0,0]
   2440   ++ " (pair " ++ bytesExpr [0,1]
   2441   ++ " (pair " ++ bytesExpr (u64 offset)
   2442   ++ " (pair " ++ bytesExpr (u64 lengthBytes)
   2443   ++ " " ++ bytesExpr (replicate 32 0)
   2444   ++ ")))))))"
   2445 
   2446 byteListUtilities :: TestTree
   2447 byteListUtilities = testGroup "Byte List Utility Tests"
   2448   [ testCase "isNil: empty list is nil" $ do
   2449       let input = "bytesNil? []"
   2450       let env = evalTricu allTestLibsEnv (parseTricu input)
   2451       result env @?= trueT
   2452 
   2453   , testCase "isNil: non-empty list is not nil" $ do
   2454       let input = "bytesNil? [(1)]"
   2455       let env = evalTricu allTestLibsEnv (parseTricu input)
   2456       result env @?= falseT
   2457 
   2458   , testCase "head: empty list is nothing" $ do
   2459       let input = "bytesHead []"
   2460       let env = evalTricu allTestLibsEnv (parseTricu input)
   2461       result env @?= nothingT
   2462 
   2463   , testCase "head: non-empty list returns first element" $ do
   2464       let input = "bytesHead [(1) (2)]"
   2465       let env = evalTricu allTestLibsEnv (parseTricu input)
   2466       result env @?= justT (byteT 1)
   2467 
   2468   , testCase "tail: empty list is nothing" $ do
   2469       let input = "bytesTail []"
   2470       let env = evalTricu allTestLibsEnv (parseTricu input)
   2471       result env @?= nothingT
   2472 
   2473   , testCase "tail: non-empty list returns rest" $ do
   2474       let input = "bytesTail [(1) (2)]"
   2475       let env = evalTricu allTestLibsEnv (parseTricu input)
   2476       result env @?= justT (bytesT [2])
   2477 
   2478   , testCase "length: empty list is zero" $ do
   2479       let input = "bytesLength []"
   2480       let env = evalTricu allTestLibsEnv (parseTricu input)
   2481       result env @?= ofNumber 0
   2482 
   2483   , testCase "length: single element list is one" $ do
   2484       let input = "bytesLength [(1)]"
   2485       let env = evalTricu allTestLibsEnv (parseTricu input)
   2486       result env @?= ofNumber 1
   2487 
   2488   , testCase "length: three element list is three" $ do
   2489       let input = "bytesLength [(1) (2) (3)]"
   2490       let env = evalTricu allTestLibsEnv (parseTricu input)
   2491       result env @?= ofNumber 3
   2492 
   2493   , testCase "append: empty ++ [1,2] = [1,2]" $ do
   2494       let input = "bytesAppend [] [(1) (2)]"
   2495       let env = evalTricu allTestLibsEnv (parseTricu input)
   2496       result env @?= bytesT [1,2]
   2497 
   2498   , testCase "append: [1,2] ++ [3] = [1,2,3]" $ do
   2499       let input = "bytesAppend [(1) (2)] [(3)]"
   2500       let env = evalTricu allTestLibsEnv (parseTricu input)
   2501       result env @?= bytesT [1,2,3]
   2502 
   2503   , testCase "append: [1,2] ++ empty = [1,2]" $ do
   2504       let input = "bytesAppend [(1) (2)] []"
   2505       let env = evalTricu allTestLibsEnv (parseTricu input)
   2506       result env @?= bytesT [1,2]
   2507 
   2508   , testCase "take: take 0 any list = empty" $ do
   2509       let input = "bytesTake 0 [(1) (2) (3)]"
   2510       let env = evalTricu allTestLibsEnv (parseTricu input)
   2511       result env @?= bytesT []
   2512 
   2513   , testCase "take: take 2 [1,2,3] = [1,2]" $ do
   2514       let input = "bytesTake 2 [(1) (2) (3)]"
   2515       let env = evalTricu allTestLibsEnv (parseTricu input)
   2516       result env @?= bytesT [1,2]
   2517 
   2518   , testCase "take: take 5 [1,2] = [1,2] (overlong)" $ do
   2519       let input = "bytesTake 5 [(1) (2)]"
   2520       let env = evalTricu allTestLibsEnv (parseTricu input)
   2521       result env @?= bytesT [1,2]
   2522 
   2523   , testCase "drop: drop 0 any list = list" $ do
   2524       let input = "bytesDrop 0 [(1) (2) (3)]"
   2525       let env = evalTricu allTestLibsEnv (parseTricu input)
   2526       result env @?= bytesT [1,2,3]
   2527 
   2528   , testCase "drop: drop 2 [1,2,3] = [3]" $ do
   2529       let input = "bytesDrop 2 [(1) (2) (3)]"
   2530       let env = evalTricu allTestLibsEnv (parseTricu input)
   2531       result env @?= bytesT [3]
   2532 
   2533   , testCase "drop: drop 5 [1,2] = empty (overlong)" $ do
   2534       let input = "bytesDrop 5 [(1) (2)]"
   2535       let env = evalTricu allTestLibsEnv (parseTricu input)
   2536       result env @?= bytesT []
   2537 
   2538   , testCase "splitAt: splitAt 0 [1,2] = pair [] [1,2]" $ do
   2539       let input = "bytesSplitAt 0 [(1) (2)]"
   2540       let env = evalTricu allTestLibsEnv (parseTricu input)
   2541       result env @?= pairT (bytesT []) (bytesT [1,2])
   2542 
   2543   , testCase "splitAt: splitAt 2 [1,2,3] = pair [1,2] [3]" $ do
   2544       let input = "bytesSplitAt 2 [(1) (2) (3)]"
   2545       let env = evalTricu allTestLibsEnv (parseTricu input)
   2546       result env @?= pairT (bytesT [1,2]) (bytesT [3])
   2547 
   2548   , testCase "splitAt: splitAt 5 [1,2] = pair [1,2] []" $ do
   2549       let input = "bytesSplitAt 5 [(1) (2)]"
   2550       let env = evalTricu allTestLibsEnv (parseTricu input)
   2551       result env @?= pairT (bytesT [1,2]) (bytesT [])
   2552 
   2553   , testCase "byteEq: equal bytes are equal" $ do
   2554       let input = "equal? 1 1"
   2555       let env = evalTricu allTestLibsEnv (parseTricu input)
   2556       result env @?= trueT
   2557 
   2558   , testCase "byteEq: unequal bytes are not equal" $ do
   2559       let input = "equal? 1 2"
   2560       let env = evalTricu allTestLibsEnv (parseTricu input)
   2561       result env @?= falseT
   2562 
   2563   , testCase "bytesEq: empty == empty" $ do
   2564       let input = "bytesEq? [] []"
   2565       let env = evalTricu allTestLibsEnv (parseTricu input)
   2566       result env @?= trueT
   2567 
   2568   , testCase "bytesEq: empty != [1]" $ do
   2569       let input = "bytesEq? [] [(1)]"
   2570       let env = evalTricu allTestLibsEnv (parseTricu input)
   2571       result env @?= falseT
   2572 
   2573   , testCase "bytesEq: [1] != empty" $ do
   2574       let input = "bytesEq? [(1)] []"
   2575       let env = evalTricu allTestLibsEnv (parseTricu input)
   2576       result env @?= falseT
   2577 
   2578   , testCase "bytesEq: equal lists are equal" $ do
   2579       let input = "bytesEq? [(1) (2) (3)] [(1) (2) (3)]"
   2580       let env = evalTricu allTestLibsEnv (parseTricu input)
   2581       result env @?= trueT
   2582 
   2583   , testCase "bytesEq: different last element" $ do
   2584       let input = "bytesEq? [(1) (2) (3)] [(1) (2) (4)]"
   2585       let env = evalTricu allTestLibsEnv (parseTricu input)
   2586       result env @?= falseT
   2587 
   2588   , testCase "bytesEq: different lengths" $ do
   2589       let input = "bytesEq? [(1) (2)] [(1) (2) (3)]"
   2590       let env = evalTricu allTestLibsEnv (parseTricu input)
   2591       result env @?= falseT
   2592   ]
   2593 
   2594 -- --------------------------------------------------------------------------
   2595 -- Binary parser combinator tests
   2596 -- --------------------------------------------------------------------------
   2597 
   2598 parserOk :: T -> T -> T
   2599 parserOk val rest = Fork trueT (Fork val rest)
   2600 
   2601 parserErr :: T -> T -> T
   2602 parserErr code rest = Fork falseT (Fork code rest)
   2603 
   2604 binaryParserTests :: TestTree
   2605 binaryParserTests = testGroup "Binary Parser Tests"
   2606   [ testCase "pureParser succeeds" $ do
   2607       let input = "pureParser 42 [(1) (2)]"
   2608           env = evalTricu allTestLibsEnv (parseTricu input)
   2609       result env @?= parserOk (ofNumber 42) (bytesT [1, 2])
   2610 
   2611   , testCase "failParser fails" $ do
   2612       let input = "failParser 99 [(1) (2)]"
   2613           env = evalTricu allTestLibsEnv (parseTricu input)
   2614       result env @?= parserErr (ofNumber 99) (bytesT [1, 2])
   2615 
   2616   , testCase "mapParser transforms value" $ do
   2617       let input = "mapParser succ readU8 [(1) (2)]"
   2618           env = evalTricu allTestLibsEnv (parseTricu input)
   2619       result env @?= parserOk (ofNumber 2) (bytesT [2])
   2620 
   2621   , testCase "bindParser chains parsers" $ do
   2622       let input = "bindParser readU8 (x : readU8) [(1) (2)]"
   2623           env = evalTricu allTestLibsEnv (parseTricu input)
   2624       result env @?= parserOk (ofNumber 2) (bytesT [])
   2625 
   2626   , testCase "thenParser discards first result" $ do
   2627       let input = "thenParser readU8 readU8 [(1) (2)]"
   2628           env = evalTricu allTestLibsEnv (parseTricu input)
   2629       result env @?= parserOk (ofNumber 2) (bytesT [])
   2630 
   2631   , testCase "orParser tries second on first failure" $ do
   2632       let input = "orParser (failParser 1) readU8 [(5)]"
   2633           env = evalTricu allTestLibsEnv (parseTricu input)
   2634       result env @?= parserOk (ofNumber 5) (bytesT [])
   2635 
   2636   , testCase "orParser returns first on success" $ do
   2637       let input = "orParser readU8 (failParser 1) [(5)]"
   2638           env = evalTricu allTestLibsEnv (parseTricu input)
   2639       result env @?= parserOk (ofNumber 5) (bytesT [])
   2640 
   2641   , testCase "readWhile consumes matching bytes" $ do
   2642       let input = "readWhile (x : lt? x 3) [(1) (2) (3) (4)]"
   2643           env = evalTricu allTestLibsEnv (parseTricu input)
   2644       result env @?= parserOk (bytesT [1, 2]) (bytesT [3, 4])
   2645 
   2646   , testCase "readWhile leaves non-matching byte" $ do
   2647       let input = "bindParser (readWhile (x : lt? x 3)) (x : readU8) [(1) (2) (3)]"
   2648           env = evalTricu allTestLibsEnv (parseTricu input)
   2649       result env @?= parserOk (ofNumber 3) (bytesT [])
   2650 
   2651   , testCase "readUntil stops at matching byte" $ do
   2652       let input = "readUntil (x : equal? x 3) [(1) (2) (3) (4)]"
   2653           env = evalTricu allTestLibsEnv (parseTricu input)
   2654       result env @?= parserOk (bytesT [1, 2]) (bytesT [3, 4])
   2655 
   2656   , testCase "readRemaining returns all bytes" $ do
   2657       let input = "readRemaining [(1) (2) (3)]"
   2658           env = evalTricu allTestLibsEnv (parseTricu input)
   2659       result env @?= parserOk (bytesT [1, 2, 3]) (bytesT [])
   2660 
   2661   , testCase "peekU8 does not consume" $ do
   2662       let input = "bindParser peekU8 (x : readU8) [(7) (8)]"
   2663           env = evalTricu allTestLibsEnv (parseTricu input)
   2664       result env @?= parserOk (ofNumber 7) (bytesT [8])
   2665 
   2666   , testCase "peekU8 second read gets same byte" $ do
   2667       let input = "bindParser peekU8 (x : bindParser peekU8 (y : pureParser (pair x y))) [(7)]"
   2668           env = evalTricu allTestLibsEnv (parseTricu input)
   2669       result env @?= parserOk (pairT (ofNumber 7) (ofNumber 7)) (bytesT [7])
   2670 
   2671   , testCase "eof? succeeds at empty input" $ do
   2672       let input = "eof? []"
   2673           env = evalTricu allTestLibsEnv (parseTricu input)
   2674       result env @?= parserOk Leaf (bytesT [])
   2675 
   2676   , testCase "eof? fails on non-empty input" $ do
   2677       let input = "eof? [(1)]"
   2678           env = evalTricu allTestLibsEnv (parseTricu input)
   2679       result env @?= parserErr (ofNumber 1) (bytesT [1])
   2680 
   2681   , testCase "expectAscii matches string" $ do
   2682       let input = "expectAscii \"hi\" [(104) (105) (106)]"
   2683           env = evalTricu allTestLibsEnv (parseTricu input)
   2684       result env @?= parserOk Leaf (bytesT [106])
   2685 
   2686   , testCase "expectAscii fails on mismatch" $ do
   2687       let input = "expectAscii \"hi\" [(104) (99)]"
   2688           env = evalTricu allTestLibsEnv (parseTricu input)
   2689       result env @?= parserErr (ofNumber 2) (bytesT [104, 99])
   2690   ]
   2691 
   2692 -- --------------------------------------------------------------------------
   2693 -- IO driver tests
   2694 -- --------------------------------------------------------------------------
   2695 
   2696 ioDriverTests :: TestTree
   2697 ioDriverTests = testGroup "IO driver tests"
   2698   [ -- Existing behaviour tests
   2699     testCase "readFile through onReadFile returns file contents" $
   2700       withSystemTempDirectory "tricu-io-read" $ \dir -> do
   2701         let sourcePath = dir ++ "/input.txt"
   2702         writeFile sourcePath "abc123"
   2703         final <- runIOSource $
   2704           unlines
   2705             [ "main = io (onReadFile \"" ++ sourcePath ++ "\""
   2706             , "  (err rest : pure \"read failed\")"
   2707             , "  (contents rest : pure contents))"
   2708             ]
   2709         final @?= ofString "abc123"
   2710 
   2711   , testCase "readFile error path returns explicit error branch" $
   2712       withSystemTempDirectory "tricu-io-read-missing" $ \dir -> do
   2713         let sourcePath = dir ++ "/missing.txt"
   2714         final <- runIOSource $
   2715           unlines
   2716             [ "main = io (onReadFile \"" ++ sourcePath ++ "\""
   2717             , "  (err rest : pure \"read failed\")"
   2718             , "  (contents rest : pure contents))"
   2719             ]
   2720         final @?= ofString "read failed"
   2721 
   2722   , testCase "chains multiple readFile actions through Result-aware helper" $
   2723       withSystemTempDirectory "tricu-io-chain" $ \dir -> do
   2724         let firstPath = dir ++ "/first.txt"
   2725             secondPath = dir ++ "/second.txt"
   2726         writeFile firstPath "abc"
   2727         writeFile secondPath "def"
   2728         final <- runIOSource $
   2729           unlines
   2730             [ "main = io (onReadFile \"" ++ firstPath ++ "\""
   2731             , "  (err rest : pure \"first read failed\")"
   2732             , "  (first rest : onReadFile \"" ++ secondPath ++ "\""
   2733             , "    (err rest : pure \"second read failed\")"
   2734             , "    (second rest : pure (append first second))))"
   2735             ]
   2736         final @?= ofString "abcdef"
   2737 
   2738     -- Monad law tests
   2739   , testCase "left identity: bind (pure x) f == f x" $ do
   2740       left <- runIOSource $
   2741         unlines
   2742           [ "f = x : pure (append x \"!\")"
   2743           , "main = io (bind (pure \"abc\") f)"
   2744           ]
   2745       right <- runIOSource $
   2746         unlines
   2747           [ "f = x : pure (append x \"!\")"
   2748           , "main = io (f \"abc\")"
   2749           ]
   2750       left @?= right
   2751       left @?= ofString "abc!"
   2752 
   2753   , testCase "right identity: bind m pure == m" $
   2754       withSystemTempDirectory "tricu-io-right-id" $ \dir -> do
   2755         let path = dir ++ "/input.txt"
   2756         writeFile path "abc"
   2757         left <- runIOSource $
   2758           unlines
   2759             [ "main = io (bind (readFile \"" ++ path ++ "\")"
   2760             , "  (result : pure result))"
   2761             ]
   2762         right <- runIOSource $
   2763           unlines
   2764             [ "main = io (readFile \"" ++ path ++ "\")"
   2765             ]
   2766         left @?= right
   2767         left @?= ioOkResult (ofString "abc")
   2768 
   2769   , testCase "associativity: bind (bind m f) g == bind m (x : bind (f x) g)" $
   2770       withSystemTempDirectory "tricu-io-assoc" $ \dir -> do
   2771         let path = dir ++ "/input.txt"
   2772         writeFile path "abc"
   2773         left <- runIOSource $
   2774           unlines
   2775             [ "m = readFile \"" ++ path ++ "\""
   2776             , "f = result : matchResult (err rest : pure \"read failed\") (contents rest : pure (append contents \"-f\")) result"
   2777             , "g = value : pure (append value \"-g\")"
   2778             , "main = io (bind (bind m f) g)"
   2779             ]
   2780         right <- runIOSource $
   2781           unlines
   2782             [ "m = readFile \"" ++ path ++ "\""
   2783             , "f = result : matchResult (err rest : pure \"read failed\") (contents rest : pure (append contents \"-f\")) result"
   2784             , "g = value : pure (append value \"-g\")"
   2785             , "main = io (bind m (x : bind (f x) g))"
   2786             ]
   2787         left @?= right
   2788         left @?= ofString "abc-f-g"
   2789 
   2790   , testCase "associativity preserves error flow" $
   2791       withSystemTempDirectory "tricu-io-assoc-err" $ \dir -> do
   2792         let missingPath = dir ++ "/missing.txt"
   2793         left <- runIOSource $
   2794           unlines
   2795             [ "m = readFile \"" ++ missingPath ++ "\""
   2796             , "f = result : matchResult (err rest : pure \"handled\") (contents rest : pure (append contents \"-ok\")) result"
   2797             , "g = value : pure (append value \"-g\")"
   2798             , "main = io (bind (bind m f) g)"
   2799             ]
   2800         right <- runIOSource $
   2801           unlines
   2802             [ "m = readFile \"" ++ missingPath ++ "\""
   2803             , "f = result : matchResult (err rest : pure \"handled\") (contents rest : pure (append contents \"-ok\")) result"
   2804             , "g = value : pure (append value \"-g\")"
   2805             , "main = io (bind m (x : bind (f x) g))"
   2806             ]
   2807         left @?= right
   2808         left @?= ofString "handled-g"
   2809 
   2810   , testCase "bind defers continuation until left action completes" $
   2811       withSystemTempDirectory "tricu-io-lazy-k" $ \dir -> do
   2812         let path = dir ++ "/created.txt"
   2813         final <- runIOSource $
   2814           unlines
   2815             [ "main = io (bind (writeFile \"" ++ path ++ "\" \"created\")"
   2816             , "  (_ : readFile \"" ++ path ++ "\"))"
   2817             ]
   2818         final @?= ioOkResult (ofString "created")
   2819 
   2820     -- Primitive effect shape tests
   2821   , testCase "readFile without continuation returns Result" $
   2822       withSystemTempDirectory "tricu-io-raw-read" $ \dir -> do
   2823         let path = dir ++ "/input.txt"
   2824         writeFile path "abc"
   2825         final <- runIOSource $
   2826           unlines
   2827             [ "main = io (readFile \"" ++ path ++ "\")"
   2828             ]
   2829         final @?= ioOkResult (ofString "abc")
   2830 
   2831   , testCase "writeFile then readFile executes exactly once" $
   2832       withSystemTempDirectory "tricu-io-once" $ \dir -> do
   2833         let path = dir ++ "/test.txt"
   2834         final <- runIOSource $
   2835           unlines
   2836             [ "main = io (bind (writeFile \"" ++ path ++ "\" \"abc\")"
   2837             , "  (_ : readFile \"" ++ path ++ "\"))"
   2838             ]
   2839         final @?= ioOkResult (ofString "abc")
   2840 
   2841   , testCase "sequencing order is left-to-right" $
   2842       withSystemTempDirectory "tricu-io-order" $ \dir -> do
   2843         let path = dir ++ "/test.txt"
   2844         final <- runIOSource $
   2845           unlines
   2846             [ "main = io (bind (writeFile \"" ++ path ++ "\" \"a\")"
   2847             , "  (_ : bind (writeFile \"" ++ path ++ "\" \"ab\")"
   2848             , "    (_ : readFile \"" ++ path ++ "\")))"
   2849             ]
   2850         final @?= ioOkResult (ofString "ab")
   2851 
   2852   , testCase "thenIO sequences two actions and discards first result" $
   2853       withSystemTempDirectory "tricu-io-then" $ \dir -> do
   2854         let path = dir ++ "/test.txt"
   2855         final <- runIOSource $
   2856           unlines
   2857             [ "main = io (thenIO (writeFile \"" ++ path ++ "\" \"x\")"
   2858             , "  (readFile \"" ++ path ++ "\"))"
   2859             ]
   2860         final @?= ioOkResult (ofString "x")
   2861 
   2862   , testCase "bind does not short-circuit on readFile error" $
   2863       withSystemTempDirectory "tricu-io-no-short" $ \dir -> do
   2864         let path = dir ++ "/missing.txt"
   2865         final <- runIOSource $
   2866           unlines
   2867             [ "main = io (bind (readFile \"" ++ path ++ "\")"
   2868             , "  (result : pure \"continued\"))"
   2869             ]
   2870         final @?= ofString "continued"
   2871 
   2872   , testCase "mapIO transforms pure value" $ do
   2873       final <- runIOSource $
   2874         unlines
   2875           [ "main = io (mapIO (pure \"abc\") (x : append x \"!\"))"
   2876           ]
   2877       final @?= ofString "abc!"
   2878 
   2879     -- Malformed action tests
   2880   , testCase "unknown IO action tag returns err result" $ do
   2881       final <- runIOSource "main = io (pair 99 t)"
   2882       final @?= ioErrResult "invalid action"
   2883 
   2884   , testCase "malformed Bind returns err result" $ do
   2885       final <- runIOSource "main = io (pair 1 t)"
   2886       final @?= ioErrResult "invalid action"
   2887 
   2888   , testCase "malformed ReadFile payload returns err result" $ do
   2889       final <- runIOSource "main = io (readFile (t t))"
   2890       final @?= ioErrResult "invalid string"
   2891 
   2892     -- Permission tests
   2893   , testCase "allowed read path succeeds" $
   2894       withSystemTempDirectory "tricu-io-allowed" $ \dir -> do
   2895         let path = dir ++ "/allowed.txt"
   2896         writeFile path "allowed"
   2897         let perms = defaultPerms { allowRead = [path] }
   2898         result <- runIOSourceWithPerms perms $
   2899           unlines
   2900             [ "main = io (readFile \"" ++ path ++ "\")"
   2901             ]
   2902         result @?= ioOkResult (ofString "allowed")
   2903 
   2904   , testCase "readFile denied path returns err result" $
   2905       withSystemTempDirectory "tricu-io-read-denied" $ \dir -> do
   2906         let allowedPath = dir ++ "/allowed.txt"
   2907             deniedPath = dir ++ "/denied.txt"
   2908         writeFile allowedPath "allowed"
   2909         writeFile deniedPath "denied"
   2910         let perms = defaultPerms { allowRead = [allowedPath] }
   2911         result <- runIOSourceWithPerms perms $
   2912           unlines
   2913             [ "main = io (readFile \"" ++ deniedPath ++ "\")"
   2914             ]
   2915         result @?= ioErrResult "permission denied"
   2916 
   2917   , testCase "writeFile denied path returns err result" $
   2918       withSystemTempDirectory "tricu-io-write-denied" $ \dir -> do
   2919         let allowedPath = dir ++ "/allowed.txt"
   2920             deniedPath = dir ++ "/denied.txt"
   2921         let perms = defaultPerms { allowWrite = [allowedPath] }
   2922         result <- runIOSourceWithPerms perms $
   2923           unlines
   2924             [ "main = io (writeFile \"" ++ deniedPath ++ "\" \"x\")"
   2925             ]
   2926         result @?= ioErrResult "permission denied"
   2927 
   2928   , testCase "path prefix does not allow prefix bypass" $
   2929       withSystemTempDirectory "tricu-io-prefix" $ \dir -> do
   2930         let allowedDir = dir ++ "/foo"
   2931             bypassPath = dir ++ "/foobar/secret.txt"
   2932         createDirectory allowedDir
   2933         createDirectory (dir ++ "/foobar")
   2934         writeFile bypassPath "secret"
   2935         let perms = defaultPerms { allowRead = [allowedDir] }
   2936         result <- runIOSourceWithPerms perms $
   2937           unlines
   2938             [ "main = io (readFile \"" ++ bypassPath ++ "\")"
   2939             ]
   2940         result @?= ioErrResult "permission denied"
   2941 
   2942     -- Pure test
   2943   , testCase "pure performs no effects" $ do
   2944       final <- runIOSource "main = io (pure \"abc\")"
   2945       final @?= ofString "abc"
   2946 
   2947     -- Reader tests
   2948   , testCase "ask returns initial environment" $ do
   2949       final <- runIOSourceWithEnv unsafePerms (ofString "dev") $
   2950         unlines
   2951           [ "main = io (bind ask (env : pure env))"
   2952           ]
   2953       final @?= ofString "dev"
   2954 
   2955   , testCase "local transforms environment" $ do
   2956       final <- runIOSourceWithEnv unsafePerms (ofString "root") $
   2957         unlines
   2958           [ "main = io (local (env : append env \"-local\") (bind ask (env : pure env)))"
   2959           ]
   2960       final @?= ofString "root-local"
   2961 
   2962   , testCase "local restores environment afterward" $ do
   2963       final <- runIOSourceWithEnv unsafePerms (ofString "root") $
   2964         unlines
   2965           [ "main = io (bind ask (before :"
   2966           , "  bind (local (env : append env \"-local\") (bind ask (env : pure env))) (inside :"
   2967           , "  bind ask (after :"
   2968           , "  pure (pair before (pair inside after))))))"
   2969           ]
   2970       final @?= Fork (ofString "root") (Fork (ofString "root-local") (ofString "root"))
   2971 
   2972   , testCase "nested local composes correctly" $ do
   2973       final <- runIOSourceWithEnv unsafePerms (ofString "root") $
   2974         unlines
   2975           [ "f = x : append x \"-f\""
   2976           , "g = x : append x \"-g\""
   2977           , "main = io (bind"
   2978           , "  (local f (local g (bind ask (env : pure env))))"
   2979           , "  (inner :"
   2980           , "  bind ask (after :"
   2981           , "  pure (pair inner after))))"
   2982           ]
   2983       final @?= Fork (ofString "root-f-g") (ofString "root")
   2984 
   2985   , testCase "local result passes through bind correctly" $ do
   2986       final <- runIOSourceWithEnv unsafePerms (ofString "root") $
   2987         unlines
   2988           [ "main = io (bind"
   2989           , "  (local (env : append env \"-local\") (pure \"value\"))"
   2990           , "  (x : pure x))"
   2991           ]
   2992       final @?= ofString "value"
   2993 
   2994   , testCase "IO inside local uses transformed environment and restores after" $ do
   2995       final <- runIOSourceWithEnv unsafePerms (ofString "root") $
   2996         unlines
   2997           [ "main = io (bind"
   2998           , "  (local (env : append env \"-local\")"
   2999           , "    (bind ask (env : pure env)))"
   3000           , "  (result :"
   3001           , "  bind ask (after :"
   3002           , "  pure (pair result after))))"
   3003           ]
   3004       final @?= Fork (ofString "root-local") (ofString "root")
   3005 
   3006   , testCase "local does not affect outer bind continuation" $ do
   3007       final <- runIOSourceWithEnv unsafePerms (ofString "root") $
   3008         unlines
   3009           [ "main = io (bind"
   3010           , "  (local (env : append env \"-local\") (pure \"x\"))"
   3011           , "  (_ : bind ask (env : pure env)))"
   3012           ]
   3013       final @?= ofString "root"
   3014 
   3015   , testCase "local environment persists across inner binds" $ do
   3016       final <- runIOSourceWithEnv unsafePerms (ofString "root") $
   3017         unlines
   3018           [ "main = io (local (env : append env \"-local\")"
   3019           , "  (bind (pure t) (_ :"
   3020           , "  bind ask (env : pure env))))"
   3021           ]
   3022       final @?= ofString "root-local"
   3023 
   3024   , testCase "local restores environment when scoped action returns error value" $ do
   3025       final <- runIOSourceWithEnv defaultPerms (ofString "root") $
   3026         unlines
   3027           [ "main = io (bind"
   3028           , "  (local (env : append env \"-local\") (readFile \"definitely-missing.txt\"))"
   3029           , "  (_ : bind ask (env : pure env)))"
   3030           ]
   3031       final @?= ofString "root"
   3032 
   3033     -- State tests
   3034   , testCase "get returns initial state" $ do
   3035       (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 42) $
   3036         unlines
   3037           [ "main = io (bind get (s : pure s))"
   3038           ]
   3039       final @?= ofNumber 42
   3040       st @?= ofNumber 42
   3041 
   3042   , testCase "put updates state" $ do
   3043       (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 0) $
   3044         unlines
   3045           [ "main = io (bind (put 100) (_ : bind get (s : pure s)))"
   3046           ]
   3047       final @?= ofNumber 100
   3048       st @?= ofNumber 100
   3049 
   3050   , testCase "state persists through bind" $ do
   3051       (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 5) $
   3052         unlines
   3053           [ "main = io (bind get (s1 :"
   3054           , "  bind (put (succ s1)) (_ :"
   3055           , "  bind get (s2 :"
   3056           , "  pure (pair s1 s2)))))"
   3057           ]
   3058       final @?= Fork (ofNumber 5) (ofNumber 6)
   3059       st @?= ofNumber 6
   3060 
   3061   , testCase "local does not restore state" $ do
   3062       (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 0) $
   3063         unlines
   3064           [ "main = io (bind (put 10) (_ :"
   3065           , "  bind (local (env : env) (put 20)) (_ :"
   3066           , "  bind get (s :"
   3067           , "  pure s))))"
   3068           ]
   3069       final @?= ofNumber 20
   3070       st @?= ofNumber 20
   3071 
   3072   , testCase "state and reader are independent" $ do
   3073       (final, st) <- runIOSourceWith unsafePerms (ofString "hello") (ofNumber 42) $
   3074         unlines
   3075           [ "main = io (bind ask (env :"
   3076           , "  bind get (s :"
   3077           , "  pure (pair env s))))"
   3078           ]
   3079       final @?= Fork (ofString "hello") (ofNumber 42)
   3080       st @?= ofNumber 42
   3081 
   3082     -- Async tests
   3083   , testCase "fork returns handle and await returns child value" $ do
   3084       (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $
   3085         unlines
   3086           [ "main = io (bind (fork (pure \"child\")) (h :"
   3087           , "  await h))"
   3088           ]
   3089       final @?= ofString "child"
   3090       st @?= Leaf
   3091 
   3092   , testCase "main completion abandons unawaited child" $ do
   3093       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3094         unlines
   3095           [ "main = io (bind (fork (pure \"child\")) (_ :"
   3096           , "  pure \"main\"))"
   3097           ]
   3098       final @?= ofString "main"
   3099 
   3100   , testCase "fork captures reader environment at fork point" $ do
   3101       (final, _) <- runIOSourceWith unsafePerms (ofString "root") Leaf $
   3102         unlines
   3103           [ "main = io (local (env : append env \"-local\")"
   3104           , "  (bind (fork (bind ask (env : pure env))) (h :"
   3105           , "  await h)))"
   3106           ]
   3107       final @?= ofString "root-local"
   3108 
   3109   , testCase "fork inside local captures child env and parent restores env" $ do
   3110       (final, _) <- runIOSourceWith unsafePerms (ofString "root") Leaf $
   3111         unlines
   3112           [ "main = io (bind"
   3113           , "  (local (env : append env \"-local\")"
   3114           , "    (fork (bind ask (env : pure env))))"
   3115           , "  (h : bind ask (after :"
   3116           , "  bind (await h) (child :"
   3117           , "  pure (pair after child)))))"
   3118           ]
   3119       final @?= Fork (ofString "root") (ofString "root-local")
   3120 
   3121   , testCase "fork copies state and child state does not merge" $ do
   3122       (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 0) $
   3123         unlines
   3124           [ "main = io (bind (put 1) (_ :"
   3125           , "  bind (fork (bind (put 99) (_ : bind get (s : pure s)))) (h :"
   3126           , "  bind (put 2) (_ :"
   3127           , "  bind (await h) (childState :"
   3128           , "  bind get (parentState :"
   3129           , "  pure (pair childState parentState)))))))"
   3130           ]
   3131       final @?= Fork (ofNumber 99) (ofNumber 2)
   3132       st @?= ofNumber 2
   3133 
   3134   , testCase "multiple awaiters receive same completed value" $ do
   3135       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3136         unlines
   3137           [ "main = io (bind (fork (pure \"done\")) (h :"
   3138           , "  bind (await h) (a :"
   3139           , "  bind (await h) (b :"
   3140           , "  pure (pair a b)))))"
   3141           ]
   3142       final @?= Fork (ofString "done") (ofString "done")
   3143 
   3144   , testCase "self await returns async error" $ do
   3145       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3146         unlines
   3147           [ "main = io (await (pair \"task\" 0))"
   3148           ]
   3149       final @?= ioErrResult "self await"
   3150 
   3151   , testCase "await invalid handle returns async error" $ do
   3152       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3153         unlines
   3154           [ "main = io (await 123)"
   3155           ]
   3156       final @?= ioErrResult "invalid task handle"
   3157 
   3158   , testCase "yield returns unit and resumes continuation" $ do
   3159       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3160         unlines
   3161           [ "main = io (bind yield (_ : pure \"after\"))"
   3162           ]
   3163       final @?= ofString "after"
   3164 
   3165   , testCase "sleep resumes continuation" $ do
   3166       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3167         unlines
   3168           [ "main = io (bind (sleep 1) (_ : pure \"awake\"))"
   3169           ]
   3170       final @?= ofString "awake"
   3171 
   3172   , testCase "await waits for sleeping child" $ do
   3173       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3174         unlines
   3175           [ "main = io (bind (fork (bind (sleep 1) (_ : pure \"awake\"))) (h :"
   3176           , "  await h))"
   3177           ]
   3178       final @?= ofString "awake"
   3179 
   3180   , testCase "await waits for sleeping child and returns child value" $ do
   3181       (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $
   3182         unlines
   3183           [ "main = io (bind (fork (bind (sleep 1) (_ : pure \"child done\"))) (h :"
   3184           , "  await h))"
   3185           ]
   3186       final @?= ofString "child done"
   3187       st @?= Leaf
   3188 
   3189   , testCase "sleep inside bind resumes as unit" $ do
   3190       (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $
   3191         unlines
   3192           [ "main = io (bind (sleep 1) (_ : pure \"awake\"))"
   3193           ]
   3194       final @?= ofString "awake"
   3195       st @?= Leaf
   3196 
   3197   , testCase "fork await returns child value" $ do
   3198       (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $
   3199         unlines
   3200           [ "main = io (bind (fork (pure \"child done\")) (h :"
   3201           , "  await h))"
   3202           ]
   3203       final @?= ofString "child done"
   3204       st @?= Leaf
   3205 
   3206   -- Scheduler hardening tests
   3207   , testCase "runIO rejects non-IO tree with sentinel error" $ do
   3208       result <- runIO unsafePerms (ofString "not an io program")
   3209       case result of
   3210         Left _  -> return ()
   3211         Right _ -> assertFailure "Expected Left for invalid sentinel"
   3212 
   3213   , testCase "cyclic await returns error instead of hanging" $ do
   3214       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3215         unlines
   3216           [ "main = io (bind (fork (await (pair \"task\" 0))) (h :"
   3217           , "  await h))"
   3218           ]
   3219       final @?= ioErrResult "cyclic await"
   3220 
   3221   , testCase "writeBytes and readFile roundtrip binary data" $
   3222       withSystemTempDirectory "tricu-io-bytes" $ \dir -> do
   3223         let path = dir ++ "/binary.bin"
   3224         final <- runIOSource $
   3225           unlines
   3226             [ "main = io (bind (writeBytes \"" ++ path ++ "\" [(0) (255) (128) (1)])"
   3227             , "  (_ : readFile \"" ++ path ++ "\"))"
   3228             ]
   3229         final @?= ioOkResult (ofBytes (BS.pack [0, 255, 128, 1]))
   3230 
   3231   , testCase "stress test: many concurrent sleepers complete promptly" $ do
   3232       let n = 5000
   3233       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $
   3234         unlines
   3235           [ "spawner = y (self n acc : if (equal? n 0) (pure acc) (bind (fork (sleep 1)) (h : self (pred n) (pair h acc))))"
   3236           , "awaitAll = y (self hs : matchList (pure \"done\") (h r : bind (await h) (_ : self r)) hs)"
   3237           , "main = io (bind (spawner " ++ show n ++ " t) (hs : awaitAll hs))"
   3238           ]
   3239       final @?= ofString "done"
   3240 
   3241   , testCase "long fork await loop does not leak" $ do
   3242       let n = 200
   3243           build 0 = "pure \"done\""
   3244           build k = "bind (fork (pure \"x\")) (h : bind (await h) (_ : " ++ build (k - 1) ++ "))"
   3245       (final, _) <- runIOSourceWith unsafePerms Leaf Leaf ("main = io (" ++ build n ++ ")")
   3246       final @?= ofString "done"
   3247 
   3248   , testGroup "Socket primitives"
   3249     [ testCase "socket returns ok result with valid handle" $ do
   3250         final <- runIOSource "main = io socket"
   3251         final @?= ioOkResult (Fork (ofString "sock") (ofNumber 0))
   3252 
   3253     , testCase "closeSocket on invalid handle returns error" $ do
   3254         final <- runIOSource "main = io (closeSocket (pair \"sock\" 99999))"
   3255         final @?= ioErrResult "invalid socket handle"
   3256 
   3257     , testCase "bindSocket and listen succeed on loopback port 0" $ do
   3258         final <- runIOSource $
   3259           unlines
   3260             [ "main = io ("
   3261             , "  onOk socket (server rest :"
   3262             , "    onOk (bindSocket server \"127.0.0.1\" 0) (_ rest :"
   3263             , "      bind (listen server 1) (listenResult :"
   3264             , "        pure listenResult))))"
   3265             ]
   3266         final @?= ioOkResult Leaf
   3267 
   3268     , testCase "connect to non-listening port returns error" $ do
   3269         final <- runIOSource "main = io (onOk socket (sock rest : connect sock \"127.0.0.1\" 1))"
   3270         case final of
   3271           Fork Leaf (Fork _ Leaf) -> return ()
   3272           other -> assertFailure $ "Expected error result, got: " ++ show other
   3273 
   3274     , testCase "accept and recv receive bytes from forked client" $
   3275         withFreePort $ \port -> do
   3276           final <- runIOSource $
   3277             unlines
   3278               [ "client = port :"
   3279               , "  onOk socket (sock rest :"
   3280               , "    onOk (connect sock \"127.0.0.1\" port) (_ rest :"
   3281               , "      send sock [104 105]))"
   3282               , ""
   3283               , "main = io ("
   3284               , "  onOk socket (server rest :"
   3285               , "    onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :"
   3286               , "      onOk (listen server 1) (_ rest :"
   3287               , "        bind (fork (client " ++ show port ++ ")) (_ :"
   3288               , "          onOk (accept server) (accepted rest :"
   3289               , "            recv (fst accepted) 2))))))"
   3290               ]
   3291           final @?= ioOkResult (ofBytes (BS.pack [104, 105]))
   3292 
   3293     , testCase "client recv receives server response via accepted socket" $
   3294         withFreePort $ \port -> do
   3295           final <- runIOSource $
   3296             unlines
   3297               [ "serverTask = (server :"
   3298               , "  onOk (accept server) (accepted rest :"
   3299               , "    onOk (recv (fst accepted) 4) (msg rest :"
   3300               , "      send (fst accepted) [112 111 110 103])))"
   3301               , ""
   3302               , "clientTask = (port :"
   3303               , "  onOk socket (sock rest :"
   3304               , "    onOk (connect sock \"127.0.0.1\" port) (_ rest :"
   3305               , "      bind (send sock [112 105 110 103]) (_ :"
   3306               , "        recv sock 4))))"
   3307               , ""
   3308               , "main = io ("
   3309               , "  onOk socket (server rest :"
   3310               , "    onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :"
   3311               , "      onOk (listen server 1) (_ rest :"
   3312               , "        bind (fork (serverTask server)) (_ :"
   3313               , "          clientTask " ++ show port ++ ")))))"
   3314               ]
   3315           final @?= ioOkResult (ofBytes (BS.pack [112, 111, 110, 103]))
   3316 
   3317     , testCase "recv on closed peer returns connection closed" $
   3318         withFreePort $ \port -> do
   3319           final <- runIOSource $
   3320             unlines
   3321               [ "clientTask = port :"
   3322               , "  onOk socket (sock rest :"
   3323               , "    onOk (connect sock \"127.0.0.1\" port) (_ rest :"
   3324               , "      closeSocket sock))"
   3325               , ""
   3326               , "main = io ("
   3327               , "  onOk socket (server rest :"
   3328               , "    onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :"
   3329               , "      onOk (listen server 1) (_ rest :"
   3330               , "        bind (fork (clientTask " ++ show port ++ ")) (_ :"
   3331               , "          onOk (accept server) (accepted rest :"
   3332               , "            bind (yield) (_ :"
   3333               , "              recv (fst accepted) 1)))))))"
   3334               ]
   3335           final @?= ioErrResult "connection closed"
   3336 
   3337     , testCase "accept invalid socket handle returns error" $ do
   3338         final <- runIOSource "main = io (accept (pair \"sock\" 99999))"
   3339         final @?= ioErrResult "invalid socket handle"
   3340 
   3341     , testCase "recv invalid socket handle returns error" $ do
   3342         final <- runIOSource "main = io (recv (pair \"sock\" 99999) 1)"
   3343         final @?= ioErrResult "invalid socket handle"
   3344 
   3345     , testCase "send invalid socket handle returns error" $ do
   3346         final <- runIOSource "main = io (send (pair \"sock\" 99999) [(1)])"
   3347         final @?= ioErrResult "invalid socket handle"
   3348 
   3349     , testCase "getSocketName returns positive port after bind 0" $ do
   3350         final <- runIOSource $
   3351           unlines
   3352             [ "main = io ("
   3353             , "  onOk socket (server rest :"
   3354             , "    onOk (bindSocket server \"127.0.0.1\" 0) (_ rest :"
   3355             , "      bind (getSocketName server) (nameResult :"
   3356             , "        pure nameResult))))"
   3357             ]
   3358         case final of
   3359           Fork (Stem Leaf) (Fork val Leaf) ->
   3360             case toNumber val of
   3361               Right port | port > 0 -> return ()
   3362               Right 0 -> assertFailure "Expected positive port, got 0"
   3363               Left _  -> assertFailure $ "Expected numeric port, got: " ++ show val
   3364           other -> assertFailure $ "Expected ok result, got: " ++ show other
   3365 
   3366     , testCase "connectTo creates connected socket" $
   3367         withFreePort $ \port -> do
   3368           final <- runIOSource $
   3369             unlines
   3370               [ "clientTask = port :"
   3371               , "  onOk (connectTo \"127.0.0.1\" port) (client rest :"
   3372               , "    onOk (send client [104 105]) (_ rest :"
   3373               , "      pure t))"
   3374               , ""
   3375               , "main = io ("
   3376               , "  onOk socket (server rest :"
   3377               , "    onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :"
   3378               , "      onOk (listen server 1) (_ rest :"
   3379               , "        bind (fork (clientTask " ++ show port ++ ")) (_ :"
   3380               , "          onOk (accept server) (accepted rest :"
   3381               , "            onOk (recv (fst accepted) 2) (msg rest :"
   3382               , "              pure msg)))))))"
   3383               ]
   3384           final @?= ofBytes (BS.pack [104, 105])
   3385 
   3386     , testCase "serveOnce handles a single client connection" $
   3387         withFreePort $ \port -> do
   3388           final <- runIOSource $
   3389             unlines
   3390               [ "echoHandler = (client peer :"
   3391               , "  onOk (recv client 2) (msg rest :"
   3392               , "    onOk (send client msg) (_ rest :"
   3393               , "      pure t)))"
   3394               , ""
   3395               , "clientTask = (port :"
   3396               , "  onOk socket (sock rest :"
   3397               , "    onOk (connect sock \"127.0.0.1\" port) (_ rest :"
   3398               , "      onOk (send sock [104 105]) (_ rest :"
   3399               , "        onOk (recv sock 2) (msg rest :"
   3400               , "          pure msg)))))"
   3401               , ""
   3402               , "main = io ("
   3403               , "  onOk socket (server rest :"
   3404               , "    onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :"
   3405               , "      onOk (listen server 1) (_ rest :"
   3406               , "        bind (fork (serveOnce server echoHandler)) (_ :"
   3407               , "          clientTask " ++ show port ++ ")))))"
   3408               ]
   3409           final @?= ofBytes (BS.pack [104, 105])
   3410 
   3411   , testCase "finally preserves successful action result" $ do
   3412       final <- runIOSource $
   3413         unlines
   3414           [ "main = io (finally (pure 42) (pure 99))"
   3415           ]
   3416       final @?= ofNumber 42
   3417 
   3418   , testCase "finally runs cleanup after successful action" $
   3419       withSystemTempDirectory "tricu-finally" $ \dir -> do
   3420         let cleanupPath = dir ++ "/cleanup.txt"
   3421         final <- runIOSource $
   3422           unlines
   3423             [ "main = io (finally"
   3424             , "  (pure 42)"
   3425             , "  (writeFile \"" ++ cleanupPath ++ "\" \"cleaned\"))"
   3426             ]
   3427         final @?= ofNumber 42
   3428         contents <- readFile cleanupPath
   3429         contents @?= "cleaned"
   3430 
   3431   , testCase "bracket passes acquired resource to use" $ do
   3432       final <- runIOSource $
   3433         unlines
   3434           [ "main = io (bracket (pure 41) (_ : pure t) (r : pure (succ r)))"
   3435           ]
   3436       final @?= ofNumber 42
   3437 
   3438   , testCase "bracket preserves successful use result over release result" $ do
   3439       final <- runIOSource $
   3440         unlines
   3441           [ "main = io (bracket (pure \"res\") (_ : pure 123) (_ : pure 99))"
   3442           ]
   3443       final @?= ofNumber 99
   3444 
   3445   , testCase "bracket runs release on successful use" $
   3446       withSystemTempDirectory "tricu-bracket" $ \dir -> do
   3447         let releasePath = dir ++ "/release.txt"
   3448         final <- runIOSource $
   3449           unlines
   3450             [ "main = io (bracket"
   3451             , "  (pure \"" ++ releasePath ++ "\")"
   3452             , "  (path : writeFile path \"released\")"
   3453             , "  (path : pure 99))"
   3454             ]
   3455         final @?= ofNumber 99
   3456         contents <- readFile releasePath
   3457         contents @?= "released"
   3458 
   3459   , testCase "bracket passes acquired resource to release" $
   3460       withSystemTempDirectory "tricu-bracket-release-resource" $ \dir -> do
   3461         let releasePath = dir ++ "/release.txt"
   3462         final <- runIOSource $
   3463           unlines
   3464             [ "main = io (bracket"
   3465             , "  (pure \"" ++ releasePath ++ "\")"
   3466             , "  (path : writeFile path \"released\")"
   3467             , "  (_ : pure 99))"
   3468             ]
   3469         final @?= ofNumber 99
   3470         contents <- readFile releasePath
   3471         contents @?= "released"
   3472 
   3473     -- Directory and file management primitives
   3474   , testGroup "listDirectory"
   3475     [ testCase "listDirectory returns entry names" $
   3476         withSystemTempDirectory "tricu-listdir" $ \dir -> do
   3477           writeFile (dir ++ "/a.txt") "a"
   3478           writeFile (dir ++ "/b.txt") "b"
   3479           final <- runIOSource $
   3480             unlines
   3481               [ "main = io (onListDirectory \"" ++ dir ++ "\""
   3482               , "  (err rest : pure false)"
   3483               , "  (entries rest :"
   3484               , "    pure (pair (lExist? \"a.txt\" entries) (lExist? \"b.txt\" entries))))"
   3485               ]
   3486           final @?= Fork (Stem Leaf) (Stem Leaf)
   3487 
   3488     , testCase "listDirectory missing path returns does not exist" $ do
   3489         final <- runIOSource $
   3490           unlines
   3491             [ "main = io (onListDirectory \"/nonexistent/path/12345\""
   3492             , "  (err rest : pure err)"
   3493             , "  (_ rest : pure \"ok\"))"
   3494             ]
   3495         final @?= ofString "does not exist"
   3496 
   3497     , testCase "listDirectory on file returns not a directory" $
   3498         withSystemTempDirectory "tricu-listdir-file" $ \dir -> do
   3499           let path = dir ++ "/file.txt"
   3500           writeFile path "x"
   3501           final <- runIOSource $
   3502             unlines
   3503               [ "main = io (onListDirectory \"" ++ path ++ "\""
   3504               , "  (err rest : pure err)"
   3505               , "  (_ rest : pure \"ok\"))"
   3506               ]
   3507           final @?= ofString "not a directory"
   3508 
   3509     , testCase "listDirectory denied path returns permission denied" $
   3510         withSystemTempDirectory "tricu-listdir-denied" $ \dir -> do
   3511           let allowedDir = dir ++ "/allowed"
   3512               deniedDir = dir ++ "/denied"
   3513           createDirectory allowedDir
   3514           createDirectory deniedDir
   3515           let perms = defaultPerms { allowRead = [allowedDir] }
   3516           final <- runIOSourceWithPerms perms $
   3517             unlines
   3518               [ "main = io (listDirectory \"" ++ deniedDir ++ "\")"
   3519               ]
   3520           final @?= ioErrResult "permission denied"
   3521     ]
   3522 
   3523     , testCase "listDirectory excludes dot entries" $
   3524        withSystemTempDirectory "tricu-listdir-dot" $ \dir -> do
   3525          final <- runIOSource $
   3526            unlines
   3527              [ "main = io (onListDirectory \"" ++ dir ++ "\""
   3528              , "  (err rest : pure false)"
   3529              , "  (entries rest :"
   3530              , "    pure (pair (lExist? \".\" entries) (lExist? \"..\" entries))))"
   3531              ]
   3532          final @?= Fork Leaf Leaf
   3533 
   3534   , testGroup "renameFile"
   3535     [ testCase "renameFile moves file atomically" $
   3536         withSystemTempDirectory "tricu-rename" $ \dir -> do
   3537           let oldPath = dir ++ "/old.txt"
   3538               newPath = dir ++ "/new.txt"
   3539           writeFile oldPath "contents"
   3540           final <- runIOSource $
   3541             unlines
   3542               [ "main = io (onRenameFile \"" ++ oldPath ++ "\" \"" ++ newPath ++ "\""
   3543               , "  (err rest : pure err)"
   3544               , "  (_ rest : pure \"ok\"))"
   3545               ]
   3546           final @?= ofString "ok"
   3547           newExists <- doesFileExist newPath
   3548           oldExists <- doesFileExist oldPath
   3549           newExists @?= True
   3550           oldExists @?= False
   3551 
   3552     , testCase "renameFile missing source returns does not exist" $ do
   3553         final <- runIOSource $
   3554           unlines
   3555             [ "main = io (onRenameFile \"/nonexistent/old.txt\" \"/nonexistent/new.txt\""
   3556             , "  (err rest : pure err)"
   3557             , "  (_ rest : pure \"ok\"))"
   3558             ]
   3559         final @?= ofString "does not exist"
   3560 
   3561     , testCase "renameFile denied destination returns permission denied" $
   3562         withSystemTempDirectory "tricu-rename-denied" $ \dir -> do
   3563           let allowedDir = dir ++ "/allowed"
   3564               deniedDir = dir ++ "/denied"
   3565           createDirectory allowedDir
   3566           createDirectory deniedDir
   3567           let oldPath = allowedDir ++ "/old.txt"
   3568               newPath = deniedDir ++ "/new.txt"
   3569           writeFile oldPath "contents"
   3570           let perms = defaultPerms { allowWrite = [allowedDir] }
   3571           final <- runIOSourceWithPerms perms $
   3572             unlines
   3573               [ "main = io (renameFile \"" ++ oldPath ++ "\" \"" ++ newPath ++ "\")"
   3574               ]
   3575           final @?= ioErrResult "permission denied"
   3576 
   3577     , testCase "renameFile replaces existing destination atomically" $
   3578       withSystemTempDirectory "tricu-rename-replace" $ \dir -> do
   3579         let oldPath = dir ++ "/old.txt"
   3580             newPath = dir ++ "/new.txt"
   3581         writeFile oldPath "new"
   3582         writeFile newPath "old"
   3583         final <- runIOSource $
   3584           unlines
   3585             [ "main = io (onRenameFile \"" ++ oldPath ++ "\" \"" ++ newPath ++ "\""
   3586             , "  (err rest : pure err)"
   3587             , "  (_ rest : pure \"ok\"))"
   3588             ]
   3589         final @?= ofString "ok"
   3590         readFile newPath >>= (@?= "new")
   3591         oldExists <- doesFileExist oldPath
   3592         oldExists @?= False
   3593     ]
   3594 
   3595   , testGroup "createDirectory"
   3596     [ testCase "createDirectory creates new directory" $
   3597         withSystemTempDirectory "tricu-mkdir" $ \dir -> do
   3598           let newDir = dir ++ "/subdir"
   3599           final <- runIOSource $
   3600             unlines
   3601               [ "main = io (onCreateDirectory \"" ++ newDir ++ "\""
   3602               , "  (err rest : pure err)"
   3603               , "  (_ rest : pure \"ok\"))"
   3604               ]
   3605           final @?= ofString "ok"
   3606           exists <- doesDirectoryExist newDir
   3607           exists @?= True
   3608 
   3609     , testCase "createDirectory is idempotent for existing directory" $
   3610         withSystemTempDirectory "tricu-mkdir-idempotent" $ \dir -> do
   3611           let existingDir = dir ++ "/exists"
   3612           createDirectory existingDir
   3613           final <- runIOSource $
   3614             unlines
   3615               [ "main = io (onCreateDirectory \"" ++ existingDir ++ "\""
   3616               , "  (err rest : pure err)"
   3617               , "  (_ rest : pure \"ok\"))"
   3618               ]
   3619           final @?= ofString "ok"
   3620 
   3621     , testCase "createDirectory on existing file returns already exists" $
   3622         withSystemTempDirectory "tricu-mkdir-file" $ \dir -> do
   3623           let path = dir ++ "/file.txt"
   3624           writeFile path "x"
   3625           final <- runIOSource $
   3626             unlines
   3627               [ "main = io (onCreateDirectory \"" ++ path ++ "\""
   3628               , "  (err rest : pure err)"
   3629               , "  (_ rest : pure \"ok\"))"
   3630               ]
   3631           final @?= ofString "already exists"
   3632 
   3633     , testCase "createDirectory missing parent returns does not exist" $ do
   3634         final <- runIOSource $
   3635           unlines
   3636             [ "main = io (onCreateDirectory \"/nonexistent/path/12345/sub\""
   3637             , "  (err rest : pure err)"
   3638             , "  (_ rest : pure \"ok\"))"
   3639             ]
   3640         final @?= ofString "does not exist"
   3641 
   3642     , testCase "createDirectory denied path returns permission denied" $
   3643         withSystemTempDirectory "tricu-mkdir-denied" $ \dir -> do
   3644           let allowedDir = dir ++ "/allowed"
   3645               deniedDir = dir ++ "/denied"
   3646           createDirectory allowedDir
   3647           createDirectory deniedDir
   3648           let perms = defaultPerms { allowWrite = [allowedDir] }
   3649           final <- runIOSourceWithPerms perms $
   3650             unlines
   3651               [ "main = io (createDirectory \"" ++ deniedDir ++ "/new\")"
   3652               ]
   3653           final @?= ioErrResult "permission denied"
   3654     , testCase "createDirectory with file parent returns not a directory or does not exist" $
   3655         withSystemTempDirectory "tricu-mkdir-file-parent" $ \dir -> do
   3656           let parentFile = dir ++ "/file"
   3657               child = parentFile ++ "/sub"
   3658           writeFile parentFile "x"
   3659           final <- runIOSource $
   3660             unlines
   3661               [ "main = io (onCreateDirectory \"" ++ child ++ "\""
   3662               , "  (err rest : pure err)"
   3663               , "  (_ rest : pure \"ok\"))"
   3664               ]
   3665           final @?= ofString "not a directory"
   3666     ]
   3667 
   3668   , testGroup "deleteFile"
   3669     [ testCase "deleteFile removes file" $
   3670         withSystemTempDirectory "tricu-delete" $ \dir -> do
   3671           let path = dir ++ "/del.txt"
   3672           writeFile path "x"
   3673           final <- runIOSource $
   3674             unlines
   3675               [ "main = io (onDeleteFile \"" ++ path ++ "\""
   3676               , "  (err rest : pure err)"
   3677               , "  (_ rest : pure \"ok\"))"
   3678               ]
   3679           final @?= ofString "ok"
   3680           exists <- doesFileExist path
   3681           exists @?= False
   3682 
   3683     , testCase "deleteFile is idempotent for missing file" $ do
   3684         final <- runIOSource $
   3685           unlines
   3686               [ "main = io (onDeleteFile \"/nonexistent/path/12345.txt\""
   3687               , "  (err rest : pure err)"
   3688               , "  (_ rest : pure \"ok\"))"
   3689               ]
   3690         final @?= ofString "ok"
   3691 
   3692     , testCase "deleteFile on directory returns is a directory" $
   3693         withSystemTempDirectory "tricu-delete-dir" $ \dir -> do
   3694           let subDir = dir ++ "/subdir"
   3695           createDirectory subDir
   3696           final <- runIOSource $
   3697             unlines
   3698               [ "main = io (onDeleteFile \"" ++ subDir ++ "\""
   3699               , "  (err rest : pure err)"
   3700               , "  (_ rest : pure \"ok\"))"
   3701               ]
   3702           final @?= ofString "is a directory"
   3703 
   3704     , testCase "deleteFile denied path returns permission denied" $
   3705         withSystemTempDirectory "tricu-delete-denied" $ \dir -> do
   3706           let allowedDir = dir ++ "/allowed"
   3707               deniedDir = dir ++ "/denied"
   3708           createDirectory allowedDir
   3709           createDirectory deniedDir
   3710           let path = deniedDir ++ "/file.txt"
   3711           writeFile path "x"
   3712           let perms = defaultPerms { allowWrite = [allowedDir] }
   3713           final <- runIOSourceWithPerms perms $
   3714             unlines
   3715               [ "main = io (deleteFile \"" ++ path ++ "\")"
   3716               ]
   3717           final @?= ioErrResult "permission denied"
   3718     ]
   3719 
   3720   , testGroup "fileExists"
   3721     [ testCase "fileExists true for existing file" $
   3722         withSystemTempDirectory "tricu-exists" $ \dir -> do
   3723           let path = dir ++ "/file.txt"
   3724           writeFile path "x"
   3725           final <- runIOSource $
   3726             unlines
   3727               [ "main = io (onFileExists \"" ++ path ++ "\""
   3728               , "  (err rest : pure err)"
   3729               , "  (exists rest : pure exists))"
   3730               ]
   3731           final @?= Stem Leaf
   3732 
   3733     , testCase "fileExists false for missing path" $ do
   3734         final <- runIOSource $
   3735           unlines
   3736               [ "main = io (onFileExists \"/nonexistent/path/12345.txt\""
   3737               , "  (err rest : pure err)"
   3738               , "  (exists rest : pure exists))"
   3739               ]
   3740         final @?= Leaf
   3741 
   3742     , testCase "fileExists denied path returns permission denied" $
   3743         withSystemTempDirectory "tricu-exists-denied" $ \dir -> do
   3744           let allowedDir = dir ++ "/allowed"
   3745               deniedDir = dir ++ "/denied"
   3746           createDirectory allowedDir
   3747           createDirectory deniedDir
   3748           let path = deniedDir ++ "/file.txt"
   3749           writeFile path "x"
   3750           let perms = defaultPerms { allowRead = [allowedDir] }
   3751           final <- runIOSourceWithPerms perms $
   3752             unlines
   3753               [ "main = io (fileExists \"" ++ path ++ "\")"
   3754               ]
   3755           final @?= ioErrResult "permission denied"
   3756     ]
   3757 
   3758   , testGroup "sha256Hex"
   3759     [ testCase "sha256Hex returns lowercase hex digest" $ do
   3760         final <- runIOSource $
   3761           unlines
   3762             [ "main = io (onSha256Hex [(104) (105)]"
   3763             , "  (err rest : pure err)"
   3764             , "  (hex rest : pure hex))"
   3765             ]
   3766         final @?= ofString "8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4"
   3767 
   3768     , testCase "sha256Hex empty bytes returns empty digest" $ do
   3769         final <- runIOSource $
   3770           unlines
   3771             [ "main = io (onSha256Hex []"
   3772             , "  (err rest : pure err)"
   3773             , "  (hex rest : pure hex))"
   3774             ]
   3775         final @?= ofString "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
   3776 
   3777     , testCase "sha256Hex hashes raw bytes" $ do
   3778       final <- runIOSource $
   3779         unlines
   3780           [ "main = io (onSha256Hex [(0) (255) (1)]"
   3781           , "  (err rest : pure err)"
   3782           , "  (hex rest : pure hex))"
   3783           ]
   3784       final @?= ofString "47ffa3ea45a70b8a41c2c0825df323c00a8b7a01c1ea06083cc41dddcc001123"
   3785     ]
   3786 
   3787   , testGroup "currentTime"
   3788     [ testCase "currentTime returns a positive integer" $ do
   3789         final <- runIOSource $
   3790           unlines
   3791             [ "main = io (onCurrentTime"
   3792             , "  (err rest : pure 0)"
   3793             , "  (v rest : pure v))"
   3794             ]
   3795         case toNumber final of
   3796           Right n | n > 1600000000 -> return ()  -- after ~Sep 2020
   3797           Right n -> assertFailure $ "Expected recent timestamp, got: " ++ show n
   3798           Left err -> assertFailure $ "Expected number, got error: " ++ err
   3799     ]
   3800     ]
   3801   ]
   3802 
   3803 httpParsingTests :: TestTree
   3804 httpParsingTests = testGroup "HTTP Parsing Tests"
   3805   [
   3806     -- chomp / request-line reader
   3807     testCase "chomp strips trailing CR" $ do
   3808       let input = "chomp [(104) (105) (13)]"
   3809           env = evalTricu allTestLibsEnv (parseTricu input)
   3810       result env @?= bytesT [104, 105]
   3811 
   3812   , testCase "chomp leaves line without CR" $ do
   3813       let input = "chomp [(104) (105)]"
   3814           env = evalTricu allTestLibsEnv (parseTricu input)
   3815       result env @?= bytesT [104, 105]
   3816 
   3817   , testCase "chomp empty list" $ do
   3818       let input = "chomp []"
   3819           env = evalTricu allTestLibsEnv (parseTricu input)
   3820       result env @?= bytesT []
   3821 
   3822   , testCase "readLineBytes with CRLF" $ do
   3823       let input = "readLineBytes [(104) (105) (13) (10) (120)]"
   3824           env = evalTricu allTestLibsEnv (parseTricu input)
   3825       result env @?= pairT (bytesT [104, 105]) (bytesT [120])
   3826 
   3827   , testCase "readLineBytes with bare LF" $ do
   3828       let input = "readLineBytes [(104) (105) (10) (120)]"
   3829           env = evalTricu allTestLibsEnv (parseTricu input)
   3830       result env @?= pairT (bytesT [104, 105]) (bytesT [120])
   3831 
   3832   , testCase "readLineBytes empty line" $ do
   3833       let input = "readLineBytes [(13) (10) (120)]"
   3834           env = evalTricu allTestLibsEnv (parseTricu input)
   3835       result env @?= pairT (bytesT []) (bytesT [120])
   3836 
   3837   , testCase "readLineBytes EOF mid-line returns line" $ do
   3838       let input = "readLineBytes [(104) (105)]"
   3839           env = evalTricu allTestLibsEnv (parseTricu input)
   3840       result env @?= pairT (bytesT [104, 105]) (bytesT [])
   3841 
   3842     -- parseRequestLine
   3843   , testCase "parseRequestLine GET slash" $ do
   3844       let input = "parseRequestLine (append \"GET / HTTP/1.1\\r\\n\" \"x\")"
   3845           env = evalTricu allTestLibsEnv (parseTricu input)
   3846       result env @?= parserOk
   3847         (pairT (ofString "GET") (pairT (ofString "/") (ofString "HTTP/1.1")))
   3848         (ofString "x")
   3849 
   3850   , testCase "parseRequestLine POST path" $ do
   3851       let input = "parseRequestLine \"POST /foo/bar HTTP/1.1\\r\\n\""
   3852           env = evalTricu allTestLibsEnv (parseTricu input)
   3853       result env @?= parserOk
   3854         (pairT (ofString "POST") (pairT (ofString "/foo/bar") (ofString "HTTP/1.1")))
   3855         (ofString "")
   3856 
   3857   , testCase "parseRequestLine too short" $ do
   3858       let input = "parseRequestLine \"GET\\r\\n\""
   3859           env = evalTricu allTestLibsEnv (parseTricu input)
   3860       result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n")
   3861 
   3862   , testCase "parseRequestLine no version" $ do
   3863       let input = "parseRequestLine \"GET /foo\\r\\n\""
   3864           env = evalTricu allTestLibsEnv (parseTricu input)
   3865       result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n")
   3866 
   3867   , testCase "parseRequestLine empty line" $ do
   3868       let input = "parseRequestLine \"\\r\\n\""
   3869           env = evalTricu allTestLibsEnv (parseTricu input)
   3870       result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n")
   3871 
   3872   , testCase "parseRequestLine rejects extra fields" $ do
   3873       let input = "parseRequestLine \"GET / HTTP/1.1 wat\\r\\n\""
   3874           env = evalTricu allTestLibsEnv (parseTricu input)
   3875       result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n")
   3876 
   3877     -- parseHeaders
   3878   , testCase "parseHeaders two headers lowercases names" $ do
   3879       let input = "parseHeaders (append \"Host: localhost\\r\\nContent-Length: 42\\r\\n\\r\\n\" \"x\")"
   3880           env = evalTricu allTestLibsEnv (parseTricu input)
   3881       result env @?= parserOk
   3882         (ofList
   3883           [ pairT (ofString "host") (ofString "localhost")
   3884           , pairT (ofString "content-length") (ofString "42")
   3885           ])
   3886         (ofString "x")
   3887 
   3888   , testCase "parseHeaders preserves colon in value" $ do
   3889       let input = "parseHeaders (append \"X-Custom: a: b\\r\\n\\r\\n\" \"x\")"
   3890           env = evalTricu allTestLibsEnv (parseTricu input)
   3891       result env @?= parserOk
   3892         (ofList [pairT (ofString "x-custom") (ofString "a: b")])
   3893         (ofString "x")
   3894 
   3895   , testCase "parseHeaders accepts empty value" $ do
   3896       let input = "parseHeaders (append \"X-Empty:\\r\\n\\r\\n\" \"x\")"
   3897           env = evalTricu allTestLibsEnv (parseTricu input)
   3898       result env @?= parserOk
   3899         (ofList [pairT (ofString "x-empty") (ofString "")])
   3900         (ofString "x")
   3901 
   3902   , testCase "parseHeaders immediate blank" $ do
   3903       let input = "parseHeaders \"\\r\\nx\""
   3904           env = evalTricu allTestLibsEnv (parseTricu input)
   3905       result env @?= parserOk (ofList []) (ofString "x")
   3906 
   3907   , testCase "parseHeaders rejects missing colon" $ do
   3908       let input = "parseHeaders \"Host\\r\\n\\r\\n\""
   3909           env = evalTricu allTestLibsEnv (parseTricu input)
   3910       result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n")
   3911 
   3912   , testCase "parseContentLengthValue accepts max body bytes" $ do
   3913       let input = "matchResult \"err\" (maybeLen rest : \"ok\") (parseContentLengthValue \"1048576\")"
   3914           env = evalTricu allTestLibsEnv (parseTricu input)
   3915       result env @?= ofString "ok"
   3916 
   3917   , testCase "parseContentLengthValue accepts shorter decimal below max" $ do
   3918       let input = "matchResult \"err\" (maybeLen rest : \"ok\") (parseContentLengthValue \"999999\")"
   3919           env = evalTricu allTestLibsEnv (parseTricu input)
   3920       result env @?= ofString "ok"
   3921 
   3922   , testCase "parseContentLengthValue strips leading zeros before limit check" $ do
   3923       let input = "parseContentLengthValue \"0000000000001\""
   3924           env = evalTricu allTestLibsEnv (parseTricu input)
   3925       result env @?= parserOk (justT (ofNumber 1)) Leaf
   3926 
   3927   , testCase "parseContentLengthValue rejects body above max" $ do
   3928       let input = "parseContentLengthValue \"1048577\""
   3929           env = evalTricu allTestLibsEnv (parseTricu input)
   3930       result env @?= parserErr (ofNumber 413) (ofString "Request body too large\n")
   3931 
   3932   , testCase "parseContentLengthValue rejects longer body above max" $ do
   3933       let input = "parseContentLengthValue \"2000000\""
   3934           env = evalTricu allTestLibsEnv (parseTricu input)
   3935       result env @?= parserErr (ofNumber 413) (ofString "Request body too large\n")
   3936 
   3937     -- statusLine / headerLine
   3938   , testCase "statusLine 200 OK" $ do
   3939       let input = "statusLine 200 \"OK\""
   3940           env = evalTricu allTestLibsEnv (parseTricu input)
   3941       result env @?= ofString "HTTP/1.1 200 OK\r\n"
   3942 
   3943   , testCase "headerLine Content-Length" $ do
   3944       let input = "headerLine \"Content-Length\" \"42\""
   3945           env = evalTricu allTestLibsEnv (parseTricu input)
   3946       result env @?= ofString "Content-Length: 42\r\n"
   3947 
   3948     -- statusPhrase
   3949   , testCase "statusPhrase 200" $ do
   3950       let input = "statusPhrase 200"
   3951           env = evalTricu allTestLibsEnv (parseTricu input)
   3952       result env @?= ofString "OK"
   3953 
   3954   , testCase "statusPhrase 201" $ do
   3955       let input = "statusPhrase 201"
   3956           env = evalTricu allTestLibsEnv (parseTricu input)
   3957       result env @?= ofString "Created"
   3958 
   3959   , testCase "statusPhrase 204" $ do
   3960       let input = "statusPhrase 204"
   3961           env = evalTricu allTestLibsEnv (parseTricu input)
   3962       result env @?= ofString "No Content"
   3963 
   3964   , testCase "statusPhrase 400" $ do
   3965       let input = "statusPhrase 400"
   3966           env = evalTricu allTestLibsEnv (parseTricu input)
   3967       result env @?= ofString "Bad Request"
   3968 
   3969   , testCase "statusPhrase 404" $ do
   3970       let input = "statusPhrase 404"
   3971           env = evalTricu allTestLibsEnv (parseTricu input)
   3972       result env @?= ofString "Not Found"
   3973 
   3974   , testCase "statusPhrase 405" $ do
   3975       let input = "statusPhrase 405"
   3976           env = evalTricu allTestLibsEnv (parseTricu input)
   3977       result env @?= ofString "Method Not Allowed"
   3978 
   3979   , testCase "statusPhrase 431" $ do
   3980       let input = "statusPhrase 431"
   3981           env = evalTricu allTestLibsEnv (parseTricu input)
   3982       result env @?= ofString "Request Header Fields Too Large"
   3983 
   3984   , testCase "statusPhrase 501" $ do
   3985       let input = "statusPhrase 501"
   3986           env = evalTricu allTestLibsEnv (parseTricu input)
   3987       result env @?= ofString "Not Implemented"
   3988 
   3989   , testCase "statusPhrase 505" $ do
   3990       let input = "statusPhrase 505"
   3991           env = evalTricu allTestLibsEnv (parseTricu input)
   3992       result env @?= ofString "HTTP Version Not Supported"
   3993 
   3994   , testCase "statusPhrase 500" $ do
   3995       let input = "statusPhrase 500"
   3996           env = evalTricu allTestLibsEnv (parseTricu input)
   3997       result env @?= ofString "Internal Server Error"
   3998 
   3999   , testCase "statusPhrase unknown" $ do
   4000       let input = "statusPhrase 999"
   4001           env = evalTricu allTestLibsEnv (parseTricu input)
   4002       result env @?= ofString "Internal Server Error"
   4003 
   4004     -- buildResponse
   4005   , testCase "buildResponse 200 no headers" $ do
   4006       let input = "buildResponse 200 [] \"hi\""
   4007           env = evalTricu allTestLibsEnv (parseTricu input)
   4008       result env @?= ofString "HTTP/1.1 200 OK\r\n\r\nhi"
   4009 
   4010   , testCase "buildResponse 404 with header" $ do
   4011       let input = "buildResponse 404 [(pair \"Content-Length\" \"9\")] \"Not found\""
   4012           env = evalTricu allTestLibsEnv (parseTricu input)
   4013       result env @?= ofString "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot found"
   4014 
   4015     -- convenience responses
   4016   , testCase "okResponse" $ do
   4017       let input = "okResponse \"hi\""
   4018           env = evalTricu allTestLibsEnv (parseTricu input)
   4019       result env @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi"
   4020 
   4021   , testCase "notFoundResponse" $ do
   4022       let input = "notFoundResponse"
   4023           env = evalTricu allTestLibsEnv (parseTricu input)
   4024       result env @?= ofString "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 10\r\nConnection: close\r\n\r\nNot found\n"
   4025 
   4026   , testCase "textResponse" $ do
   4027       let input = "textResponse \"hi\""
   4028           env = evalTricu allTestLibsEnv (parseTricu input)
   4029       result env @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi"
   4030 
   4031   , testCase "jsonResponse" $ do
   4032       let input = "jsonResponse \"{}\""
   4033           env = evalTricu allTestLibsEnv (parseTricu input)
   4034       result env @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}"
   4035 
   4036   , testCase "createdResponse" $ do
   4037       let input = "createdResponse \"created\\n\""
   4038           env = evalTricu allTestLibsEnv (parseTricu input)
   4039       result env @?= ofString "HTTP/1.1 201 Created\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 8\r\nConnection: close\r\n\r\ncreated\n"
   4040 
   4041   , testCase "emptyResponse 204" $ do
   4042       let input = "emptyResponse 204"
   4043           env = evalTricu allTestLibsEnv (parseTricu input)
   4044       result env @?= ofString "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
   4045 
   4046   , testCase "badRequestResponse" $ do
   4047       let input = "badRequestResponse \"Bad Request\\n\""
   4048           env = evalTricu allTestLibsEnv (parseTricu input)
   4049       result env @?= ofString "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 12\r\nConnection: close\r\n\r\nBad Request\n"
   4050 
   4051   , testCase "errorResponse 405" $ do
   4052       let input = "errorResponse 405 \"Method Not Allowed\\n\""
   4053           env = evalTricu allTestLibsEnv (parseTricu input)
   4054       result env @?= ofString "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 19\r\nConnection: close\r\n\r\nMethod Not Allowed\n"
   4055   ]
   4056 
   4057 containsAll :: String -> [String] -> Assertion
   4058 containsAll text needles =
   4059   forM_ needles $ \needle ->
   4060     assertBool ("expected " ++ show needle ++ " in: " ++ text) (needle `isInfixOf` text)
   4061 
   4062 withFreePort :: (Int -> IO a) -> IO a
   4063 withFreePort action =
   4064   bracket
   4065     (NS.socket NS.AF_INET NS.Stream NS.defaultProtocol)
   4066     NS.close
   4067     (\s -> do
   4068       NS.setSocketOption s NS.ReuseAddr 1
   4069       NS.bind s (NS.SockAddrInet 0 (NS.tupleToHostAddress (127, 0, 0, 1)))
   4070       port <- NS.socketPort s
   4071       action (fromIntegral port))
   4072 
   4073 runIOSourceWith :: IOPermissions -> T -> T -> String -> IO (T, T)
   4074 runIOSourceWith perms readerEnv initialState source = do
   4075   let asts = parseTricu source
   4076       evalEnv = evalTricu allTestLibsEnv asts
   4077       fullTree = mainResult evalEnv
   4078   result <- runIOWith perms readerEnv initialState fullTree
   4079   case result of
   4080     Left err   -> assertFailure ("IO runtime error: " ++ err)
   4081     Right pair -> pure pair
   4082 
   4083 runIOSource :: String -> IO T
   4084 runIOSource source = fmap fst $ runIOSourceWith unsafePerms Leaf Leaf source
   4085 
   4086 runIOSourceWithPerms :: IOPermissions -> String -> IO T
   4087 runIOSourceWithPerms perms source = fmap fst $ runIOSourceWith perms Leaf Leaf source
   4088 
   4089 runIOSourceWithEnv :: IOPermissions -> T -> String -> IO T
   4090 runIOSourceWithEnv perms readerEnv source = fmap fst $ runIOSourceWith perms readerEnv Leaf source
   4091 
   4092 ioOkResult :: T -> T
   4093 ioOkResult val = Fork (Stem Leaf) (Fork val Leaf)
   4094 
   4095 ioErrResult :: String -> T
   4096 ioErrResult msg = Fork Leaf (Fork (ofString msg) Leaf)