Spec.hs (183643B)
1 module Main where 2 3 import WorkspaceTests (workspaceTests) 4 import IOBoundaryTests (ioBoundaryTests) 5 import PortableCompilerTests (portableCompilerTests) 6 import ExecutableLoadingTests (executableLoadingTests) 7 import ScriptedEffects (runScriptedEffects) 8 import IODriverTests (ioDriverTests) 9 import LibraryProgramTests (maybeTests, providedLibraries, contractsTests, arithmetic) 10 import qualified ParserValueTests 11 import qualified LibraryValueTests 12 import PortableTestCompiler (compileFixtureFile, compileFixtureDefinitions) 13 import ReferenceFixtures 14 import PackageTests (packageTests) 15 import InteractionInspectionTests (interactionInspectionTests) 16 import REPL 17 import Runtime 18 import TreeObservation (forceTreeShared) 19 import qualified NodeTable as Nodes 20 import Wire 21 import SupportFixture (canonicalEnvs, verifyReuseProviders, verifySupportSources, verifyRelocatedSupportSources, 22 verifySupportExports, verifySupportDependencies, readFixtureSourceBytes, readTestSource, readPinnedSource) 23 import ContentStore 24 import ContentStore.Bundle 25 import Module.Manifest 26 import IODriver (IOPermissions(..), checkIOSentinel, runIO, runIOWithEnv, runIOWith, unsafePerms, defaultPerms) 27 28 import Control.Exception (bracket, evaluate, try, SomeException) 29 import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar) 30 import System.IO.Unsafe (unsafePerformIO) 31 import System.Mem.StableName (makeStableName) 32 import Data.IORef (IORef, newIORef, modifyIORef', readIORef, atomicModifyIORef') 33 import Control.Monad (foldM, forM, forM_, unless) 34 import Control.Monad.IO.Class (liftIO) 35 import System.IO.Temp (withSystemTempDirectory) 36 import qualified System.Timeout as Timeout 37 import System.Directory (createDirectory, createFileLink, doesFileExist, doesDirectoryExist, listDirectory) 38 import System.FilePath ((</>)) 39 import Data.Bits (xor) 40 import Data.List (find, isInfixOf, isPrefixOf, stripPrefix) 41 import Data.Text (Text, unpack, pack) 42 import Data.Text.Encoding (decodeUtf8, encodeUtf8) 43 import Data.Word (Word8) 44 import Test.Tasty 45 import Test.Tasty.HUnit 46 47 import Data.ByteString (ByteString) 48 import qualified Data.Foldable as Foldable 49 import qualified Data.ByteString as BS 50 import qualified Data.Map as Map 51 import qualified Data.Sequence as Seq 52 import qualified Data.Set as Set 53 import qualified Data.Vector as V 54 55 main :: IO () 56 main = do 57 putStrLn "Reference-artifact tests: authenticated compiler/support snapshots, not working-tree candidate coverage." 58 defaultMain tests 59 60 tests :: TestTree 61 tests = testGroup "Tricu Tests" 62 [ workspaceTests 63 , ioBoundaryTests 64 , portableCompilerTests 65 , executableLoadingTests 66 , runtimePresentationTests 67 , interactionInspectionTests 68 , packageTests 69 , evaluationDemandTests 70 , arithmetic 71 , providedLibraries 72 , contractsTests 73 , maybeTests 74 , fileEval 75 , demos 76 , decoding 77 , byteMarshallingTests 78 , sourceObjectCodecTests 79 , sharedTreeDecodingTests 80 , verifiedTreeLoadingTests 81 , indexedTreeTransportTests 82 , verifiedTreePublicationTests 83 , wireTests 84 , tricuReaderTests 85 , LibraryValueTests.byteListUtilities (fixtureExports "bytes") (fixtureExports "base") 86 , ParserValueTests.binaryParserTests (fixtureExports "binary") (fixtureExports "parser") (fixtureExports "base") 87 , ParserValueTests.portableParserTests (fixtureExports "parser") (fixtureExports "base") 88 , locatedLexerTests 89 , compilerFixtureTests 90 , digestHexTests 91 , gateLexerTests 92 , gateParserTests 93 , gateProgramTests 94 , equationHelperTests 95 , sourceGraphTests 96 , gateOrderingTests 97 , gateUnitTests 98 , gateResolutionTests 99 , gateCompilationTests 100 , LibraryValueTests.httpParsingTests (fixtureExports "http") 101 , httpPipelineTests 102 , registryContractTests 103 , registryPipelineTests 104 , localControlTests 105 , contentStoreTests 106 , artifactSafetyTests 107 , ioDriverTests 108 ] 109 110 111 -- Constructor display is a runtime presentation format, not a frontend AST. 112 runtimePresentationTests :: TestTree 113 runtimePresentationTests = testGroup "Runtime presentation" 114 [ testCase "AST leaf constructor golden" $ formatT AST Leaf @?= "TLeaf" 115 , testCase "AST stem constructor golden" $ formatT AST (Stem Leaf) @?= "TStem TLeaf" 116 , testCase "AST nested constructor golden" $ 117 formatT AST (Fork (Stem Leaf) (Fork Leaf Leaf)) @?= 118 "TFork (TStem TLeaf) (TFork TLeaf TLeaf)" 119 ] 120 121 -- Demand uses exceptions rather than unbounded divergent reductions. 122 -- Physical sharing checks the Haskell implementation, not a tree ABI promise. 123 evaluationDemandTests :: TestTree 124 evaluationDemandTests = localOption (mkTimeout 30000000) $ testGroup "Evaluation demand boundaries" 125 [ testCase "constant application discards its argument" $ 126 apply (Fork Leaf (ofNumber 23)) poison @?= ofNumber 23 127 , testCase "constructor application exposes only the outer constructor" $ do 128 case apply Leaf poison of 129 Stem _ -> pure () 130 _ -> assertFailure "expected Stem" 131 case apply (Stem poison) poison of 132 Fork _ _ -> pure () 133 _ -> assertFailure "expected Fork" 134 , testCase "triage demands shape but not unused handlers or children" $ do 135 let value = ofNumber 23 136 constant = Fork Leaf value 137 apply (Fork (Fork value poison) poison) Leaf @?= value 138 apply (Fork (Fork poison constant) poison) (Stem poison) @?= value 139 apply (Fork (Fork poison poison) (Fork Leaf constant)) 140 (Fork poison poison) @?= value 141 demanded (evaluate (apply (Fork (Fork Leaf Leaf) Leaf) poison)) 142 , testCase "distribution retains the duplicated argument's physical sharing" $ do 143 -- t (t t) t c -> t c (t c). Both occurrences reference c. 144 input <- newIORef (Fork (ofNumber 23) (ofNumber 24)) >>= readIORef 145 case apply (Fork (Stem Leaf) Leaf) input of 146 Fork left (Stem right) -> do 147 a <- evaluate left >>= makeStableName 148 b <- evaluate right >>= makeStableName 149 assertBool "distribution copied the argument" (a == b) 150 _ -> assertFailure "unexpected distribution result" 151 , testCase "host list decoding checks the whole spine but not elements" $ do 152 fmap length (toList (Fork poison Leaf)) @?= Right 1 153 demanded (evaluate (toList (Fork Leaf poison))) 154 , testCase "complete flat serialization demands hidden children" $ 155 demanded (evaluate (BS.length (encodeTreeTerm (Fork Leaf poison)))) 156 , testCase "complete indexed serialization demands hidden children" $ 157 demanded (buildBundleShared [("root", Fork Leaf poison)]) 158 , testCase "full observation visits a shared depth-80 DAG within a bounded interval" $ do 159 let tower 0 = Leaf 160 tower n = let child = tower (n - 1) in Fork child child 161 outcome <- Timeout.timeout 2000000 (forceTreeShared (tower (80 :: Int))) 162 outcome @?= Just () 163 , testCase "full observation demands hidden children and does not retain failures" $ do 164 demanded (forceTreeShared (Fork Leaf poison)) 165 forceTreeShared (Fork Leaf (Stem Leaf)) 166 , testCase "full observation handles deep trees without expanding a recursive call stack" $ do 167 let chain = foldr (\_ child -> Stem child) Leaf [1..65536 :: Int] 168 outcome <- Timeout.timeout 2000000 (forceTreeShared chain) 169 outcome @?= Just () 170 , testCase "stable local bindings and ordinary applications discard unused inputs" $ do 171 forM_ ["main input = (ignored : 23) input", 172 "main input = let unused = input in 23"] $ \source -> do 173 function <- compileLocalControl [] source 174 apply function poison @?= ofNumber 23 175 , testCase "stable function consumes a finite prefix of an infinite producer" $ do 176 function <- compileLocalControl [] $ unlines 177 [ "main input = (let rec ones x = t x (ones x) in case (ones input) of" 178 , " [head | _] = head" 179 , " _ = 0)" ] 180 apply function (ofNumber 23) @?= ofNumber 23 181 ] 182 where 183 poison = error "evaluation-demand sentinel" :: T 184 demanded :: IO a -> Assertion 185 demanded action = do 186 outcome <- try (action >> pure ()) :: IO (Either SomeException ()) 187 case outcome of 188 Left err -> assertBool (show err) ("evaluation-demand sentinel" `isInfixOf` show err) 189 Right () -> assertFailure "expected the observer to demand the poisoned subtree" 190 191 -- Host-only negative controls: no source compiler or retained toolchain needed. 192 artifactSafetyTests :: TestTree 193 artifactSafetyTests = testGroup "artifact safety" $ 194 [ testGroup "alias components" 195 [ testCase (show name) $ withSystemTempDirectory "tricu-alias-safety" $ \dir -> do 196 let store = StorePath (dir </> "store") 197 ref = treeTermRef (hashObject treeTermDomain (encodeTreeTerm Leaf)) 198 forM_ [NameAlias, ModuleAlias, PackageAlias] $ \kind -> do 199 blocked "invalid alias name" (writeAlias store kind name ref) 200 blocked "invalid alias name" (readAlias store kind name) 201 doesDirectoryExist (unStorePath store) >>= (@?= False) 202 | name <- ["", ".", "..", "../escape", "a/../../escape", "/absolute", 203 "a\\b", "C:escape", "C:\\escape", "a\NULb", "a\nb", "a\rb"] 204 ] 205 , testCase "flat import counts exponential DAGs without expansion" $ do 206 let base = buildBundle [("root", Leaf)] 207 graph = base { bundleNodes = Seq.fromList (BNLeaf : [BNFork (i-1) (i-1) | i <- [1..10000]]) 208 , bundleRoots = [10000] 209 , bundleManifest = (bundleManifest base) { manifestRoots = [BundleRoot 10000 "default"], 210 manifestExports = [BundleExport "root" 10000 "term" "arboricx.abi.tree.v1"] } } 211 case checkFlatImportSize defaultFlatImportLimit graph of 212 Left err -> assertBool err ("flat import byte limit exceeded" `isInfixOf` err) 213 Right () -> assertFailure "expanded size was not bounded" 214 , testCase "flat byte accounting matches exact small serializations" $ 215 forM_ [Leaf, Stem Leaf, Fork (Stem Leaf) (Stem Leaf)] $ \tree -> do 216 let bundle = buildBundle [("a", tree), ("b", tree)] 217 size = 2 * fromIntegral (BS.length (encodeTreeTerm tree)) 218 checkFlatImportSize size bundle @?= Right () 219 assertBool "one byte under budget must fail" (either (const True) (const False) (checkFlatImportSize (size-1) bundle)) 220 , testCase "duplicate export names rejected by bundle and stored manifest readers" $ do 221 let bundle = buildBundle [("same", Leaf), ("same", Stem Leaf)] 222 manifest = ModuleManifest [] [ModuleExport "same" (treeTermRef (nodeHash NLeaf)) "arboricx.abi.tree.v1", 223 ModuleExport "same" (treeTermRef (nodeHash NLeaf)) "arboricx.abi.tree.v1"] 224 verifyBundle bundle @?= Left "duplicate export name" 225 decodeManifest (encodeManifest manifest) @?= Left "duplicate export name" 226 , testCase "qualified and symbolic aliases remain usable" $ 227 withSystemTempDirectory "tricu-alias-valid" $ \dir -> do 228 let store = StorePath dir 229 ref = treeTermRef (hashObject treeTermDomain (encodeTreeTerm Leaf)) 230 forM_ ["native_math.isNat?", "++", "hello world", "λ", ".hidden", "a..b"] $ \name -> do 231 writeAlias store NameAlias name ref 232 readAlias store NameAlias name >>= (@?= Just ref) 233 , testCase "alias absolute paths cannot read or overwrite an outside sentinel" $ 234 withSystemTempDirectory "tricu-alias-sentinel" $ \dir -> do 235 let store = StorePath (dir </> "store") 236 outside = dir </> "sentinel" 237 name = pack outside 238 ref = treeTermRef (hashObject treeTermDomain (encodeTreeTerm Leaf)) 239 BS.writeFile outside "unchanged" 240 blocked "invalid alias name" (writeAlias store NameAlias name ref) 241 blocked "invalid alias name" (readAlias store NameAlias name) 242 BS.readFile outside >>= (@?= "unchanged") 243 , testGroup "object path hashes" 244 [ testCase (show h) $ withSystemTempDirectory "tricu-hash-safety" $ \dir -> 245 do 246 blocked "invalid object hash" (getObject (StorePath dir) h) 247 blocked "invalid object hash" (evaluate (objectPath (StorePath dir) h)) 248 | h <- ["", "../escape", "../../escape", "/absolute", "abc", pack (replicate 64 'A'), 249 pack (replicate 63 'a'), pack (replicate 65 'a'), pack (replicate 64 'g')] 250 ] 251 , testCase "object paths cannot read an absolute outside sentinel" $ 252 withSystemTempDirectory "tricu-hash-sentinel" $ \dir -> do 253 let outside = dir </> "sentinel" 254 BS.writeFile outside "not a CAS object" 255 blocked "invalid object hash" (getObject (StorePath (dir </> "store")) (pack outside)) 256 , testCase "cached authenticated objects are immutable snapshots" $ fresh $ \store -> do 257 h <- putTreeTerm store Leaf 258 resolver <- cachedFilesystemResolver store 259 resolverObject resolver (treeTermRef h) >>= (@?= Just (encodeTreeTerm Leaf)) 260 BS.writeFile (objectPath store h) (encodeTreeTerm (Stem Leaf)) 261 resolverObject resolver (treeTermRef h) >>= (@?= Just (encodeTreeTerm Leaf)) 262 blocked "digest mismatch" (resolverObject (filesystemResolver store) (treeTermRef h)) 263 , testCase "object resolution verifies bytes from custom resolver callbacks" $ fresh $ \store -> do 264 h <- putTreeTerm store Leaf 265 mh <- putManifest store (ModuleManifest [] [ModuleExport "x" (treeTermRef h) "arboricx.abi.tree.v1"]) 266 let resolver = (filesystemResolver store) { resolverObject = \_ -> pure (Just (encodeTreeTerm (Stem Leaf))) } 267 blocked "digest mismatch" (resolveObjectByHash resolver (unDomain treeTermDomain) h) 268 , testGroup "execution contract" 269 [ testCase label $ do 270 let base = buildBundle [("first", Leaf), ("second", Stem Leaf)] 271 bundle = base { bundleManifest = change (bundleManifest base), bundleManifestBytes = BS.empty } 272 case verifyBundle bundle of 273 Left err -> assertBool err (needle `isInfixOf` err) 274 Right () -> assertFailure "unsupported execution contract accepted" 275 | (label, needle, change) <- 276 [("evaluation", "runtime evaluation", \m -> m { manifestRuntime = (manifestRuntime m) { runtimeEvaluation = "call-by-value" } }), 277 ("export kind", "export kind", \m -> m { manifestExports = map (\e -> e { exportKind = "foreign" }) (manifestExports m) }), 278 ("second export ABI", "export ABI", \m -> m { manifestExports = case manifestExports m of 279 [a,b] -> [a,b { exportAbi = "foreign.abi" }]; es -> es })] 280 ] 281 ] ++ 282 [ testGroup label 283 [ testCase "tampered manifest" $ fresh $ \store -> do 284 h <- putManifest store (ModuleManifest [] []) 285 BS.writeFile (objectPath store h) (encodeManifest (ModuleManifest [] [ModuleExport "x" (treeTermRef h) "arboricx.abi.tree.v1"])) 286 resolver <- makeResolver store 287 blocked "digest mismatch" (resolveManifest resolver h) 288 blocked "digest mismatch" (getManifest store h) 289 , testCase "tampered exported term" $ fresh $ \store -> do 290 h <- putTreeTerm store Leaf 291 mh <- putManifest store (ModuleManifest [] [ModuleExport "x" (treeTermRef h) "arboricx.abi.tree.v1"]) 292 BS.writeFile (objectPath store h) (encodeTreeTerm (Stem Leaf)) 293 resolver <- makeResolver store 294 blocked "digest mismatch" (resolveObjectByHash resolver (unDomain treeTermDomain) h) 295 blocked "digest mismatch" (getTreeTerm store h) 296 , testCase "tampered Merkle child" $ fresh $ \store -> do 297 h <- putTree store (Stem Leaf) 298 BS.writeFile (objectPath store (nodeHash NLeaf)) (serializeNode (NStem (pack (replicate 64 '0')))) 299 resolver <- makeResolver store 300 blocked "digest mismatch" (resolveTree resolver h) 301 blocked "digest mismatch" (getTree store h) 302 , testCase "tampered self-cycle rejected before hydration" $ fresh $ \store -> do 303 h <- putTree store Leaf 304 BS.writeFile (objectPath store h) (serializeNode (NStem h)) 305 resolver <- makeResolver store 306 completed <- Timeout.timeout 1000000 (blocked "digest mismatch" (resolveTree resolver h)) 307 completed @?= Just () 308 , testCase "failed verification never poisons the cache" $ fresh $ \store -> do 309 h <- putTreeTerm store Leaf 310 resolver <- makeResolver store 311 BS.writeFile (objectPath store h) (encodeTreeTerm (Stem Leaf)) 312 blocked "digest mismatch" (resolverObject resolver (treeTermRef h)) 313 BS.writeFile (objectPath store h) (encodeTreeTerm Leaf) 314 resolverObject resolver (treeTermRef h) >>= (@?= Just (encodeTreeTerm Leaf)) 315 , testCase "wrong object domain" $ fresh $ \store -> do 316 h <- putTreeTerm store Leaf 317 resolver <- makeResolver store 318 blocked "digest mismatch" (resolverObject resolver (ObjectRef (unDomain manifestDomain) h)) 319 -- Export ABI rejection is exercised against the production executable in 320 -- executable_arguments.py, not the retired source-module resolver. 321 , testCase "valid objects and genuine missing objects" $ fresh $ \store -> do 322 h <- putTreeTerm store (Stem Leaf) 323 mh <- putManifest store (ModuleManifest [] [ModuleExport "x" (treeTermRef h) "arboricx.abi.tree.v1"]) 324 nh <- putTree store (Stem Leaf) 325 resolver <- makeResolver store 326 resolveManifest resolver mh >>= (@?= Just (ModuleManifest [] [ModuleExport "x" (treeTermRef h) "arboricx.abi.tree.v1"])) 327 resolveTree resolver nh >>= (@?= Just (Stem Leaf)) 328 resolveObjectByHash resolver (unDomain treeTermDomain) h >>= (@?= Just (encodeTreeTerm (Stem Leaf))) 329 loadExecutableObject store (treeTermRef h) >>= (@?= Stem Leaf) 330 resolverObject resolver (treeTermRef (pack (replicate 64 '0'))) >>= (@?= Nothing) 331 ] 332 | (label, makeResolver) <- [("filesystem integrity", pure . filesystemResolver), ("cached integrity", cachedFilesystemResolver)] 333 ] 334 where 335 treeTermRef = ObjectRef (unDomain treeTermDomain) 336 fresh f = withSystemTempDirectory "tricu-integrity" (f . StorePath) 337 blocked needle action = do 338 outcome <- try (action >> pure ()) :: IO (Either SomeException ()) 339 case outcome of 340 Left err -> assertBool (show err) (needle `isInfixOf` show err) 341 Right () -> assertFailure ("expected rejection: " ++ needle) 342 343 sharedTreeDecodingTests :: TestTree 344 sharedTreeDecodingTests = testGroup "shared inert tree decoding" 345 [ testCase "small trees preserve exact structure and canonical bytes" $ 346 forM_ (trees 3) $ \tree -> do 347 let bytes = encodeTreeTerm tree 348 decodeTreeTermShared bytes @?= Right tree 349 fmap encodeTreeTerm (decodeTreeTermShared bytes) @?= Right bytes 350 , testCase "integer interning distinguishes constructors, pair order and wide IDs" $ do 351 let keys = [Nodes.LeafKey] ++ map Nodes.StemKey values ++ 352 [Nodes.ForkKey a b | a <- values, b <- values] 353 values = [minBound, -1, 0, 1, 65535, maxBound] ++ 354 map fromInteger [2147483647, 2147483648, 4294967295, 4294967296] 355 entries = zip keys [(0 :: Int)..] 356 table = foldl (\acc (key, value) -> Nodes.insert key value acc) Nodes.empty entries 357 expected = Map.fromList entries 358 forM_ (Map.toList expected) $ \(key, value) -> 359 Nodes.lookup key table @?= Just value 360 Nodes.lookup (Nodes.ForkKey 17 19) table @?= Nothing 361 Nodes.lookup (Nodes.StemKey 17) table @?= Nothing 362 let updated = Nodes.insert (Nodes.ForkKey 0 1) 999 table 363 Nodes.lookup (Nodes.ForkKey 0 1) updated @?= Just 999 364 Nodes.lookup (Nodes.ForkKey 1 0) updated @?= Map.lookup (Nodes.ForkKey 1 0) expected 365 , testCase "deep shared decoding preserves bytes without recursive reference decoding" $ do 366 let bytes = BS.replicate 65536 1 <> BS.singleton 0 367 decoded <- either assertFailure pure (decodeTreeTermShared bytes) 368 encodeTreeTerm decoded @?= bytes 369 , testCase "malformed bytes retain ordinary decoder errors" $ 370 forM_ (concat [sequence (replicate n [0, 1, 2, 255]) | n <- [0..4]]) $ \tags -> 371 decodeTreeTermShared (BS.pack tags) @?= decodeTreeTerm (BS.pack tags) 372 , testCase "repeated branches regain physical sharing without application" $ do 373 let tower 0 = Fork (Stem Leaf) Leaf 374 tower n = let child = tower (n - 1) in Fork child child 375 value = tower (14 :: Int) 376 decoded <- either assertFailure pure (decodeTreeTermShared (encodeTreeTerm value)) 377 let check (Fork left right) = do 378 a <- evaluate left >>= makeStableName 379 b <- evaluate right >>= makeStableName 380 assertBool "equal branches were separately allocated" (a == b) 381 check _ = assertFailure "missing fork" 382 check decoded 383 case decoded of 384 Fork left _ -> check left 385 _ -> assertFailure "missing root" 386 ] 387 where 388 trees 0 = [Leaf] 389 trees n = let children = trees (n - 1) 390 in Leaf : map Stem children ++ [Fork a b | a <- children, b <- children] 391 392 -- Indexed IO transports inert normal forms. The tiny exponential logical tree 393 -- is deliberately never unfolded for equality/encoding by these controls. 394 indexedTreeTransportTests :: TestTree 395 indexedTreeTransportTests = testGroup "indexed tree transport" 396 [ testCase "canonical bytes agree with independent ordinary writer" $ 397 forM_ (trees 2) $ \tree -> do 398 encoded <- encodeIndexedTree tree 399 encoded @?= encodeBundle (buildBundle [("root", tree)]) 400 decodeIndexedTree encoded @?= Right tree 401 , testCase "shared depth-80 tree encodes within a bounded interval" $ do 402 let tower 0 = Leaf 403 tower n = let child = tower (n - 1) in Fork child child 404 measured <- Timeout.timeout 2000000 $ do 405 payload <- encodeIndexedTree (tower (80 :: Int)) 406 size <- evaluate (BS.length payload) 407 assertBool "DAG unexpectedly expanded" (size < 2048) 408 bundle <- either assertFailure pure (decodeBundle payload) 409 Seq.length (bundleNodes bundle) @?= 81 410 value <- either assertFailure pure (decodeIndexedTree payload) 411 case value of 412 Fork left right -> do 413 a <- evaluate left >>= makeStableName 414 b <- evaluate right >>= makeStableName 415 assertBool "decoded branches lost sharing" (a == b) 416 _ -> assertFailure "missing fork" 417 measured @?= Just () 418 , testCase "shared writer is name-neutral and byte-identical to indexed import" $ 419 withSystemTempDirectory "tricu-indexed-import" $ \dir -> do 420 let tree = Fork (Stem Leaf) (Stem Leaf) 421 encoded <- encodeIndexedTree tree 422 [(name, pin)] <- unpackIndexedBundleToStore (StorePath dir) 423 (encodeBundle (buildBundle [("publicName", tree)])) 424 name @?= "publicName" 425 pin @?= hashObject indexedTreeDomain encoded 426 , testCase "hash, no-replace publication, repeat and read exact inert tree" $ 427 withSystemTempDirectory "tricu-indexed-io" $ \dir -> do 428 let tree = Fork (Stem (Fork Leaf Leaf)) (Fork Leaf (Stem Leaf)) 429 path = dir </> "object" 430 payload <- encodeIndexedTree tree 431 let pin = hashObject indexedTreeDomain payload 432 action unsafePerms 51 tree >>= (@?= Right (ioOkResult (ofString (unpack pin)))) 433 writeIndexed unsafePerms path pin tree >>= (@?= Right (ioOkResult (Stem Leaf))) 434 BS.readFile path >>= (@?= payload) 435 writeIndexed unsafePerms path pin tree >>= (@?= Right (ioOkResult Leaf)) 436 readIndexed unsafePerms path pin >>= (@?= Right (ioOkResult tree)) 437 , testCase "read checks permission, canonical digest, and domain" $ 438 withSystemTempDirectory "tricu-indexed-denied" $ \dir -> do 439 let path = dir </> "object" 440 payload <- encodeIndexedTree Leaf 441 BS.writeFile path payload 442 let pin = hashObject indexedTreeDomain payload 443 readIndexed defaultPerms path pin >>= (@?= Right (ioErrResult "permission denied")) 444 readIndexed unsafePerms path "BAD" >>= (@?= Right (ioErrResult "invalid CAS digest")) 445 readIndexed unsafePerms path (hashObject treeTermDomain payload) 446 >>= (@?= Right (ioErrResult "CAS digest mismatch")) 447 , testCase "verification precedes malformed indexed payload decoding" $ 448 withSystemTempDirectory "tricu-indexed-bad" $ \dir -> do 449 let path = dir </> "object" 450 payload = "bad bundle" 451 BS.writeFile path payload 452 readIndexed unsafePerms path (pack (replicate 64 '0')) 453 >>= (@?= Right (ioErrResult "CAS digest mismatch")) 454 readIndexed unsafePerms path (hashObject indexedTreeDomain payload) 455 >>= (@?= Right (ioErrResult "invalid indexed tree")) 456 let named = encodeBundle (buildBundle [("other", Leaf)]) 457 BS.writeFile path named 458 readIndexed unsafePerms path (hashObject indexedTreeDomain named) 459 >>= (@?= Right (ioErrResult "invalid indexed tree")) 460 , testCase "publication needs both permissions and never overwrites corruption" $ 461 withSystemTempDirectory "tricu-indexed-publish" $ \dir -> do 462 let path = dir </> "object" 463 payload <- encodeIndexedTree Leaf 464 let pin = hashObject indexedTreeDomain payload 465 forM_ [defaultPerms { allowRead = [dir] }, defaultPerms { allowWrite = [dir] }] $ \perms -> 466 writeIndexed perms path pin Leaf >>= (@?= Right (ioErrResult "permission denied")) 467 doesFileExist path >>= (@?= False) 468 writeIndexed unsafePerms path (pack (replicate 64 '0')) Leaf 469 >>= (@?= Right (ioErrResult "CAS digest mismatch")) 470 doesFileExist path >>= (@?= False) 471 BS.writeFile path "existing corruption" 472 writeIndexed unsafePerms path pin Leaf 473 >>= (@?= Right (ioErrResult "CAS object exists with different bytes")) 474 BS.readFile path >>= (@?= "existing corruption") 475 ] 476 where 477 trees 0 = [Leaf] 478 trees n = let xs = trees (n - 1) in Leaf : map Stem xs ++ [Fork a b | a <- xs, b <- xs] 479 action perms tag payload = runIO perms $ 480 Fork (ofString "tricuIO") (Fork (ofNumber 1) (Fork (ofNumber tag) payload)) 481 readIndexed perms path pin = action perms 50 (Fork (ofString path) (ofString (unpack pin))) 482 writeIndexed perms path pin tree = action perms 52 483 (Fork (ofString path) (Fork (ofString (unpack pin)) tree)) 484 485 -- Small host/unit codec gate, not compiler-closure compilation. Raw trees 486 -- must be inspected/encoded without invoking them as functions. 487 verifiedTreeLoadingTests :: TestTree 488 verifiedTreeLoadingTests = testGroup "verified tree loading" 489 [ testCase "exact inert tree, including function-shaped forks" $ 490 withSystemTempDirectory "tricu-tree-load" $ \dir -> do 491 let value = Fork (Stem (Fork Leaf Leaf)) (Fork Leaf (Stem Leaf)) 492 payload = encodeTreeTerm value 493 path = dir </> "object" 494 BS.writeFile path payload 495 readTerm (defaultPerms { allowRead = [dir] }) path 496 (unpack (hashObject treeTermDomain payload)) >>= (@?= Right (ioOkResult value)) 497 , testCase "read denied even with write permission" $ 498 withSystemTempDirectory "tricu-tree-denied" $ \dir -> do 499 let payload = BS.singleton 0 500 path = dir </> "object" 501 BS.writeFile path payload 502 readTerm (defaultPerms { allowWrite = [dir] }) path 503 (unpack (hashObject treeTermDomain payload)) >>= (@?= Right (ioErrResult "permission denied")) 504 , testCase "symlink does not bypass read permission" $ 505 withSystemTempDirectory "tricu-tree-link" $ \dir -> do 506 let allowed = dir </> "allowed" 507 denied = dir </> "outside" 508 link = allowed </> "link" 509 payload = BS.singleton 0 510 createDirectory allowed 511 BS.writeFile denied payload 512 createFileLink denied link 513 readTerm (defaultPerms { allowRead = [allowed] }) link 514 (unpack (hashObject treeTermDomain payload)) >>= (@?= Right (ioErrResult "permission denied")) 515 , testCase "missing file" $ 516 withSystemTempDirectory "tricu-tree-missing" $ \dir -> 517 readTerm (defaultPerms { allowRead = [dir] }) (dir </> "missing") 518 (replicate 64 '0') >>= (@?= Right (ioErrResult "does not exist")) 519 , testCase "mismatched digest precedes decoding" $ 520 withSystemTempDirectory "tricu-tree-mismatch" $ \dir -> do 521 let path = dir </> "object" 522 BS.writeFile path (BS.singleton 255) 523 readTerm unsafePerms path (replicate 64 '0') >>= (@?= Right (ioErrResult "CAS digest mismatch")) 524 , testCase "wrong domain rejected" $ 525 withSystemTempDirectory "tricu-tree-domain" $ \dir -> do 526 let payload = BS.singleton 0 527 path = dir </> "object" 528 BS.writeFile path payload 529 readTerm unsafePerms path (unpack (hashObject (Domain "wrong.domain") payload)) 530 >>= (@?= Right (ioErrResult "CAS digest mismatch")) 531 , testCase "digest must be canonical lowercase hexadecimal" $ 532 forM_ ["bad", replicate 64 'G', replicate 64 'A', replicate 63 '0'] $ \digest -> 533 readTerm unsafePerms "/definitely-missing" digest >>= (@?= Right (ioErrResult "invalid CAS digest")) 534 , testCase "malformed canonical payloads rejected after valid digest" $ 535 withSystemTempDirectory "tricu-tree-malformed" $ \dir -> 536 forM_ [BS.empty, BS.pack [1], BS.pack [2,0], BS.pack [3], BS.pack [0,0]] $ \payload -> do 537 let path = dir </> "object" 538 BS.writeFile path payload 539 readTerm unsafePerms path (unpack (hashObject treeTermDomain payload)) 540 >>= (@?= Right (ioErrResult "invalid canonical tree-term")) 541 , testCase "invalid argument strings" $ do 542 runAction unsafePerms (Stem Leaf) (ofString (replicate 64 '0')) 543 >>= (@?= Right (ioErrResult "invalid string")) 544 , testCase "readTreeTerm library constructor" $ 545 withSystemTempDirectory "tricu-tree-constructor" $ \dir -> do 546 let value = Stem Leaf 547 payload = encodeTreeTerm value 548 path = dir </> "object" 549 BS.writeFile path payload 550 let io = fixtureExports "io" 551 action = apply (io Map.! "io") (foldl apply (io Map.! "readTreeTerm") 552 [ofString path, ofString (unpack (hashObject treeTermDomain payload))]) 553 runIO (defaultPerms { allowRead = [dir] }) action >>= (@?= Right (ioOkResult value)) 554 ] 555 where 556 readTerm perms path digest = runAction perms (ofString path) (ofString digest) 557 runAction perms path digest = runIO perms $ 558 Fork (ofString "tricuIO") (Fork (ofNumber 1) (Fork (ofNumber 34) (Fork path digest))) 559 560 verifiedTreePublicationTests :: TestTree 561 verifiedTreePublicationTests = testGroup "verified tree publication" 562 [ testCase "canonical hashing agrees with independent bytes" $ do 563 let value = Fork (Stem Leaf) (Fork Leaf (Stem Leaf)) 564 golden = BS.pack [2,1,0,2,0,1,0] 565 encodeTreeTerm value @?= golden 566 hashTerm value >>= (@?= Right (ioOkResult (ofString (unpack (hashObject treeTermDomain golden))))) 567 , testCase "new publication has exact bytes and no temporary residue" $ 568 withSystemTempDirectory "tricu-tree-publish" $ \dir -> do 569 let path = dir </> "object" 570 tree = Fork (Stem Leaf) Leaf 571 payload = BS.pack [2,1,0,0] 572 publish (permissions dir) path (digest payload) tree >>= (@?= Right (ioOkResult (Stem Leaf))) 573 BS.readFile path >>= (@?= payload) 574 listDirectory dir >>= (@?= ["object"]) 575 , testCase "existing identical object returns false" $ 576 withSystemTempDirectory "tricu-tree-existing" $ \dir -> do 577 let path = dir </> "object" 578 tree = Stem Leaf 579 payload = BS.pack [1,0] 580 BS.writeFile path payload 581 publish (permissions dir) path (digest payload) tree >>= (@?= Right (ioOkResult Leaf)) 582 BS.readFile path >>= (@?= payload) 583 , testCase "corrupt winner is not replaced" $ 584 withSystemTempDirectory "tricu-tree-corrupt" $ \dir -> do 585 let path = dir </> "object" 586 old = BS.pack [255] 587 BS.writeFile path old 588 publish (permissions dir) path (digest (BS.singleton 0)) Leaf 589 >>= (@?= Right (ioErrResult "CAS object exists with different bytes")) 590 BS.readFile path >>= (@?= old) 591 listDirectory dir >>= (@?= ["object"]) 592 , testCase "mismatch fails before publication" $ 593 withSystemTempDirectory "tricu-tree-wrong" $ \dir -> do 594 publish (permissions dir) (dir </> "object") (replicate 64 '0') Leaf 595 >>= (@?= Right (ioErrResult "CAS digest mismatch")) 596 listDirectory dir >>= (@?= []) 597 , testCase "write denied without forcing tree" $ 598 withSystemTempDirectory "tricu-tree-write-denied" $ \dir -> do 599 publish (defaultPerms { allowRead = [dir] }) (dir </> "object") 600 (digest (BS.singleton 0)) (error "denied write must not force tree") 601 >>= (@?= Right (ioErrResult "permission denied")) 602 listDirectory dir >>= (@?= []) 603 , testCase "read denied without forcing tree" $ 604 withSystemTempDirectory "tricu-tree-read-denied" $ \dir -> do 605 publish (defaultPerms { allowWrite = [dir] }) (dir </> "object") 606 (digest (BS.singleton 0)) (error "denied read must not force tree") 607 >>= (@?= Right (ioErrResult "permission denied")) 608 listDirectory dir >>= (@?= []) 609 , testCase "symlink does not bypass write permission" $ 610 withSystemTempDirectory "tricu-tree-write-link" $ \dir -> do 611 let allowed = dir </> "allowed" 612 outside = dir </> "outside" 613 link = allowed </> "link" 614 payload = BS.singleton 0 615 createDirectory allowed 616 BS.writeFile outside payload 617 createFileLink outside link 618 publish (permissions allowed) link (digest payload) Leaf 619 >>= (@?= Right (ioErrResult "permission denied")) 620 BS.readFile outside >>= (@?= payload) 621 , testCase "invalid digest fails without filesystem effects" $ 622 withSystemTempDirectory "tricu-tree-bad-digest" $ \dir -> do 623 forM_ ["bad", replicate 64 'G', replicate 64 'A'] $ \bad -> 624 publish (permissions dir) (dir </> "object") bad Leaf 625 >>= (@?= Right (ioErrResult "invalid CAS digest")) 626 listDirectory dir >>= (@?= []) 627 , testCase "parent creation remains explicit" $ 628 withSystemTempDirectory "tricu-tree-parent" $ \dir -> do 629 publish (permissions dir) (dir </> "missing" </> "object") (digest (BS.singleton 0)) Leaf 630 >>= (@?= Right (ioErrResult "does not exist")) 631 listDirectory dir >>= (@?= []) 632 , testCase "concurrent publishers have one winner" $ 633 withSystemTempDirectory "tricu-tree-race" $ \dir -> do 634 let tree = iterate (\t -> Fork t t) Leaf !! 12 635 bytes = encodeTreeTerm tree 636 path = dir </> "object" 637 action = publish (permissions dir) path (digest bytes) tree 638 a <- newEmptyMVar 639 b <- newEmptyMVar 640 _ <- forkIO (action >>= putMVar a) 641 _ <- forkIO (action >>= putMVar b) 642 results <- sequence [takeMVar a, takeMVar b] 643 length (filter (== Right (ioOkResult (Stem Leaf))) results) @?= 1 644 length (filter (== Right (ioOkResult Leaf)) results) @?= 1 645 BS.readFile path >>= (@?= bytes) 646 listDirectory dir >>= (@?= ["object"]) 647 , testCase "sourceObjectPutTree creates canonical shards and reuses objects" $ 648 withSystemTempDirectory "tricu-tree-library" $ \dir -> do 649 env <- evaluateCompilerFile "./lib/source/object.tri" 650 let root = dir </> "cas" 651 tree = Fork Leaf (Stem Leaf) 652 payload = encodeBundle (buildBundle [("root", tree)]) 653 expected = unpack (hashObject indexedTreeDomain payload) 654 action = sentinel (apply (apply (env Map.! "sourceObjectPutTree") (ofString root)) tree) 655 path = root </> "objects" </> take 3 expected </> expected 656 runIO (permissions dir) action >>= (@?= Right (ioOkResult (ofString expected))) 657 runIO (permissions dir) action >>= (@?= Right (ioOkResult (ofString expected))) 658 BS.readFile path >>= (@?= payload) 659 ] 660 where 661 digest = unpack . hashObject treeTermDomain 662 permissions dir = defaultPerms { allowRead = [dir], allowWrite = [dir] } 663 sentinel action = Fork (ofString "tricuIO") (Fork (ofNumber 1) action) 664 hashTerm tree = runIO defaultPerms (sentinel (Fork (ofNumber 35) tree)) 665 publish perms path expected tree = runIO perms $ 666 sentinel (Fork (ofNumber 36) (Fork (ofString path) (Fork (ofString expected) tree))) 667 668 sourceObjectCodecTests :: TestTree 669 sourceObjectCodecTests = testGroup "Portable tree object codec" 670 [ testCase "canonical preorder bytes, suffix law and complete round trip" $ 671 withSystemTempDirectory "source-object-codec" $ \dir -> do 672 env <- evaluateCompilerFile "./lib/source/object.tri" 673 let encoder = env Map.! "sourceObjectEncode" 674 fixed = env Map.! "sourceObjectEncodeOnto" 675 decoder = env Map.! "sourceObjectDecodeComplete" 676 suffix = BS.pack [7, 0, 255] 677 seeds = [Leaf, Stem Leaf, Fork Leaf Leaf] 678 small = seeds ++ [Fork l r | l <- seeds, r <- seeds] 679 leftHeavy = iterate (\tree -> Fork tree Leaf) Leaf !! 64 680 rightHeavy = iterate (Fork Leaf) Leaf !! 64 681 balanced = iterate (\tree -> Fork tree tree) Leaf !! 8 682 forM_ (small ++ [leftHeavy, rightHeavy, balanced]) $ \tree -> do 683 let bytes = encodeTreeTerm tree 684 encoded = apply encoder tree 685 toBytes encoded @?= Right bytes 686 toBytes (apply (apply fixed tree) (ofBytes suffix)) @?= 687 Right (bytes <> suffix) 688 apply decoder encoded @?= Fork (Stem Leaf) (Fork tree Leaf) 689 ] 690 691 fileEval :: TestTree 692 fileEval = testGroup "Portable source file tests" 693 [ testCase "Forks" $ check "./test/fork.tri" (Fork Leaf Leaf) 694 , testCase "File ends with comment" $ check "./test/comments-1.tri" (Fork (Stem Leaf) Leaf) 695 , testCase "Mapping and Equality" $ check "./test/map.tri" (Stem Leaf) 696 , testCase "Eval and decoding string" $ check "./test/string.tri" (ofString "String test!") 697 ] 698 where 699 check path expected = do 700 source <- readFile path 701 let external = ofList [Fork (ofString name) value | (name,value) <- Map.toList (fixtureExports "prelude")] 702 foldl apply (fixtureExports "source.unit" Map.! "compileGateProgram") 703 [external, ofString source] @?= parserOk expected Leaf 704 705 -- All of our demo tests are also module tests 706 demos :: TestTree 707 demos = testGroup "Test provided demo functionality" 708 [ testCase "Constructor-safe lowering matches the authored Boolean not tree" $ do 709 env <- evaluateCompilerFile "./demos/equality.tri" 710 -- Constructor-only K pruning intentionally changes inspectable function 711 -- trees. This specimen now equals its authored TC form; that is not a 712 -- structural-equivalence promise for arbitrary extensional functions. 713 env Map.! "main" @?= Stem Leaf 714 env Map.! "not_TC?" @?= 715 Fork (Fork (Stem Leaf) (Fork Leaf Leaf)) (Fork Leaf (Fork Leaf Leaf)) 716 env Map.! "not_Lambda?" @?= env Map.! "not_TC?" 717 forM_ ["not_TC?", "not_Lambda?"] $ \name -> do 718 apply (env Map.! name) Leaf @?= Stem Leaf 719 apply (env Map.! name) (Stem Leaf) @?= Leaf 720 env Map.! "bothTrueEqual?" @?= Stem Leaf 721 env Map.! "bothFalseEqual?" @?= Stem Leaf 722 , testCase "Convert values back to source code demo" $ do 723 res <- liftIO $ ((Map.! "main") <$> evaluateCompilerFile "./demos/toSource.tri") 724 -- Native portable foundation trees, not the Haskell recovery cohort. 725 decodeResult res @?= "\"(t (t (t t) (t t t)) (t t (t t t)))\"" 726 , testCase "Determining the size of stable-compiled functions" $ do 727 env <- evaluateCompilerFile "./demos/size.tri" 728 -- Count the actual executable independently of the compiled size worker. 729 -- This is a representation check of the retained stable artifact, 730 -- not a parity requirement across compiler backends. 731 let nodes Leaf = 1 :: Integer 732 nodes (Stem child) = 1 + nodes child 733 nodes (Fork left right) = 1 + nodes left + nodes right 734 actual = nodes (env Map.! "size") 735 -- Constructor-K publication: the independent host count changes from 736 -- 1845 to 1233; the compiled worker must still count its actual tree. 737 actual @?= 1233 738 env Map.! "main" @?= ofNumber actual 739 , testCase "Level Order Traversal demo" $ do 740 res <- liftIO $ ((Map.! "main") <$> evaluateCompilerFile "./demos/levelOrderTraversal.tri") 741 decodeResult res @?= "\"\n1 \n2 3 \n4 5 6 7 \n8 11 10 9 12 \"" 742 , testCase "Contract effect demo with do notation" $ do 743 res <- liftIO $ ((Map.! "main") <$> evaluateCompilerFile "./demos/contractEffects.tri") 744 decodeResult res @?= "[t t, 10]" 745 , testCase "Safe base wrappers demo" $ do 746 res <- liftIO $ ((Map.! "main") <$> evaluateCompilerFile "./demos/contractBasics.tri") 747 decodeResult res @?= "[3, t t, t, t t]" 748 ] 749 750 decoding :: TestTree 751 decoding = testGroup "Decoding Tests" 752 [ testCase "Decode Leaf" $ do 753 decodeResult Leaf @?= "t" 754 755 , testCase "Decode list of non-ASCII numbers" $ do 756 let input = ofList [ofNumber 1, ofNumber 14, ofNumber 6] 757 decodeResult input @?= "[1, 14, 6]" 758 759 , testCase "Decode list of ASCII numbers as a string" $ do 760 let input = ofList [ofNumber 97, ofNumber 98, ofNumber 99] 761 decodeResult input @?= "\"abc\"" 762 763 , testCase "Decode small number" $ do 764 decodeResult (ofNumber 42) @?= "42" 765 766 , testCase "Decode large number" $ do 767 decodeResult (ofNumber 9999) @?= "9999" 768 769 , testCase "Decode string in list" $ do 770 let input = ofList [ofString "hello", ofString "world"] 771 decodeResult input @?= "[\"hello\", \"world\"]" 772 773 , testCase "Decode mixed list with strings" $ do 774 let input = ofList [ofString "hello", ofNumber 42, ofString "world"] 775 decodeResult input @?= "[\"hello\", 42, \"world\"]" 776 777 , testCase "Decode nested lists with strings" $ do 778 let input = ofList [ofList [ofString "nested"], ofString "string"] 779 decodeResult input @?= "[[\"nested\"], \"string\"]" 780 ] 781 782 -- -------------------------------------------------------------------------- 783 -- Byte marshalling tests 784 -- -------------------------------------------------------------------------- 785 786 byteMarshallingTests :: TestTree 787 byteMarshallingTests = testGroup "Byte Marshalling Tests" 788 [ testCase "ofByte / toByte round-trip: 0" $ do 789 let w8 = (0 :: Word8) 790 toByte (ofByte w8) @?= Right w8 791 792 , testCase "ofByte / toByte round-trip: 1" $ do 793 let w8 = (1 :: Word8) 794 toByte (ofByte w8) @?= Right w8 795 796 , testCase "ofByte / toByte round-trip: 127" $ do 797 let w8 = (127 :: Word8) 798 toByte (ofByte w8) @?= Right w8 799 800 , testCase "ofByte / toByte round-trip: 128" $ do 801 let w8 = (128 :: Word8) 802 toByte (ofByte w8) @?= Right w8 803 804 , testCase "ofByte / toByte round-trip: 255" $ do 805 let w8 = (255 :: Word8) 806 toByte (ofByte w8) @?= Right w8 807 808 , testCase "toByte rejects value > 255" $ do 809 -- ofNumber 256 = Fork Leaf (Fork Leaf Leaf) — value 256 810 toByte (ofNumber 256) @?= Left "Byte value out of range: 256" 811 812 , testCase "toByte accepts Leaf" $ do 813 toByte (Leaf) @?= Right 0 814 815 , testCase "toByte rejects non-number tree" $ do 816 toByte (Stem Leaf) @?= Left "Invalid Tree Calculus number" 817 toByte (Stem (Stem Leaf)) @?= Left "Invalid Tree Calculus number" 818 819 , testCase "ofBytes / toBytes round-trip: empty ByteString" $ do 820 toBytes (ofBytes BS.empty) @?= Right BS.empty 821 822 , testCase "ofBytes / toBytes round-trip: [0x00]" $ do 823 toBytes (ofBytes (BS.pack [0x00])) @?= Right (BS.pack [0x00]) 824 825 , testCase "ofBytes / toBytes round-trip: [0xff]" $ do 826 toBytes (ofBytes (BS.pack [0xff])) @?= Right (BS.pack [0xff]) 827 828 , testCase "ofBytes / toBytes round-trip: mixed bytes" $ do 829 let bytes = BS.pack [0x00, 0x01, 0x7f, 0x80, 0xff, 0x41, 0x42, 0x43] 830 toBytes (ofBytes bytes) @?= Right bytes 831 832 , testCase "toBytes rejects non-list tree" $ do 833 -- Leaf is a valid list (empty), so this won't work. 834 -- Stem Leaf is not a list. 835 toBytes (Stem Leaf) @?= Left "Invalid Tree Calculus list" 836 837 , testCase "toBytes rejects list containing invalid byte (>255)" $ do 838 -- [ofNumber 256, ofNumber 1] — first element is > 255 839 let badList = ofList [ofNumber 256, ofNumber 1] 840 toBytes badList @?= Left "Byte value out of range: 256" 841 842 , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Leaf payload" $ do 843 -- Leaf payload is 0x00 (1 byte) 844 let payload = BS.pack [0x00] 845 treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload 846 847 , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Stem payload" $ do 848 -- Stem payload: 0x01 || 32-byte hash = 33 bytes 849 let payload = BS.pack (0x01 : replicate 32 0x42) 850 treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload 851 852 , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Fork payload" $ do 853 -- Fork payload: 0x02 || 32-byte hash || 32-byte hash = 65 bytes 854 let payload = BS.pack (0x02 : replicate 64 0x42) 855 treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload 856 857 , testCase "hashToTreeBytes / treeBytesToHash round-trip" $ do 858 -- Use a known 32-byte hash (SHA256 of "") 859 let hashStr :: MerkleHash 860 hashStr = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 861 case hashToTreeBytes hashStr of 862 Left err -> assertFailure $ "hashToTreeBytes failed: " ++ err 863 Right tree -> treeBytesToHash tree @?= Right hashStr 864 865 , testCase "hashToTreeBytes rejects invalid hex hash" $ do 866 hashToTreeBytes "not-a-hash" @?= Left "Invalid hex MerkleHash" 867 868 , testCase "hashToTreeBytes rejects non-32-byte hash" $ do 869 -- "00" decodes to 1 byte, not 32 870 hashToTreeBytes "00" @?= Left "Hash raw bytes must be 32 bytes" 871 872 , testCase "treeBytesToHash rejects wrong byte count" $ do 873 -- Only 16 bytes, not 32 874 let t16 = ofBytes (BS.pack [0x41 | _ <- [1..16]]) 875 treeBytesToHash t16 @?= Left "Expected exactly 32 byte elements for hash" 876 ] 877 878 -- -------------------------------------------------------------------------- 879 -- Content store tests 880 -- -------------------------------------------------------------------------- 881 882 contentStoreTests :: TestTree 883 contentStoreTests = testGroup "Content Store Tests" 884 [ testCase "Filesystem CAS: put/get object and sharded path" $ 885 withSystemTempDirectory "tricu-store" $ \dir -> do 886 let store = StorePath dir 887 domain = Domain "test.object.v1" 888 payload = BS.pack [1, 2, 3, 4] 889 h <- putObject store domain payload 890 shardForHash h @?= take 3 (unpack h) 891 objectPath store h @?= dir </> "objects" </> take 3 (unpack h) </> unpack h 892 doesFileExist (objectPath store h) >>= (@?= True) 893 getObject store h >>= (@?= Just payload) 894 895 , testCase "Filesystem CAS: idempotent object writes" $ 896 withSystemTempDirectory "tricu-store" $ \dir -> do 897 let store = StorePath dir 898 domain = Domain "test.object.v1" 899 payload = BS.pack [9, 8, 7] 900 h1 <- putObject store domain payload 901 h2 <- putObject store domain payload 902 h1 @?= h2 903 countStoredObjects store >>= (@?= 1) 904 905 , testCase "Filesystem CAS: putTree/getTree round trip" $ 906 withSystemTempDirectory "tricu-store" $ \dir -> do 907 let store = StorePath dir 908 term = Fork (Stem Leaf) (Fork Leaf (Stem Leaf)) 909 leafHash = nodeHash NLeaf 910 stemHash = nodeHash (NStem leafHash) 911 rightHash = nodeHash (NFork leafHash stemHash) 912 expectedRoot = nodeHash (NFork stemHash rightHash) 913 root <- putTree store term 914 root @?= expectedRoot 915 getTree store root >>= (@?= Just term) 916 917 , testCase "Filesystem CAS: shared subtrees are deduplicated" $ 918 withSystemTempDirectory "tricu-store" $ \dir -> do 919 let store = StorePath dir 920 shared = Stem Leaf 921 term = Fork shared shared 922 _ <- putTree store term 923 countStoredObjects store >>= (@?= 3) 924 925 , testCase "Workspace aliases: write/read/list object refs" $ 926 withSystemTempDirectory "tricu-store" $ \dir -> do 927 let store = StorePath dir 928 ref = ObjectRef "arboricx.tree-root.v1" "abc123" 929 writeAlias store NameAlias "main" ref 930 readAlias store NameAlias "main" >>= (@?= Just ref) 931 listAliases store NameAlias >>= (@?= [("main", ref)]) 932 933 , testCase "Module manifests: put/get round trip through CAS" $ 934 withSystemTempDirectory "tricu-store" $ \dir -> do 935 let store = StorePath dir 936 term = Fork Leaf (Stem Leaf) 937 manifestFor root = ModuleManifest [] 938 [ ModuleExport 939 "main" 940 (ObjectRef (unDomain treeTermDomain) root) 941 "arboricx.abi.tree.v1" 942 ] 943 root <- putTreeTerm store term 944 h <- putManifest store (manifestFor root) 945 getManifest store h >>= (@?= Just (manifestFor root)) 946 947 , testCase "ObjectResolver: resolves manifests and trees without filesystem coupling" $ do 948 let term = Fork Leaf (Stem Leaf) 949 leafH = nodeHash NLeaf 950 stemH = nodeHash (NStem leafH) 951 rootH = nodeHash (NFork leafH stemH) 952 termH = hashObject treeTermDomain (encodeTreeTerm term) 953 manifest = ModuleManifest [] 954 [ ModuleExport 955 "value" 956 (ObjectRef (unDomain treeTermDomain) termH) 957 "arboricx.abi.tree.v1" 958 ] 959 manifestBytes = encodeManifest manifest 960 manifestH = hashObject manifestDomain manifestBytes 961 objects = Map.fromList 962 [ (("arboricx.merkle.node.v1", leafH), serializeNode NLeaf) 963 , (("arboricx.merkle.node.v1", stemH), serializeNode (NStem leafH)) 964 , (("arboricx.merkle.node.v1", rootH), serializeNode (NFork leafH stemH)) 965 , ((unDomain treeTermDomain, termH), encodeTreeTerm term) 966 , ((unDomain manifestDomain, manifestH), manifestBytes) 967 ] 968 hydrate objs h = case deserializeNode <$> Map.lookup ("arboricx.merkle.node.v1", h) objs of 969 Nothing -> return Nothing 970 Just NLeaf -> return (Just Leaf) 971 Just (NStem child) -> fmap Stem <$> hydrate objs child 972 Just (NFork left right) -> do 973 l <- hydrate objs left 974 r <- hydrate objs right 975 return $ Fork <$> l <*> r 976 resolver = ObjectResolver 977 { resolverAlias = \kind name -> return $ if kind == ModuleAlias && name == "demo" 978 then Just (ObjectRef (unDomain manifestDomain) manifestH) 979 else Nothing 980 , resolverObject = \ref -> return $ Map.lookup (objectRefKind ref, objectRefHash ref) objects 981 , resolverManifest = \h -> return $ do 982 bytes <- Map.lookup (unDomain manifestDomain, h) objects 983 either (const Nothing) Just (decodeManifest bytes) 984 , resolverTree = hydrate objects 985 } 986 resolverAlias resolver ModuleAlias "demo" >>= (@?= Just (ObjectRef (unDomain manifestDomain) manifestH)) 987 resolveManifest resolver manifestH >>= (@?= Just manifest) 988 resolveTree resolver rootH >>= (@?= Just term) 989 990 , testCase "Arboricx bundle: unpack transport bundle into CAS tree terms" $ 991 withSystemTempDirectory "tricu-store" $ \dir -> do 992 let store = StorePath dir 993 term = Fork (Stem Leaf) Leaf 994 bundle = buildBundle [("main", term)] 995 exports <- unpackBundleToStore store (encodeBundle bundle) 996 case exports of 997 [("main", root)] -> getTreeTerm store root >>= (@?= Just term) 998 other -> assertFailure $ "unexpected exports: " ++ show other 999 1000 , testCase "Indexed CAS: extract roots without labels or unrelated nodes affecting identity" $ do 1001 let term = Fork (Stem Leaf) (Stem Leaf) 1002 first = buildBundle [("alpha", term)] 1003 second = buildBundle [("unrelated", ofNumber 99), ("beta", term)] 1004 [ex] = manifestExports (bundleManifest first) 1005 [_, other] = manifestExports (bundleManifest second) 1006 a <- either assertFailure pure (extractBundleRoot first $ exportRoot ex) 1007 b <- either assertFailure pure (extractBundleRoot second $ exportRoot other) 1008 encodeBundle a @?= encodeBundle b 1009 decodeIndexedTree (encodeBundle a) @?= Right term 1010 hashObject indexedTreeDomain (encodeBundle a) /= hashObject treeTermDomain (encodeTreeTerm term) @? "distinct object identities" 1011 1012 , testCase "Indexed CAS: exponentially shared logical tree stays an indexed graph" $ do 1013 let base = buildBundle [("unused", Leaf)] 1014 graph = base { bundleNodes = Seq.fromList 1015 (BNLeaf : [BNFork (i - 1) (i - 1) | i <- [1..80]]) } 1016 root <- either assertFailure pure (extractBundleRoot graph 80) 1017 Seq.length (bundleNodes root) @?= 81 1018 BS.length (encodeBundle root) < 2048 @? "graph storage must not expand 2^81-1 logical nodes" 1019 case decodeIndexedTree (encodeBundle root) of 1020 Right (Fork _ _) -> pure () 1021 other -> assertFailure $ "expected shared fork, got " ++ either id (const "non-fork") other 1022 1023 , testCase "Indexed CAS: multiple exports become independently loadable objects" $ 1024 withSystemTempDirectory "tricu-indexed-store" $ \dir -> do 1025 let store = StorePath dir 1026 terms = [("left", ofNumber 23), ("right", ofNumber 24)] 1027 roots <- unpackIndexedBundleToStore store (encodeBundle $ buildBundle terms) 1028 map fst roots @?= map fst terms 1029 forM_ (zip roots terms) $ \((_, digest), (_, term)) -> do 1030 payload <- getObject store digest >>= maybe (assertFailure "missing indexed object" >> pure BS.empty) pure 1031 hashObject indexedTreeDomain payload @?= digest 1032 decodeIndexedTree payload @?= Right term 1033 1034 , testCase "Indexed CAS: reject malformed graphs and ambiguous root containers" $ do 1035 let leaf = buildBundle [("root", Leaf)] 1036 forward = leaf { bundleNodes = Seq.singleton (BNStem 0) } 1037 multiple = buildBundle [("a", Leaf), ("b", Stem Leaf)] 1038 case extractBundleRoot forward 0 of 1039 Left _ -> pure () 1040 Right _ -> assertFailure "accepted cyclic node" 1041 case extractBundleRoot leaf 1 of 1042 Left _ -> pure () 1043 Right _ -> assertFailure "accepted out-of-range root" 1044 case decodeIndexedTree (encodeBundle multiple) of 1045 Left _ -> pure () 1046 Right _ -> assertFailure "accepted multiple root container" 1047 1048 , testCase "Streaming pack: exact legacy bundle bytes across chunk boundaries" $ do 1049 let terms = [(pack ("value" ++ show n), ofNumber n) | n <- [0..128]] ++ 1050 [("alias", ofNumber 97), ("tree", Fork (Stem Leaf) (Fork Leaf Leaf))] 1051 packed chunkSize = do 1052 builder <- foldM (appendTerm chunkSize) emptyTreeTermBundle terms 1053 pure (finishTreeTermBundle builder) 1054 appendTerm chunkSize builder (name, term) = do 1055 let chunks bytes | BS.null bytes = [] 1056 | otherwise = let (first, rest) = BS.splitAt chunkSize bytes 1057 in first : chunks rest 1058 cursor <- foldM (flip feedTreeTerm) (startTreeTerm builder) (chunks (encodeTreeTerm term)) 1059 endTreeTerm name cursor 1060 forM_ [1, 2, 3, 7, 65536] $ \chunkSize -> do 1061 actual <- either assertFailure pure (packed chunkSize) 1062 encodeBundle actual @?= encodeBundle (buildBundle terms) 1063 verifyBundle actual @?= Right () 1064 encodeBundle (finishTreeTermBundle emptyTreeTermBundle) @?= encodeBundle (buildBundle []) 1065 1066 , testCase "Streaming pack: rejects truncated, unknown and trailing tree bytes" $ do 1067 let parse bytes = feedTreeTerm bytes (startTreeTerm emptyTreeTermBundle) >>= endTreeTerm "main" 1068 failure bytes expected = case parse bytes of 1069 Left problem -> problem @?= expected 1070 Right _ -> assertFailure "accepted malformed canonical tree" 1071 forM_ [[], [1], [2,0], [2,1,0]] $ \bytes -> 1072 failure (BS.pack bytes) "unexpected end of tree term" 1073 failure (BS.pack [3]) "unknown tree term tag: 3" 1074 failure (BS.pack [0,0]) "trailing bytes after tree term" 1075 failure (BS.pack [1,0,3]) "trailing bytes after tree term" 1076 1077 , testCase "Streaming pack: deep input does not need recursive tree construction" $ do 1078 let depth = 16384 1079 cursor <- either assertFailure pure $ 1080 feedTreeTerm (BS.replicate depth 1 <> BS.singleton 0) (startTreeTerm emptyTreeTermBundle) 1081 builder <- either assertFailure pure (endTreeTerm "main" cursor) 1082 let bundle = finishTreeTermBundle builder 1083 Seq.length (bundleNodes bundle) @?= depth + 1 1084 bundleRoots bundle @?= [fromIntegral depth] 1085 verifyBundle bundle @?= Right () 1086 1087 , testCase "Streaming pack: CAS digest verification and byte-equivalent shared exports" $ 1088 withSystemTempDirectory "tricu-streaming-pack" $ \dir -> do 1089 let store = StorePath dir 1090 terms = [("a", Fork (Stem Leaf) (Stem Leaf)), ("b", Stem Leaf), 1091 ("alias", Fork (Stem Leaf) (Stem Leaf))] 1092 roots <- forM terms $ \(name, term) -> (,) name <$> putTreeTerm store term 1093 bundle <- packBundleFromStore store roots 1094 encodeBundle bundle @?= encodeBundle (buildBundle terms) 1095 -- Cross the actual file reader's 64 KiB boundary with unfinished frames. 1096 let depth = 65537 1097 deep <- putObject store treeTermDomain (BS.replicate depth 1 <> BS.singleton 0) 1098 deepBundle <- packBundleFromStore store [("deep", deep), ("alias", deep)] 1099 Seq.length (bundleNodes deepBundle) @?= depth + 1 1100 bundleRoots deepBundle @?= replicate 2 (fromIntegral depth) 1101 verifyBundle deepBundle @?= Right () 1102 let root = snd (head roots) 1103 BS.writeFile (objectPath store root) (encodeTreeTerm Leaf) 1104 outcome <- try (packBundleFromStore store roots) :: IO (Either SomeException Bundle) 1105 case outcome of 1106 Left problem -> show problem `containsAll` ["Executable tree digest mismatch", unpack root] 1107 Right _ -> assertFailure "packed corrupted CAS bytes" 1108 forM_ [BS.empty, BS.pack [2,0], BS.pack [3], BS.pack [0,0]] $ \bytes -> do 1109 malformed <- putObject store treeTermDomain bytes 1110 rejected <- try (packBundleFromStore store [("bad", malformed)]) :: IO (Either SomeException Bundle) 1111 case rejected of 1112 Left _ -> pure () 1113 Right _ -> assertFailure "packed malformed but correctly hashed CAS bytes" 1114 1115 , testCase "Arboricx bundle: pack CAS tree terms into transport bundle" $ 1116 withSystemTempDirectory "tricu-store" $ \dir -> do 1117 let store = StorePath dir 1118 term = Fork Leaf (Stem Leaf) 1119 root <- putTreeTerm store term 1120 bundle <- packBundleFromStore store [("main", root)] 1121 bundleRoots bundle @?= [2] 1122 let terms = reconstructBundleTermsForTest (bundleNodes bundle) 1123 case manifestExports (bundleManifest bundle) of 1124 [exported] -> do 1125 exportName exported @?= "main" 1126 terms V.! fromIntegral (exportRoot exported) @?= term 1127 other -> assertFailure $ "unexpected exports: " ++ show other 1128 ] 1129 1130 reconstructBundleTermsForTest :: Seq.Seq BundleNode -> V.Vector T 1131 reconstructBundleTermsForTest nodes = V.fromList (go <$> Foldable.toList nodes) 1132 where 1133 built = V.fromList (go <$> Foldable.toList nodes) 1134 go BNLeaf = Leaf 1135 go (BNStem child) = Stem (built V.! fromIntegral child) 1136 go (BNFork left right) = Fork (built V.! fromIntegral left) (built V.! fromIntegral right) 1137 1138 countStoredObjects :: StorePath -> IO Int 1139 countStoredObjects store@(StorePath root) = do 1140 ensureStore store 1141 shards <- listDirectory (root </> "objects") 1142 fmap sum $ forM shards $ \shard -> do 1143 files <- listDirectory (root </> "objects" </> shard) 1144 return (length files) 1145 1146 -- -------------------------------------------------------------------------- 1147 -- Wire module tests 1148 -- -------------------------------------------------------------------------- 1149 1150 -- | Helper: create a temporary file-backed DB, store a term, return the 1151 1152 wireTests :: TestTree 1153 wireTests = testGroup "Wire Tests" 1154 [ testCase "Indexed bundle: header and manifest declare indexed format" $ do 1155 let term = Leaf 1156 bundle = buildBundle [("main", term)] 1157 wireData = encodeBundle bundle 1158 BS.take 8 wireData @?= BS.pack [0x41, 0x52, 0x42, 0x4f, 0x52, 0x49, 0x43, 0x58] 1159 case decodeBundle wireData of 1160 Left err -> assertFailure $ "decodeBundle failed: " ++ err 1161 Right decoded -> do 1162 let manifest = bundleManifest decoded 1163 tree = manifestTree manifest 1164 hashSpec = treeNodeHash tree 1165 manifestSchema manifest @?= "arboricx.bundle.manifest.v1" 1166 manifestBundleType manifest @?= "tree-calculus-executable-object" 1167 manifestClosure manifest @?= ClosureComplete 1168 treeCalculus tree @?= "tree-calculus.v1" 1169 treeNodePayload tree @?= "arboricx.indexed.payload.v1" 1170 nodeHashAlgorithm hashSpec @?= "indexed" 1171 nodeHashDomain hashSpec @?= "arboricx.indexed.node.v1" 1172 bundleRoots decoded @?= bundleRoots bundle 1173 case manifestExports manifest of 1174 [exported] -> do 1175 exportName exported @?= "main" 1176 exportRoot exported @?= head (bundleRoots bundle) 1177 exportKind exported @?= "term" 1178 exportAbi exported @?= "arboricx.abi.tree.v1" 1179 exports -> assertFailure $ "Expected one export, got: " ++ show exports 1180 1181 , testCase "Indexed bundle: deterministic encoding" $ do 1182 let term = Stem (Stem Leaf) 1183 bundle1 = buildBundle [("main", term)] 1184 bundle2 = buildBundle [("main", term)] 1185 encodeBundle bundle1 @?= encodeBundle bundle2 1186 1187 , testCase "Indexed bundle: renaming export changes bytes" $ do 1188 let term = Leaf 1189 mainBundle = buildBundle [("main", term)] 1190 renamedBundle = buildBundle [("validate", term)] 1191 encodeBundle mainBundle /= encodeBundle renamedBundle @? "different export names should produce different bytes" 1192 -- But nodes are identical 1193 bundleNodes mainBundle @?= bundleNodes renamedBundle 1194 1195 , testCase "Indexed bundle: verify rejects out-of-bounds root" $ do 1196 let term = Leaf 1197 bundle = buildBundle [("main", term)] 1198 badBundle = bundle { bundleRoots = [99] } 1199 case verifyBundle badBundle of 1200 Left err -> assertBool ("Expected bounds error, got: " ++ err) ("out of bounds" `isInfixOf` err) 1201 Right () -> assertFailure "Expected out-of-bounds root to be rejected" 1202 1203 , testCase "Indexed bundle: verify rejects out-of-bounds child index" $ do 1204 let bundle = Bundle 1205 { bundleVersion = 1000 1206 , bundleRoots = [1] 1207 , bundleNodes = Seq.fromList [BNLeaf, BNStem 99] 1208 , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)]) 1209 { manifestRoots = [BundleRoot 1 "default"] 1210 , manifestExports = [BundleExport "main" 1 "term" "arboricx.abi.tree.v1"] 1211 } 1212 , bundleManifestBytes = BS.empty 1213 } 1214 case verifyBundle bundle of 1215 Left err -> assertBool ("Expected bounds error, got: " ++ err) ("references child 99" `isInfixOf` err) 1216 Right () -> assertFailure "Expected out-of-bounds child to be rejected" 1217 1218 , testCase "Indexed bundle: verify rejects acyclic (forward reference)" $ do 1219 let bundle = Bundle 1220 { bundleVersion = 1000 1221 , bundleRoots = [1] 1222 , bundleNodes = Seq.fromList [BNStem 1, BNLeaf] -- index 0 refers to 1 (forward) 1223 , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)]) 1224 { manifestRoots = [BundleRoot 1 "default"] 1225 , manifestExports = [BundleExport "main" 1 "term" "arboricx.abi.tree.v1"] 1226 } 1227 , bundleManifestBytes = BS.empty 1228 } 1229 case verifyBundle bundle of 1230 Left err -> assertBool ("Expected acyclicity error, got: " ++ err) ("references child 1" `isInfixOf` err) 1231 Right () -> assertFailure "Expected forward reference to be rejected" 1232 1233 , testCase "Indexed bundle: verify rejects duplicate nodes" $ do 1234 let bundle = Bundle 1235 { bundleVersion = 1000 1236 , bundleRoots = [0] 1237 , bundleNodes = Seq.fromList [BNLeaf, BNLeaf] 1238 , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)]) 1239 { manifestRoots = [BundleRoot 0 "default"] 1240 , manifestExports = [BundleExport "main" 0 "term" "arboricx.abi.tree.v1"] 1241 } 1242 , bundleManifestBytes = BS.empty 1243 } 1244 case verifyBundle bundle of 1245 Left err -> assertBool ("Expected duplicate error, got: " ++ err) ("duplicate" `isInfixOf` err) 1246 Right () -> assertFailure "Expected duplicate nodes to be rejected" 1247 1248 , testCase "Indexed bundle: unpack into filesystem CAS" $ 1249 withSystemTempDirectory "tricu-store" $ \dir -> do 1250 let term = Leaf 1251 bundle = buildBundle [("validateEmail", term)] 1252 wireData = encodeBundle bundle 1253 store = StorePath dir 1254 roots <- unpackBundleToStore store wireData 1255 case roots of 1256 [("validateEmail", root)] -> getTreeTerm store root >>= (@?= Just term) 1257 other -> assertFailure $ "unexpected roots: " ++ show other 1258 1259 , testCase "Indexed bundle: round-trip decode and verify" $ do 1260 let term = Stem (Stem Leaf) 1261 bundle = buildBundle [("main", term)] 1262 wireData = encodeBundle bundle 1263 case decodeBundle wireData of 1264 Left err -> assertFailure $ "decodeBundle failed: " ++ err 1265 Right decoded -> case verifyBundle decoded of 1266 Left err -> assertFailure $ "verifyBundle failed: " ++ err 1267 Right () -> do 1268 bundleRoots decoded @?= bundleRoots bundle 1269 Seq.length (bundleNodes decoded) @?= Seq.length (bundleNodes bundle) 1270 1271 , testCase "Indexed bundle: unsupported manifest semantics rejected" $ do 1272 let term = Leaf 1273 bundle = buildBundle [("main", term)] 1274 manifest = bundleManifest bundle 1275 partialBundle = bundle 1276 { bundleManifest = manifest { manifestClosure = ClosurePartial } 1277 , bundleManifestBytes = BS.empty 1278 } 1279 capabilityBundle = bundle 1280 { bundleManifest = manifest 1281 { manifestRuntime = (manifestRuntime manifest) 1282 { runtimeCapabilities = ["host.io"] } 1283 } 1284 , bundleManifestBytes = BS.empty 1285 } 1286 wrongHashBundle = bundle 1287 { bundleManifest = manifest 1288 { manifestTree = (manifestTree manifest) 1289 { treeNodeHash = (treeNodeHash $ manifestTree manifest) 1290 { nodeHashAlgorithm = "blake3" } 1291 } 1292 } 1293 , bundleManifestBytes = BS.empty 1294 } 1295 case verifyBundle partialBundle of 1296 Left err -> assertBool ("Expected closure error, got: " ++ err) ("closure = complete" `isInfixOf` err) 1297 Right () -> assertFailure "Expected partial closure to be rejected" 1298 case verifyBundle capabilityBundle of 1299 Left err -> assertBool ("Expected capability error, got: " ++ err) ("capabilities" `isInfixOf` err) 1300 Right () -> assertFailure "Expected runtime capabilities to be rejected" 1301 case verifyBundle wrongHashBundle of 1302 Left err -> assertBool ("Expected hash algorithm error, got: " ++ err) ("node hash algorithm" `isInfixOf` err) 1303 Right () -> assertFailure "Expected unsupported node hash algorithm to be rejected" 1304 ] 1305 1306 -- -------------------------------------------------------------------------- 1307 -- Tricu reader tests 1308 -- Smoke-test the tricu-native Arboricx reader against indexed bundles. 1309 -- -------------------------------------------------------------------------- 1310 1311 tricuReaderTests :: TestTree 1312 tricuReaderTests = testGroup "Tricu Reader Tests" 1313 [ testCase "Tricu reader parses indexed bundle (id fixture)" $ check "id" 1314 , testCase "Tricu reader parses indexed bundle (append fixture)" $ check "append" 1315 , testCase "Tricu reader parses indexed bundle (bool fixtures)" $ mapM_ check ["true", "false"] 1316 ] 1317 where 1318 check name = do 1319 bytes <- BS.readFile ("./test/fixtures/" ++ name ++ ".arboricx") 1320 let reply = foldl apply (fixtureExports "arboricx" Map.! "runArboricx") [ofBytes bytes, Leaf] 1321 apply (fixtureExports "base" Map.! "fst") reply @?= trueT 1322 1323 -- -------------------------------------------------------------------------- 1324 -- Byte-list utility tests 1325 -- Expected values built with canonical Haskell-side T constructors. 1326 -- -------------------------------------------------------------------------- 1327 1328 -- | Helpers for byte-list test expectations. 1329 1330 trueT :: T 1331 trueT = Stem Leaf 1332 1333 falseT :: T 1334 falseT = Leaf 1335 1336 pairT :: T -> T -> T 1337 pairT = Fork 1338 1339 portableReply :: Bool -> T -> T -> Bool -> Bool -> T 1340 portableReply success payload rest consumed committed = 1341 pairT (if success then trueT else falseT) 1342 (pairT payload (pairT rest (pairT 1343 (if consumed then trueT else falseT) 1344 (if committed then trueT else falseT)))) 1345 1346 portableDiagnostic :: T -> T -> T -> T -> T -> T 1347 portableDiagnostic code expected found location contexts = 1348 pairT code (pairT expected (pairT found (pairT location contexts))) 1349 1350 -- -------------------------------------------------------------------------- 1351 -- Reusable, located character-scanning primitives 1352 -- -------------------------------------------------------------------------- 1353 1354 locatedLexerTests :: TestTree 1355 locatedLexerTests = testGroup "Located character-scanning library" 1356 [ testCase "checked native scanner handles abstract input" $ do 1357 env <- evaluateCompilerFile "./test/lexerScanImport.tri" 1358 case Map.lookup "scanner" env of 1359 Nothing -> assertFailure "missing portable scanner" 1360 Just scanner -> do 1361 apply scanner (ofString "aa!") @?= 1362 portableReply True 1363 (pairT (ofList [ofNumber 97, ofNumber 97]) 1364 (gatePosition 1 3 2)) 1365 (ofString "!") True False 1366 apply scanner Leaf @?= 1367 portableReply True (pairT Leaf (gatePosition 1 1 0)) Leaf False False 1368 apply scanner (ofString "a\nx") @?= 1369 portableReply True 1370 (pairT (ofList [ofNumber 97]) (gatePosition 1 2 1)) 1371 (ofString "\nx") True False 1372 case Map.lookup "dispatch" env of 1373 Nothing -> assertFailure "missing ordered dispatch" 1374 Just dispatch -> do 1375 apply dispatch (ofNumber 97) @?= ofNumber 1 1376 apply dispatch (ofNumber 98) @?= ofNumber 3 1377 ] 1378 1379 -- -------------------------------------------------------------------------- 1380 -- Single-expression feasibility-gate source lexer 1381 -- -------------------------------------------------------------------------- 1382 1383 gateLexerEnv :: Env 1384 gateLexerEnv = fixtureExports "source.lexer" 1385 {-# NOINLINE gateLexerEnv #-} 1386 1387 gatePosition :: Integer -> Integer -> Integer -> T 1388 gatePosition line column offset = 1389 pairT (ofNumber line) (pairT (ofNumber column) (ofNumber offset)) 1390 1391 gateToken :: Integer -> T -> T -> T -> T 1392 gateToken kind payload start end = 1393 pairT (ofNumber kind) (pairT payload (pairT start end)) 1394 1395 gateLexerTests :: TestTree 1396 gateLexerTests = testGroup "Feasibility-gate source lexer" 1397 [ testCase "module export normalizes under abstract source input" $ 1398 withSystemTempDirectory "gate-lexer-import" $ \dir -> do 1399 env <- evaluateCompilerFile "./test/sourceLexerImport.tri" 1400 Map.lookup "main" env @?= Just Leaf 1401 , testCase "keyword, identifiers, and punctuation carry spans" $ do 1402 check "t (foo : turtle)" 1403 (portableReply True 1404 (ofList 1405 [ gateToken 1 (ofString "t") (gatePosition 1 1 0) (gatePosition 1 2 1) 1406 , gateToken 3 Leaf (gatePosition 1 3 2) (gatePosition 1 4 3) 1407 , gateToken 2 (ofString "foo") (gatePosition 1 4 3) (gatePosition 1 7 6) 1408 , gateToken 5 Leaf (gatePosition 1 8 7) (gatePosition 1 9 8) 1409 , gateToken 2 (ofString "turtle") (gatePosition 1 10 9) (gatePosition 1 16 15) 1410 , gateToken 4 Leaf (gatePosition 1 16 15) (gatePosition 1 17 16) 1411 ]) Leaf True False) 1412 , testCase "decimal digits accumulate and retain leading-zero span" $ do 1413 check "009 x" (portableReply True 1414 (ofList 1415 [ gateToken 7 (ofNumber 9) (gatePosition 1 1 0) (gatePosition 1 4 3) 1416 , gateToken 2 (ofString "x") (gatePosition 1 5 4) (gatePosition 1 6 5) 1417 ]) Leaf True False) 1418 , testCase "quoted string decodes escapes and retains raw source span" $ do 1419 let input = "\"a\\n\\\"b\"" 1420 check input (portableReply True 1421 (ofList [gateToken 8 (ofString "a\n\"b") 1422 (gatePosition 1 1 0) (gatePosition 1 9 8)]) Leaf True False) 1423 , testCase "source string escape repertoire is preserved" $ do 1424 let input = "\"\\t\\r\\f\\b\\\\\\'\"" 1425 decoded = "\t\r\f\b\\'" 1426 width = fromIntegral (length input) 1427 check input (portableReply True 1428 (ofList [gateToken 8 (ofString decoded) 1429 (gatePosition 1 1 0) (gatePosition 1 (width + 1) width)]) 1430 Leaf True False) 1431 , testCase "bracket punctuation retains both half-open spans" $ do 1432 check "[t]" (portableReply True 1433 (ofList 1434 [ gateToken 9 Leaf (gatePosition 1 1 0) (gatePosition 1 2 1) 1435 , gateToken 1 (ofString "t") (gatePosition 1 2 1) (gatePosition 1 3 2) 1436 , gateToken 10 Leaf (gatePosition 1 3 2) (gatePosition 1 4 3) 1437 ]) Leaf True False) 1438 , testCase "equation commas are located tokens, not expression-list syntax" $ 1439 check "[t, t]" (portableReply True (ofList 1440 [ gateToken 9 Leaf (gatePosition 1 1 0) (gatePosition 1 2 1) 1441 , gateToken 1 (ofString "t") (gatePosition 1 2 1) (gatePosition 1 3 2) 1442 , gateToken 20 Leaf (gatePosition 1 3 2) (gatePosition 1 4 3) 1443 , gateToken 1 (ofString "t") (gatePosition 1 5 4) (gatePosition 1 6 5) 1444 , gateToken 10 Leaf (gatePosition 1 6 5) (gatePosition 1 7 6) 1445 ]) Leaf True False) 1446 , testCase "comment delimiters inside strings remain literal" $ 1447 check "\"--|-\"" (portableReply True 1448 (ofList [gateToken 8 (ofString "--|-") 1449 (gatePosition 1 1 0) (gatePosition 1 7 6)]) Leaf True False) 1450 , testCase "invalid and unfinished string escapes return diagnostics" $ do 1451 check "\"\\q\"" (portableReply False 1452 (portableDiagnostic (ofNumber 6) (ofNumber 34) (ofNumber 113) 1453 (gatePosition 1 3 2) Leaf) (ofString "\\q\"") True False) 1454 check "\"hi" (portableReply False 1455 (portableDiagnostic (ofNumber 5) (ofNumber 34) Leaf 1456 (gatePosition 1 1 0) Leaf) Leaf True False) 1457 , testCase "line comment preserves newline token and locations" $ do 1458 check "t --hello\nx" (portableReply True 1459 (ofList 1460 [ gateToken 1 (ofString "t") (gatePosition 1 1 0) (gatePosition 1 2 1) 1461 , gateToken 6 Leaf (gatePosition 1 10 9) (gatePosition 2 1 10) 1462 , gateToken 2 (ofString "x") (gatePosition 2 1 10) (gatePosition 2 2 11) 1463 ]) Leaf True False) 1464 , testCase "block comment consumes internal newlines but preserves end span" $ do 1465 let input = "t |-ab\nc-| t" 1466 check input (portableReply True 1467 (ofList 1468 [ gateToken 1 (ofString "t") (gatePosition 1 1 0) (gatePosition 1 2 1) 1469 , gateToken 1 (ofString "t") (gatePosition 2 5 11) (gatePosition 2 6 12) 1470 ]) Leaf True False) 1471 , testCase "empty block comment consumes both delimiters" $ 1472 check "|- -|t" (portableReply True 1473 (ofList [gateToken 1 (ofString "t") 1474 (gatePosition 1 6 5) (gatePosition 1 7 6)]) Leaf True False) 1475 , testCase "unterminated block comment returns a structured diagnostic" $ 1476 check "t |-oops" (portableReply False 1477 (portableDiagnostic (ofNumber 5) (ofString "-|") Leaf 1478 (gatePosition 1 3 2) Leaf) Leaf True False) 1479 , testCase "comment at EOF leaves no phantom token" $ 1480 check "t --bye" (portableReply True 1481 (ofList [gateToken 1 (ofString "t") 1482 (gatePosition 1 1 0) (gatePosition 1 2 1)]) Leaf True False) 1483 , testCase "lone dash remains a located lexical error" $ 1484 check "-" (portableReply False 1485 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 45) 1486 (gatePosition 1 1 0) Leaf) (ofString "-") False False) 1487 , testCase "newline advances source position" $ do 1488 check "t\nfoo" 1489 (portableReply True 1490 (ofList 1491 [ gateToken 1 (ofString "t") (gatePosition 1 1 0) (gatePosition 1 2 1) 1492 , gateToken 6 Leaf (gatePosition 1 2 1) (gatePosition 2 1 2) 1493 , gateToken 2 (ofString "foo") (gatePosition 2 1 2) (gatePosition 2 4 5) 1494 ]) Leaf True False) 1495 , testCase "invalid character returns structured diagnostic at actual input" $ 1496 check "t #" 1497 (portableReply False 1498 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 35) 1499 (gatePosition 1 3 2) Leaf) (ofString "#") True False) 1500 , testCase "explicit scalar BOM is rejected, matching browser character input" $ 1501 apply (gateLexerEnv Map.! "lexGateSource") (ofString "\xfeff\&t") @?= 1502 portableReply False 1503 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 65279) 1504 (gatePosition 1 1 0) Leaf) (ofString "\xfeff\&t") False False 1505 , testCase "empty input yields empty tokens and no consumption" $ 1506 check "" 1507 (portableReply True Leaf Leaf False False) 1508 , testCase "tab is rejected at its source position" $ 1509 check "t\tfoo" 1510 (portableReply False 1511 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 9) 1512 (gatePosition 1 2 1) Leaf) (ofString "\tfoo") True False) 1513 , testCase "pipe operators retain token kinds and two-character spans" $ do 1514 check "<| |>" (portableReply True 1515 (ofList 1516 [ gateToken 15 Leaf (gatePosition 1 1 0) (gatePosition 1 3 2) 1517 , gateToken 16 Leaf (gatePosition 1 4 3) (gatePosition 1 6 5) 1518 ]) Leaf True False) 1519 , testCase "do and bind arrow have independent located token kinds" $ do 1520 check "do <-" (portableReply True 1521 (ofList 1522 [ gateToken 17 (ofString "do") 1523 (gatePosition 1 1 0) (gatePosition 1 3 2) 1524 , gateToken 18 Leaf (gatePosition 1 4 3) (gatePosition 1 6 5) 1525 ]) Leaf True False) 1526 , testCase "ASCII letter/digit range boundaries remain recognized" $ do 1527 check "AZaz09" (portableReply True 1528 (ofList [gateToken 2 (ofString "AZaz09") 1529 (gatePosition 1 1 0) (gatePosition 1 7 6)]) Leaf True False) 1530 , testCase "longer ASCII identifier scans without discarding suffix" $ 1531 let name = replicate 32 'a' 1532 in check (name ++ " t") 1533 (portableReply True 1534 (ofList 1535 [ gateToken 2 (ofString name) (gatePosition 1 1 0) (gatePosition 1 33 32) 1536 , gateToken 1 (ofString "t") (gatePosition 1 34 33) (gatePosition 1 35 34) 1537 ]) Leaf True False) 1538 ] 1539 where 1540 check input expected = 1541 apply (gateLexerEnv Map.! "lexGateSource") (ofString input) @?= expected 1542 1543 -- -------------------------------------------------------------------------- 1544 -- Single-expression feasibility-gate surface parser 1545 -- -------------------------------------------------------------------------- 1546 1547 -- Library paths select authenticated published interfaces. Unmapped test units 1548 -- use the portable compiler; neither path interprets historical frontend syntax. 1549 evaluateCompilerFile :: FilePath -> IO Env 1550 evaluateCompilerFile path = 1551 case [legacy | (legacy, _, source) <- fst compiledLibraries, 1552 source == maybe path id (stripPrefix "./" path)] of 1553 [legacy] -> pure (fixtureExports legacy) 1554 [] -> compileFixtureFile path 1555 _ -> error "ambiguous compiler fixture module" 1556 1557 -- Test wrappers are compiled by the retained portable stable unit API; library 1558 -- payloads come from authenticated fixtures. Packing remains a host mechanism. 1559 packCompilerFile :: FilePath -> FilePath -> [Text] -> IO () 1560 packCompilerFile input output names = do 1561 env <- evaluateCompilerFile input 1562 let selected = [(name, env Map.! unpack name) | name <- names] 1563 BS.writeFile output (encodeBundle (buildBundle selected)) 1564 1565 -- Immutable pre-optimization worker from the accepted d7b3c37 generation. 1566 -- No source compilation or fresh module builds enter the fast gate. Keeping 1567 -- this separate from the current compiler fixture makes fallback comparisons 1568 -- independent of the implementation under test. 1569 hashHexReference :: Env 1570 hashHexReference = unsafePerformIO $ do 1571 bytes <- BS.readFile "test/fixtures/hash-hex-reference.arboricx" 1572 let digest = unpack (hashObject (Domain "tricu.hash-hex-reference.v1") bytes) 1573 unless (digest == "b5419c577b5a3e8a4458261bfa6934a43cce40eb86e24326aa9a33d6d0552e81") $ 1574 error "hash-hex reference fixture changed; do not refresh it from the candidate" 1575 bundle <- either error pure (decodeBundle bytes) 1576 either error pure (verifyBundle bundle) 1577 let terms = reconstructBundleTerms (bundleNodes bundle) 1578 pure $ Map.fromList 1579 [(unpack (exportName entry), terms V.! fromIntegral (exportRoot entry)) | 1580 entry <- manifestExports (bundleManifest bundle)] 1581 {-# NOINLINE hashHexReference #-} 1582 1583 digestHexTests :: TestTree 1584 digestHexTests = testGroup "guarded digest hex" 1585 [ testCase "all canonical byte values match integer oracle and old kernel" $ 1586 forM_ [0..255] $ \value -> do 1587 let input = ofList [ofNumber value] 1588 expected = if (value >= 48 && value <= 57) || 1589 (value >= 97 && value <= 102) then Stem Leaf else Leaf 1590 apply old input @?= expected 1591 apply current input @?= expected 1592 , testCase "wider, noncanonical and malformed trees retain old behavior" $ 1593 forM_ unusual $ \input -> apply current input @?= apply old input 1594 , testCase "complete digests and late invalid bytes retain old behavior" $ 1595 forM_ strings $ \input -> apply current (ofString input) @?= apply old (ofString input) 1596 , testCase "public digest length and alphabet checks remain exact" $ do 1597 let valid = snd compilerFixture Map.! "source_object.sourceObjectHashValid" 1598 forM_ [0,1,63,64,65] $ \size -> 1599 apply valid (ofString (replicate size 'a')) @?= if size == 64 then Stem Leaf else Leaf 1600 forM_ ['A','G','/', '\NUL', '\255'] $ \byte -> 1601 apply valid (ofString (replicate 63 'a' ++ [byte])) @?= Leaf 1602 ] 1603 where 1604 fix = hashHexReference Map.! "fix" 1605 old = apply fix (hashHexReference Map.! "worker") 1606 current = snd compilerFixture Map.! "source_object.sourceObjectHashHex" 1607 pad Leaf count = foldr (\_ rest -> Fork Leaf rest) Leaf [1..count] 1608 pad (Fork bit rest) count = Fork bit (pad rest count) 1609 pad other _ = other 1610 unusual = 1611 map (ofList . (:[]) . ofNumber) [256,257,511,512,65535] ++ 1612 [ofList [pad (ofNumber value) count] | 1613 value <- [0,1,47,48,49,57,58,96,97,102,103,255], count <- [1,2]] ++ 1614 map (ofList . (:[])) 1615 [Stem Leaf, Stem (Stem Leaf), Fork (Fork Leaf Leaf) Leaf, 1616 Fork (Stem (Stem Leaf)) Leaf, Fork Leaf (Stem Leaf)] ++ 1617 [Leaf, Stem Leaf, Stem (Stem Leaf), 1618 Fork (ofNumber 97) (Stem Leaf), Fork (ofNumber 103) (Stem Leaf)] 1619 strings = ["0123456789abcdef", replicate 64 'a', replicate 64 '0', 1620 replicate 64 'f', concat (replicate 4 "0123456789abcdef"), 1621 'A' : replicate 63 'a', replicate 63 'a' ++ "G", 1622 replicate 31 'a' ++ "\NUL" ++ replicate 32 'a', 1623 replicate 31 'a' ++ "\255" ++ replicate 32 'a'] 1624 1625 compilerFixtureTests :: TestTree 1626 compilerFixtureTests = testGroup "self-built reference compiler fixture" 1627 [ testCase "matches all recorded reference source inputs" $ do 1628 length (fst compilerFixture) @?= 28 1629 assertBool "fixture-only patterns still in compiler closure" 1630 (all (\(_, alias, _) -> alias /= "patterns") (fst compilerFixture)) 1631 assertBool "patterns missing from support cohort" 1632 (any (\(_, alias, _) -> alias == "patterns") (fst supportFixture)) 1633 assertBool "missing name index exports" 1634 (all (`Map.member` snd compilerFixture) 1635 ["source_index.sourceIndexBuild", "source_index.sourceIndexGet", "source_index.sourceIndexPut"]) 1636 assertBool "missing unit compiler export" 1637 (Map.member "source_unit.compileSource" (snd compilerFixture)) 1638 , testCase "direct fixture interfaces exclude imports and reject unknown modules" $ do 1639 let http = fixtureExports "http" 1640 assertBool "missing owned HTTP export" (Map.member "parseHeaders" http) 1641 forM_ ["append", "kernel", "source_unit.compileSource"] $ \name -> 1642 assertBool ("leaked imported/injected name: " ++ name) (Map.notMember name http) 1643 rejected <- try (evaluate (Map.size (fixtureExports "missing.fixture"))) :: IO (Either SomeException Int) 1644 case rejected of 1645 Left _ -> pure () 1646 Right _ -> assertFailure "unknown fixture module silently accepted" 1647 , testCase "portable wrapper compilation keeps unused definitions lazy" $ do 1648 env <- compileFixtureDefinitions Map.empty 1649 "unused = (x : x x) (x : x x)\nmain = t" 1650 Map.keys env @?= ["main", "unused"] 1651 Map.lookup "main" env @?= Just Leaf 1652 , testCase "portable wrapper compilation rejects invalid source without fallback" $ do 1653 forM_ ["main = missing", "main = t\nmain = t", "t", "main = #"] $ \source -> do 1654 rejected <- try (compileFixtureDefinitions Map.empty source) :: IO (Either SomeException Env) 1655 case rejected of 1656 Left err -> assertBool "expected portable diagnostic" 1657 ("portable test compilation failed" `isInfixOf` show err) 1658 Right _ -> assertFailure ("accepted invalid wrapper: " ++ source) 1659 , testCase "portable wrapper compilation uses only explicit externals" $ do 1660 env <- compileFixtureDefinitions (Map.singleton "outside" (Stem Leaf)) "main = outside t" 1661 Map.keys env @?= ["main"] 1662 Map.lookup "main" env @?= Just (Fork Leaf Leaf) 1663 rejected <- try (compileFixtureDefinitions (Map.singleton "outside" Leaf) "outside = t") 1664 :: IO (Either SomeException Env) 1665 case rejected of 1666 Left _ -> pure () 1667 Right _ -> assertFailure "wrapper shadows an imported name" 1668 , testCase "every reference compiler module authors native headers" $ do 1669 forM_ (fst compilerFixture) $ \(_, _, path) -> do 1670 source <- readTestSource path 1671 assertBool ("missing native export: " ++ path) 1672 (any ("!export " `isPrefixOf`) (lines source)) 1673 assertBool ("legacy import: " ++ path) 1674 (not (any ("!import \"" `isPrefixOf`) (lines source))) 1675 , testCase "reference source rejects corrupt bytes and unsafe digests" $ 1676 withSystemTempDirectory "tricu-reference-source" $ \dir -> do 1677 let bytes = BS.pack [120,32,61,32,116,10] 1678 pin = unpack (hashObject (Domain (pack "arboricx.source.v1")) bytes) 1679 path = dir </> (pin ++ ".tri") 1680 rejected action = do 1681 result <- try action :: IO (Either SomeException BS.ByteString) 1682 case result of 1683 Left _ -> pure () 1684 Right _ -> assertFailure "invalid reference source accepted" 1685 rejected (readPinnedSource dir pin) 1686 BS.writeFile path bytes 1687 readPinnedSource dir pin >>= (@?= bytes) 1688 BS.writeFile path (bytes <> BS.singleton 32) 1689 rejected (readPinnedSource dir pin) 1690 rejected (readPinnedSource dir "../source") 1691 , testCase "native prelude retains exactly its dependency re-exports" $ do 1692 let exports = snd compilerFixture 1693 owned alias = Map.fromList 1694 [(drop (length alias + 1) name, tree) | (name, tree) <- Map.toList exports, 1695 (alias ++ ".") `isPrefixOf` name] 1696 owned "prelude" @?= Map.unions (map owned ["base", "bytes", "conversions"]) 1697 , testCase "native base effect interpreter and guard use the rebuilt kernels" $ do 1698 env <- pure (fixtureExports "base") 1699 let pureValue value = Fork (ofNumber 0) value 1700 success value rest = Fork (Stem Leaf) (Fork value rest) 1701 failure value = Fork Leaf (Fork value Leaf) 1702 value = ofNumber 42 1703 exception = Fork (ofNumber 99) 1704 (Fork (ofString "wanted") (Fork value Leaf)) 1705 handler = apply (env Map.! "const") 1706 (apply (env Map.! "const") (pureValue (ofNumber 9))) 1707 handled = foldl apply (env Map.! "handleM") [ofString "wanted", handler, exception] 1708 predicate = apply (env Map.! "const") (Fork Leaf Leaf) 1709 apply (env Map.! "runM") (pureValue value) @?= success value Leaf 1710 apply (env Map.! "runM") exception @?= failure value 1711 apply (env Map.! "runM") (Stem Leaf) @?= Stem Leaf 1712 handled @?= pureValue (ofNumber 9) 1713 foldl apply (env Map.! "guardC") [ofString "bad", predicate, value, ofNumber 7] 1714 @?= success value (ofNumber 7) 1715 , testCase "mixed support reuse requires all provider trees, including unused ones" $ do 1716 let before = Map.fromList [("P.used",1),("P.unused",2)] 1717 source = "!import P as X\n!export f\nf x = X.used x\n" 1718 verifyReuseProviders before before source @?= Right () 1719 forM_ ["P.used", "P.unused"] $ \name -> 1720 verifyReuseProviders before (Map.insert name 3 before) source @?= 1721 Left "provider kernels changed: P" 1722 forM_ [Map.insert "P.extra" 4 before, Map.delete "P.unused" before, Map.empty] $ \after -> 1723 verifyReuseProviders before after source @?= Left "full export set changed: P" 1724 verifyReuseProviders Map.empty before source @?= Left "missing original provider: P" 1725 , testCase "mixed support reuse no longer exempts shadowed imports" $ do 1726 let before = Map.fromList [("P.used",1),("P.unused",2)] 1727 shadowed = "!import P as X\n!export f\nf used = used\n" 1728 verifyReuseProviders before before shadowed @?= Right () 1729 verifyReuseProviders before (Map.insert "P.used" 4 before) shadowed @?= 1730 Left "provider kernels changed: P" 1731 , testCase "mixed support reuse checks standalone reexports and all import forms" $ do 1732 let before = Map.fromList [("P.used",1),("P.unused",2),("PQ.other",3)] 1733 forM_ ["!import P", "!import P *", "!import P as X", "!export * from P", 1734 " !import P *", "!import P *\n!export * from P"] $ \header -> do 1735 let source = header ++ "\n!export f\nf x = x\n" 1736 verifyReuseProviders before before source @?= Right () 1737 verifyReuseProviders before (Map.insert "PQ.other" 4 before) source @?= Right () 1738 verifyReuseProviders before (Map.insert "P.used" 4 before) source @?= 1739 Left "provider kernels changed: P" 1740 verifyReuseProviders before before "!export * from Missing\n" @?= 1741 Left "missing original provider: Missing" 1742 , testCase "mixed support reuse rejects changed namespace collision outcomes" $ do 1743 let before = Map.fromList [("P.used",1),("Q.used",1)] 1744 source = "!import P *\n!import Q *\n!export f\nf x = x\n" 1745 verifyReuseProviders before before source @?= Right () 1746 verifyReuseProviders before (Map.insert "Q.used" 2 before) source @?= 1747 Left "provider kernels changed: Q" 1748 , testCase "mixed support reuse rejects unsupported dependency directives" $ do 1749 let before = Map.singleton "P.used" 1 1750 forM_ ["!import", "!import P as", "!import P * extra", "!import P as *", 1751 "!import \"P\" *", "!export * from", "!export * from P extra", 1752 "!source P", "!unknown P", "!import " ++ replicate 64 'a' ++ " *"] $ \header -> 1753 assertBool ("accepted unsupported reuse header: " ++ header) 1754 (case verifyReuseProviders before before (header ++ "\n") of Left _ -> True; _ -> False) 1755 , testCase "mixed support reuse compares canonical trees, not bundle-local node indices" $ do 1756 let original = buildBundle [("P.used", Stem Leaf), ("P.unused", Fork Leaf Leaf)] 1757 reordered = buildBundle [("Q.extra", ofNumber 42), ("P.unused", Fork Leaf Leaf), 1758 ("P.used", Stem Leaf)] 1759 changed = buildBundle [("P.used", Stem Leaf), ("P.unused", Fork Leaf (Stem Leaf))] 1760 forM_ [original, reordered, changed] $ \bundle -> verifyBundle bundle @?= Right () 1761 case canonicalEnvs [original, reordered, changed] of 1762 [before, same, different] -> do 1763 verifyReuseProviders before same "!import P *\n" @?= Right () 1764 verifyReuseProviders before different "!import P *\n" @?= 1765 Left "provider kernels changed: P" 1766 _ -> assertFailure "incorrect canonical environment count" 1767 , testCase "mixed support reuse permits provider-free unchanged programs" $ 1768 verifyReuseProviders Map.empty Map.empty "-- no dependencies\n!export main\nmain = t\n" @?= Right () 1769 , testCase "support publication allows new rebuilt sources, not relabeled retained sources" $ do 1770 let old = [["module","http","http","lib/http.tri","old-source"]] 1771 new = [["module","http","http","lib/http.tri","new-source"]] 1772 verifySupportSources old new (Set.singleton "http") @?= Right () 1773 verifySupportSources old old Set.empty @?= Right () 1774 verifySupportSources old new Set.empty @?= 1775 Left "retained source fingerprint changed: http" 1776 verifySupportSources old [["module","other","http","lib/http.tri","new-source"]] 1777 (Set.singleton "http") @?= Left "support cohort identity changed" 1778 , testCase "compiler-to-support relocation requires exact identity and a rebuild" $ do 1779 let old = [["module","http","http","lib/http.tri","old-source"]] 1780 added = ["module","patterns","patterns","lib/patterns.tri","new-source"] 1781 declared = ["relocated","compiler-library","patterns","patterns","lib/patterns.tri"] 1782 current = old ++ [added, declared] 1783 verifyRelocatedSupportSources old current (Set.singleton "patterns") @?= Right () 1784 forM_ [old ++ [added], old ++ [added, declared, declared], 1785 old ++ [added, declared, ["module","extra","extra","lib/extra.tri","pin"]], 1786 old ++ [["module","patterns","patterns","lib/other.tri","pin"], declared]] $ \bad -> 1787 assertBool "unauthorized relocation accepted" 1788 (case verifyRelocatedSupportSources old bad (Set.singleton "patterns") of Left _ -> True; _ -> False) 1789 verifyRelocatedSupportSources old current Set.empty @?= Left "invalid patterns cohort relocation" 1790 verifySupportSources old current (Set.singleton "patterns") @?= Left "support cohort identity changed" 1791 , testCase "rebuilt export sets follow the new manifest, not the old interface" $ do 1792 let manifest kind names = unlines ("arboricx.module-manifest.v1" : 1793 ["export " ++ name ++ " " ++ kind ++ " hash arboricx.abi.tree.v1" | name <- names]) 1794 names = Set.fromList ["parseHeaders", "httpAsciiBit"] 1795 forM_ ["arboricx.tree-term.v1", "arboricx.indexed-tree.v1"] $ \kind -> do 1796 verifySupportExports names (manifest kind (Set.toList names)) @?= Right () 1797 forM_ [["parseHeaders"], ["parseHeaders", "httpAsciiBit", "byte7BitsOr"], 1798 ["parseHeaders", "httpAsciiBit", "httpAsciiBit"]] $ \bad -> 1799 verifySupportExports names (manifest kind bad) @?= Left "rebuilt manifest export set mismatch" 1800 verifySupportExports names (manifest "unknown-kind" (Set.toList names)) 1801 @?= Left "rebuilt manifest export set mismatch" 1802 verifySupportExports names (unlines ["arboricx.module-manifest.v1", 1803 "export parseHeaders arboricx.tree-term.v1 hash arboricx.abi.tree.v1", 1804 "export httpAsciiBit arboricx.indexed-tree.v1 hash arboricx.abi.tree.v1"]) 1805 @?= Left "rebuilt manifest export set mismatch" 1806 , testCase "rebuilt consumers reject stale support provider manifests" $ do 1807 let providers = Map.singleton "http" "new-manifest" 1808 rows pin = [["local","http",pin,"facade-source"], 1809 ["pin","http",pin,"parseHeaders","tree"]] 1810 verifySupportDependencies providers (rows "new-manifest") @?= Right () 1811 verifySupportDependencies providers (rows "old-manifest") @?= 1812 Left "stale support dependency: http" 1813 verifySupportDependencies providers 1814 [["local","http","new-manifest","source"], ["pin","http","old-manifest","f","tree"]] 1815 @?= Left "stale support dependency: http" 1816 verifySupportDependencies providers 1817 (replicate 2 ["local","http","new-manifest","source"]) @?= Left "duplicate local dependency" 1818 , testCase "native staging preserves namespaces and explicit exports" $ do 1819 env <- pure (fixtureExports "source.map") 1820 let row = pairT (ofString "example") (pairT (ofString "example") 1821 (pairT (ofString "example.tri") 1822 (pairT (ofList [ofString "native_math"]) (ofString "native")))) 1823 source = ofString "!import native_math as N\n!export main\nmain = N.add 2 3\n" 1824 prepare text = apply (apply (apply (env Map.! "sourceMapPrepareBody") row) text) Leaf 1825 prepare source @?= pairT (Stem Leaf) (pairT source Leaf) 1826 apply (apply (env Map.! "sourceMapOverlay") row) source @?= source 1827 forM_ ["", "!source other\n", "!source extra\n"] $ \bad -> 1828 apply (fixtureExports "base" Map.! "fst") (prepare (ofString bad)) @?= Leaf 1829 , testCase "shared native header derives ordered deduplicated edges and masks positions" $ do 1830 env <- pure (fixtureExports "source.module") 1831 let header = "-- lead\r\n!import math\n!export * from base\n!import math as M\n!source build_only\n!export main\n" 1832 body = "main = missing\n" 1833 masked = map (\c -> if c == '\n' || c == '\r' then c else ' ') header ++ body 1834 wordsT = ofList . map ofString 1835 staged = pairT (wordsT ["math", "base", "build_only"]) 1836 (pairT (ofList [wordsT ["math"], wordsT ["math", "as", "M"]]) 1837 (pairT (ofList [wordsT ["*", "from", "base"], wordsT ["main"]]) (ofString masked))) 1838 apply (env Map.! "sourceModuleHeaderStage") (ofString (header ++ body)) @?= 1839 pairT (Stem Leaf) (pairT staged Leaf) 1840 , testCase "native header rejects redundant edges and bare hashes at original locations" $ do 1841 env <- pure (fixtureExports "source.module") 1842 let bad sources = forM_ sources $ \source -> 1843 apply (fixtureExports "base" Map.! "fst") (apply (env Map.! "sourceModuleHeaderStage") (ofString source)) @?= Leaf 1844 bad ["!source dep\n!import dep\n", "!import dep\n!source dep\n", 1845 "!source dep\n!export * from dep\n", "!source dep\n!source dep\n", 1846 "!import " ++ replicate 64 'a' ++ "\n", "!use dep *\n", "!import \"dep\" !Local\n"] 1847 let outcome = apply (env Map.! "sourceModuleHeaderStage") 1848 (ofString "-- lead\n\n!use dep *\n") 1849 problem = apply (fixtureExports "base" Map.! "fst") (apply (fixtureExports "base" Map.! "snd") outcome) 1850 apply (fixtureExports "parser" Map.! "parserDiagnosticLocation") problem @?= gatePosition 3 1 9 1851 , testCase "native math exports are public only and general equality remains" $ do 1852 env <- pure (fixtureExports "math.native") 1853 assertBool "missing math decimal" (Map.member "decimal" env) 1854 let owned = [name | name <- Map.keys (snd compilerFixture), "native_math." `isPrefixOf` name] 1855 length owned @?= 27 1856 forM_ ["divMod", "quotient", "remainder", "power"] $ \name -> 1857 assertBool ("missing native arithmetic export: " ++ name) (Map.member name env) 1858 assertBool "private math helper exported" (not ("native_math.compareStep" `elem` owned)) 1859 prelude <- pure (fixtureExports "prelude") 1860 assertBool "general equality removed" (Map.member "equal?" prelude) 1861 ] 1862 1863 gateParserEnv :: Env 1864 gateParserEnv = fixtureExports "source.parser" 1865 {-# NOINLINE gateParserEnv #-} 1866 1867 gateAst :: Integer -> T -> T -> T -> T 1868 gateAst kind payload start end = 1869 pairT (ofNumber kind) (pairT payload (pairT start end)) 1870 1871 gateParserTests :: TestTree 1872 gateParserTests = testGroup "Feasibility-gate source parser" 1873 [ testCase "self-built parser export executes through its wrapper" $ do 1874 env <- evaluateCompilerFile "./test/sourceGateParserImport.tri" 1875 Map.lookup "main" env @?= Just 1876 (portableReply True 1877 (gateAst 1 Leaf (gatePosition 1 1 0) (gatePosition 1 2 1)) 1878 Leaf True False) 1879 , testCase "leaf retains its source span" $ 1880 check "t" (portableReply True 1881 (gateAst 1 Leaf (gatePosition 1 1 0) (gatePosition 1 2 1)) 1882 Leaf True False) 1883 , testCase "decimal integer is a located value" $ 1884 check "007" (portableReply True 1885 (gateAst 6 (ofNumber 7) (gatePosition 1 1 0) (gatePosition 1 4 3)) 1886 Leaf True False) 1887 , testCase "string is a located decoded value" $ 1888 check "\"hi\"" (portableReply True 1889 (gateAst 7 (ofString "hi") (gatePosition 1 1 0) (gatePosition 1 5 4)) 1890 Leaf True False) 1891 , testCase "empty list retains its brackets and canonical empty payload" $ 1892 check "[]" (portableReply True 1893 (gateAst 8 Leaf (gatePosition 1 1 0) (gatePosition 1 3 2)) 1894 Leaf True False) 1895 , testCase "list elements are separate atoms, not an application" $ 1896 let leaf1 = gateAst 1 Leaf (gatePosition 1 2 1) (gatePosition 1 3 2) 1897 leaf2 = gateAst 1 Leaf (gatePosition 1 4 3) (gatePosition 1 5 4) 1898 in check "[t t]" (portableReply True 1899 (gateAst 8 (ofList [leaf1, leaf2]) 1900 (gatePosition 1 1 0) (gatePosition 1 6 5)) Leaf True False) 1901 , testCase "nested lists and grouped applications retain distinct spans" $ 1902 let innerLeaf = gateAst 1 Leaf 1903 (gatePosition 1 3 2) (gatePosition 1 4 3) 1904 innerList = gateAst 8 (ofList [innerLeaf]) 1905 (gatePosition 1 2 1) (gatePosition 1 5 4) 1906 left = gateAst 1 Leaf (gatePosition 1 7 6) (gatePosition 1 8 7) 1907 right = gateAst 1 Leaf (gatePosition 1 9 8) (gatePosition 1 10 9) 1908 app = gateAst 3 (pairT left right) 1909 (gatePosition 1 7 6) (gatePosition 1 10 9) 1910 group = gateAst 5 app (gatePosition 1 6 5) (gatePosition 1 11 10) 1911 in check "[[t] (t t)]" (portableReply True 1912 (gateAst 8 (ofList [innerList, group]) 1913 (gatePosition 1 1 0) (gatePosition 1 12 11)) Leaf True False) 1914 , testCase "unclosed list reports the expected closing bracket" $ 1915 check "[t" (portableReply False 1916 (portableDiagnostic (ofNumber 2) (ofNumber 10) Leaf Leaf Leaf) 1917 Leaf True False) 1918 , testCase "unexpected separator retains its token and location" $ 1919 check "[t : t]" (portableReply False 1920 (portableDiagnostic (ofNumber 2) (ofNumber 10) (ofNumber 5) 1921 (gatePosition 1 4 3) Leaf) 1922 (ofList 1923 [ gateToken 5 Leaf (gatePosition 1 4 3) (gatePosition 1 5 4) 1924 , gateToken 1 (ofString "t") 1925 (gatePosition 1 6 5) (gatePosition 1 7 6) 1926 , gateToken 10 Leaf (gatePosition 1 7 6) (gatePosition 1 8 7) 1927 ]) True False) 1928 , testCase "application is left associative" $ 1929 let name = gateAst 2 (ofString "f") (gatePosition 1 1 0) (gatePosition 1 2 1) 1930 arg1 = gateAst 1 Leaf (gatePosition 1 3 2) (gatePosition 1 4 3) 1931 arg2 = gateAst 1 Leaf (gatePosition 1 5 4) (gatePosition 1 6 5) 1932 first = gateAst 3 (pairT name arg1) (gatePosition 1 1 0) (gatePosition 1 4 3) 1933 in check "f t t" (portableReply True 1934 (gateAst 3 (pairT first arg2) (gatePosition 1 1 0) (gatePosition 1 6 5)) 1935 Leaf True False) 1936 , testCase "let retains binding, value, body, and complete span" $ 1937 let name = gateToken 2 (ofString "x") 1938 (gatePosition 1 5 4) (gatePosition 1 6 5) 1939 value = gateAst 1 Leaf (gatePosition 1 9 8) (gatePosition 1 10 9) 1940 body = gateAst 2 (ofString "x") 1941 (gatePosition 1 14 13) (gatePosition 1 15 14) 1942 in check "let x = t in x" (portableReply True 1943 (gateAst 9 (pairT name (pairT value body)) 1944 (gatePosition 1 1 0) (gatePosition 1 15 14)) Leaf True False) 1945 , testCase "where retains the span of the whole expression" $ 1946 let name = gateToken 2 (ofString "x") 1947 (gatePosition 1 9 8) (gatePosition 1 10 9) 1948 value = gateAst 1 Leaf (gatePosition 1 13 12) (gatePosition 1 14 13) 1949 body = gateAst 2 (ofString "x") 1950 (gatePosition 1 1 0) (gatePosition 1 2 1) 1951 in check "x where x = t" (portableReply True 1952 (gateAst 9 (pairT name (pairT value body)) 1953 (gatePosition 1 1 0) (gatePosition 1 14 13)) Leaf True False) 1954 , testCase "a top-level newline does not silently join application operands" $ 1955 check "t\n t" (portableReply False 1956 (portableDiagnostic (ofNumber 2) Leaf (ofNumber 6) 1957 (gatePosition 1 2 1) Leaf) 1958 (ofList 1959 [ gateToken 6 Leaf 1960 (gatePosition 1 2 1) (gatePosition 2 1 2) 1961 , gateToken 1 (ofString "t") 1962 (gatePosition 2 2 3) (gatePosition 2 3 4) 1963 ]) True False) 1964 , testCase "parentheses permit multiline application with original spans" $ 1965 let left = gateAst 1 Leaf 1966 (gatePosition 1 2 1) (gatePosition 1 3 2) 1967 right = gateAst 1 Leaf 1968 (gatePosition 2 2 4) (gatePosition 2 3 5) 1969 application = gateAst 3 (pairT left right) 1970 (gatePosition 1 2 1) (gatePosition 2 3 5) 1971 in check "(t\n t)" (portableReply True 1972 (gateAst 5 application 1973 (gatePosition 1 1 0) (gatePosition 2 4 6)) Leaf True False) 1974 , testCase "pipe retains source order and direction before elaboration" $ 1975 let value = gateAst 6 (ofNumber 2) 1976 (gatePosition 1 1 0) (gatePosition 1 2 1) 1977 name = gateAst 2 (ofString "f") 1978 (gatePosition 1 6 5) (gatePosition 1 7 6) 1979 in check "2 |> f" (portableReply True 1980 (gateAst 10 (pairT (ofNumber 16) (pairT value name)) 1981 (gatePosition 1 1 0) (gatePosition 1 7 6)) Leaf True False) 1982 , testCase "do retains operator, located binders, and statements" $ 1983 let op = gateAst 2 (ofString "b") 1984 (gatePosition 1 4 3) (gatePosition 1 5 4) 1985 name = gateToken 2 (ofString "x") 1986 (gatePosition 2 3 7) (gatePosition 2 4 8) 1987 action = gateAst 6 (ofNumber 2) 1988 (gatePosition 2 8 12) (gatePosition 2 9 13) 1989 body = gateAst 2 (ofString "x") 1990 (gatePosition 3 3 16) (gatePosition 3 4 17) 1991 in check "do b\n x <- 2\n x" (portableReply True 1992 (gateAst 11 (pairT op (ofList 1993 [pairT (Stem name) action, pairT Leaf body])) 1994 (gatePosition 1 1 0) (gatePosition 3 4 17)) Leaf True False) 1995 , testCase "single-argument lambda preserves binder and body" $ 1996 let body = gateAst 2 (ofString "x") (gatePosition 1 5 4) (gatePosition 1 6 5) 1997 in check "x : x" (portableReply True 1998 (gateAst 4 (pairT (ofString "x") body) 1999 (gatePosition 1 1 0) (gatePosition 1 6 5)) Leaf True False) 2000 , testCase "multiple same-line lambda binders elaborate in source order" $ 2001 let body = gateAst 2 (ofString "a") 2002 (gatePosition 1 7 6) (gatePosition 1 8 7) 2003 inner = gateAst 4 (pairT (ofString "b") body) 2004 (gatePosition 1 3 2) (gatePosition 1 8 7) 2005 in check "a b : a" (portableReply True 2006 (gateAst 4 (pairT (ofString "a") inner) 2007 (gatePosition 1 1 0) (gatePosition 1 8 7)) Leaf True False) 2008 , testCase "lambda body may start on a later line" $ 2009 let body = gateAst 2 (ofString "x") 2010 (gatePosition 2 3 6) (gatePosition 2 4 7) 2011 in check "x :\n x" (portableReply True 2012 (gateAst 4 (pairT (ofString "x") body) 2013 (gatePosition 1 1 0) (gatePosition 2 4 7)) Leaf True False) 2014 , testCase "nested lambdas associate to the right" $ 2015 let body = gateAst 2 (ofString "x") (gatePosition 1 9 8) (gatePosition 1 10 9) 2016 inner = gateAst 4 (pairT (ofString "y") body) 2017 (gatePosition 1 5 4) (gatePosition 1 10 9) 2018 in check "x : y : x" (portableReply True 2019 (gateAst 4 (pairT (ofString "x") inner) 2020 (gatePosition 1 1 0) (gatePosition 1 10 9)) Leaf True False) 2021 , testCase "grouping preserves its outer span" $ 2022 let leaf = gateAst 1 Leaf (gatePosition 1 2 1) (gatePosition 1 3 2) 2023 group = gateAst 5 leaf (gatePosition 1 1 0) (gatePosition 1 4 3) 2024 arg = gateAst 1 Leaf (gatePosition 1 5 4) (gatePosition 1 6 5) 2025 in check "(t) t" (portableReply True 2026 (gateAst 3 (pairT group arg) 2027 (gatePosition 1 1 0) (gatePosition 1 6 5)) Leaf True False) 2028 , testCase "empty input reports expected atom" $ 2029 check "" (portableReply False 2030 (portableDiagnostic (ofNumber 2) (ofNumber 7) Leaf Leaf Leaf) 2031 Leaf False False) 2032 , testCase "opened group fails after consumption" $ 2033 check "t (" (portableReply False 2034 (portableDiagnostic (ofNumber 2) (ofNumber 7) Leaf Leaf Leaf) 2035 Leaf True False) 2036 , testCase "missing lambda body is a consumed parse failure" $ 2037 check "x : " (portableReply False 2038 (portableDiagnostic (ofNumber 2) (ofNumber 7) Leaf Leaf Leaf) 2039 Leaf True False) 2040 , testCase "unmatched close paren retains its span" $ 2041 check "t)" (portableReply False 2042 (portableDiagnostic (ofNumber 2) Leaf (ofNumber 4) 2043 (gatePosition 1 2 1) Leaf) 2044 (ofList [gateToken 4 Leaf (gatePosition 1 2 1) (gatePosition 1 3 2)]) 2045 True False) 2046 , testCase "lexical error passes through the source parser" $ 2047 check "t #" (portableReply False 2048 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 35) 2049 (gatePosition 1 3 2) Leaf) 2050 (ofString "#") True False) 2051 , testCase "newline does not turn into application whitespace" $ 2052 check "t\nfoo" (portableReply False 2053 (portableDiagnostic (ofNumber 2) Leaf (ofNumber 6) 2054 (gatePosition 1 2 1) Leaf) 2055 (ofList 2056 [ gateToken 6 Leaf (gatePosition 1 2 1) (gatePosition 2 1 2) 2057 , gateToken 2 (ofString "foo") (gatePosition 2 1 2) (gatePosition 2 4 5) 2058 ]) True False) 2059 ] 2060 where 2061 check input expected = 2062 apply (gateParserEnv Map.! "parseGateSource") (ofString input) @?= expected 2063 2064 -- -------------------------------------------------------------------------- 2065 -- Program-level feasibility-gate parsing (definitions and final expression) 2066 -- -------------------------------------------------------------------------- 2067 2068 equationHelperEnv :: Env 2069 equationHelperEnv = fixtureExports "source.equations" 2070 {-# NOINLINE equationHelperEnv #-} 2071 2072 equationHelperTests :: TestTree 2073 equationHelperTests = testGroup "Equation helper pattern adoption" 2074 [ testCase "expected token consumes one cell and preserves suffix" $ 2075 expect 7 input @?= portableReply True token suffix True False 2076 , testCase "wrong token preserves full input and original location" $ 2077 expect 2 input @?= portableReply False 2078 (portableDiagnostic (ofNumber 2) (ofNumber 2) (ofNumber 7) start Leaf) 2079 input False False 2080 , testCase "empty token input has unchanged end diagnostic" $ 2081 expect 2 Leaf @?= endReply 2 Leaf 2082 , testCase "non-list token input has unchanged end diagnostic" $ 2083 expect 2 (Stem Leaf) @?= endReply 2 (Stem Leaf) 2084 , testCase "labelled error preserves token kind span and full remainder" $ 2085 labelled input @?= portableReply False 2086 (portableDiagnostic (ofNumber 48) label (ofNumber 7) start Leaf) 2087 input False False 2088 , testCase "labelled empty error retains protocol flags" $ 2089 labelled Leaf @?= labelledEnd Leaf 2090 , testCase "labelled non-list error retains original input" $ 2091 labelled (Stem Leaf) @?= labelledEnd (Stem Leaf) 2092 , testCase "empty pattern prefix returns supplied terminator" $ 2093 list Leaf @?= nil 2094 , testCase "non-list pattern prefix returns supplied terminator" $ 2095 list (Stem Leaf) @?= nil 2096 , testCase "pattern prefix has independent nested payload and span golden" $ 2097 list (ofList [left, right]) @?= joined left (joined right nil) 2098 , testCase "improper pattern suffix retains prior non-list termination" $ 2099 list (Fork left (Stem Leaf)) @?= joined left nil 2100 , testCase "empty pattern list retains an opaque supplied tail" $ 2101 apply (fixtureExports "base" Map.! "fst") 2102 (call "sourceEquationList" [Leaf, Fork (ofNumber 23) 2103 (error "unused suffix demanded")]) @?= ofNumber 23 2104 ] 2105 where 2106 call name args = foldl apply (equationHelperEnv Map.! name) args 2107 start = gatePosition 2 3 5 2108 end = gatePosition 2 4 6 2109 token = gateToken 7 (ofNumber 23) start end 2110 suffix = ofList [Leaf] 2111 input = Fork token suffix 2112 label = ofString "pattern" 2113 expect kind value = call "sourceEquationExpect" [ofNumber kind, value] 2114 labelled value = call "sourceEquationError" [label, value] 2115 endReply kind value = portableReply False 2116 (portableDiagnostic (ofNumber 2) (ofNumber kind) Leaf Leaf Leaf) 2117 value False False 2118 labelledEnd value = portableReply False 2119 (portableDiagnostic (ofNumber 48) label Leaf Leaf Leaf) value False False 2120 patternNode tag payload a b = gateAst 13 (pairT (ofNumber tag) payload) a b 2121 nil = patternNode 1 Leaf (gatePosition 2 7 9) (gatePosition 2 8 10) 2122 left = patternNode 4 (ofString "x") start end 2123 right = patternNode 4 (ofString "y") (gatePosition 2 5 7) (gatePosition 2 6 8) 2124 joined a b = patternNode 3 (pairT a b) 2125 (case a of Fork _ (Fork _ (Fork x _)) -> x; _ -> error "invalid golden") 2126 (gatePosition 2 8 10) 2127 list value = call "sourceEquationList" [value, nil] 2128 2129 gateProgramEnv :: Env 2130 gateProgramEnv = fixtureExports "source.program" 2131 {-# NOINLINE gateProgramEnv #-} 2132 2133 gateProgramTests :: TestTree 2134 gateProgramTests = testGroup "Feasibility-gate program parser" 2135 [ testCase "portable wrapper normalizes the definition parser" $ do 2136 env <- evaluateCompilerFile 2137 "./test/sourceGateProgramImport.tri" 2138 Map.lookup "main" env @?= Just programResult 2139 , testCase "definition arguments elaborate to located lambdas" $ do 2140 check "id x = x\nid t" programResult 2141 , testCase "an identifier-led expression without assignment stays an expression" $ 2142 let name = gateAst 2 (ofString "f") 2143 (gatePosition 1 1 0) (gatePosition 1 2 1) 2144 leaf = gateAst 1 Leaf 2145 (gatePosition 1 3 2) (gatePosition 1 4 3) 2146 expression = gateAst 3 (pairT name leaf) 2147 (gatePosition 1 1 0) (gatePosition 1 4 3) 2148 in check "f t" (portableReply True 2149 (pairT Leaf (Stem expression)) Leaf True False) 2150 , testCase "unfinished definition fails after consuming the assignment" $ 2151 check "id x =" (portableReply False 2152 (portableDiagnostic (ofNumber 2) (ofNumber 7) Leaf Leaf Leaf) 2153 Leaf True False) 2154 , testCase "an empty unit has no definitions or final expression" $ 2155 check "" (portableReply True (pairT Leaf Leaf) Leaf False False) 2156 ] 2157 where 2158 programResult = 2159 let name = gateToken 2 (ofString "id") 2160 (gatePosition 1 1 0) (gatePosition 1 3 2) 2161 bodyName = gateAst 2 (ofString "x") 2162 (gatePosition 1 8 7) (gatePosition 1 9 8) 2163 body = gateAst 4 (pairT (ofString "x") bodyName) 2164 (gatePosition 1 4 3) (gatePosition 1 9 8) 2165 expressionName = gateAst 2 (ofString "id") 2166 (gatePosition 2 1 9) (gatePosition 2 3 11) 2167 leaf = gateAst 1 Leaf 2168 (gatePosition 2 4 12) (gatePosition 2 5 13) 2169 expression = gateAst 3 (pairT expressionName leaf) 2170 (gatePosition 2 1 9) (gatePosition 2 5 13) 2171 in portableReply True 2172 (pairT (ofList [pairT name body]) (Stem expression)) Leaf True False 2173 check input expected = 2174 apply (gateProgramEnv Map.! "parseGateProgram") (ofString input) @?= expected 2175 2176 -- -------------------------------------------------------------------------- 2177 -- Portable, stable dependency ordering and cycle witnesses 2178 -- -------------------------------------------------------------------------- 2179 2180 sourceGraphEnv :: Env 2181 sourceGraphEnv = fixtureExports "graph" 2182 {-# NOINLINE sourceGraphEnv #-} 2183 2184 sourceGraphTests :: TestTree 2185 sourceGraphTests = testGroup "Source dependency graph" 2186 [ testCase "checked native graph sorts dependencies before dependents" $ do 2187 env <- evaluateCompilerFile "./test/sourceGraphImport.tri" 2188 Map.lookup "main" env @?= Just 2189 (parserOk (ofList (map ofString ["leaf", "inner", "outer"])) Leaf) 2190 , testCase "sort preserves source order among simultaneously ready nodes" $ 2191 check [("b", []), ("a", [])] 2192 (parserOk (ofList [ofString "b", ofString "a"]) Leaf) 2193 , testCase "cycle witness is in the cycle, not just a dependent" $ 2194 check [("outside", ["inside"]), ("inside", ["last"]), ("last", ["inside"])] 2195 (parserErr (ofString "inside") Leaf) 2196 ] 2197 where 2198 check entries expected = 2199 apply (sourceGraphEnv Map.! "graphSort") 2200 (ofList [Fork (ofString name) (ofList (map ofString dependencies)) 2201 | (name, dependencies) <- entries]) @?= expected 2202 2203 -- -------------------------------------------------------------------------- 2204 -- Program-level free names and dependency ordering 2205 -- -------------------------------------------------------------------------- 2206 2207 gateOrderingEnv :: Env 2208 gateOrderingEnv = fixtureExports "source.order" 2209 {-# NOINLINE gateOrderingEnv #-} 2210 2211 gateOrderingTests :: TestTree 2212 gateOrderingTests = testGroup "Feasibility-gate definition ordering" 2213 [ testCase "portable wrapper orders forward references" $ do 2214 env <- evaluateCompilerFile 2215 "./test/sourceGateOrderImport.tri" 2216 Map.lookup "main" env @?= Just (parserOk 2217 (ofList [ofString "f", ofString "main"]) Leaf) 2218 , testCase "unused definitions still have their names checked" $ 2219 check Leaf "main = t\nbroken = u" (parserErr 2220 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 2221 (gatePosition 2 10 18) Leaf) 2222 (gateAst 2 (ofString "u") 2223 (gatePosition 2 10 18) (gatePosition 2 11 19))) 2224 , testCase "lexical binder shadows a definition and external name" $ 2225 check (ofList [Fork (ofString "x") Leaf]) "x = t\nf x = x" (parserOk 2226 (ofList [ofString "x", ofString "f"]) Leaf) 2227 , testCase "top-level self reference returns a cycle witness" $ 2228 check Leaf "f = f" (parserErr 2229 (portableDiagnostic (ofNumber 31) (ofString "acyclic") 2230 (ofString "f") (gatePosition 1 1 0) 2231 (ofList [ofString "f"])) Leaf) 2232 , testCase "duplicate definition reports the later name location" $ 2233 let name = gateToken 2 (ofString "f") 2234 (gatePosition 2 1 6) (gatePosition 2 2 7) 2235 body = gateAst 1 Leaf 2236 (gatePosition 2 5 10) (gatePosition 2 6 11) 2237 in check Leaf "f = t\nf = t" (parserErr 2238 (portableDiagnostic (ofNumber 30) (ofString "f") 2239 (ofString "f") (gatePosition 2 1 6) Leaf) 2240 (pairT name body)) 2241 , testCase "a final expression also checks unresolved names" $ 2242 check Leaf "f = t\nu" (parserErr 2243 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 2244 (gatePosition 2 1 6) Leaf) 2245 (gateAst 2 (ofString "u") 2246 (gatePosition 2 1 6) (gatePosition 2 2 7))) 2247 ] 2248 where 2249 check environment source expected = 2250 foldl apply (gateOrderingEnv Map.! "orderGateSource") [environment, ofString source] 2251 @?= expected 2252 2253 -- -------------------------------------------------------------------------- 2254 -- End-to-end program compilation through ordered definitions 2255 -- -------------------------------------------------------------------------- 2256 2257 gateUnitEnv :: Env 2258 gateUnitEnv = unsafePerformIO $ evaluateCompilerFile "./lib/source/unit.tri" 2259 {-# NOINLINE gateUnitEnv #-} 2260 2261 gateUnitTests :: TestTree 2262 gateUnitTests = testGroup "Feasibility-gate program compiler" 2263 [ testCase "real source files compile without rewriting or relexing" $ do 2264 let corpus = 2265 [ ("./test/fork.tri", [], Fork Leaf Leaf) 2266 , ("./test/comments-1.tri", [], Fork (Stem Leaf) Leaf) 2267 , ("./test/map.tri", ["map", "append", "equal?", "head"], trueT) 2268 ] 2269 forM_ corpus $ \(path, names, expected) -> do 2270 source <- readFile path 2271 let external = ofList [Fork (ofString name) (fixtureExports "base" Map.! name) | name <- names] 2272 foldl apply (fixtureExports "source.unit" Map.! "compileGateProgram") 2273 [external, ofString source] @?= parserOk expected Leaf 2274 , testCase "portable size counts leaf, stem, and fork arguments" $ do 2275 source <- readFile "./test/size.tri" 2276 let definitions = unlines (init (lines source)) 2277 external = ofList [Fork (ofString name) (fixtureExports "base" Map.! name) 2278 | name <- ["y", "triage"]] 2279 forM_ [("t", 1), ("t t", 2), ("t t t", 3)] $ \(argument, count) -> do 2280 let input = definitions ++ "size (" ++ argument ++ ")\n" 2281 foldl apply (fixtureExports "source.unit" Map.! "compileGateProgram") 2282 [external, ofString input] @?= parserOk (ofNumber count) Leaf 2283 , testCase "real file inputs match the natively packed self-built artifact" $ 2284 withSystemTempDirectory "gate-file-corpus" $ \dir -> do 2285 let path = dir </> "file-matches.arboricx" 2286 corpus = 2287 [ ("./test/fork.tri", parserOk (Fork Leaf Leaf) Leaf) 2288 , ("./test/comments-1.tri", 2289 parserOk (Fork (Stem Leaf) Leaf) Leaf) 2290 , ("./test/map.tri", parserOk trueT Leaf) 2291 , -- Optimized unit lowering with constructor-K-compiled y/triage. 2292 -- Independently counted the emitted size tree on the host: 500 2293 -- nodes (formerly 680 with the old support representations). 2294 ("./test/size.tri", parserOk (ofNumber 500) Leaf) 2295 ] 2296 packCompilerFile 2297 "./test/sourceGateFileCompiler.tri" path [pack "matchesFile"] 2298 bytes <- BS.readFile path 2299 case decodeBundle bytes of 2300 Left diagnostic -> assertFailure diagnostic 2301 Right bundle -> do 2302 let artifact = reconstructBundleTerms (bundleNodes bundle) 2303 V.! fromIntegral (head (bundleRoots bundle)) 2304 forM_ corpus $ \(fixture, expected) -> do 2305 source <- readFile fixture 2306 apply (apply artifact (ofString source)) expected @?= trueT 2307 , testCase "self-built local binding compiler executes through its wrapper" $ do 2308 env <- evaluateCompilerFile 2309 "./test/sourceGateLocalImport.tri" 2310 Map.lookup "main" env @?= Just (parserOk Leaf Leaf) 2311 , testCase "independent program goldens match the Haskell bundle" $ 2312 withSystemTempDirectory "gate-unit-goldens" $ \dir -> do 2313 let path = dir </> "program-matches.arboricx" 2314 packCompilerFile 2315 "./test/sourceGateUnitCompiler.tri" path [pack "matchesExpected"] 2316 bytes <- BS.readFile path 2317 goldenEnv <- evaluateCompilerFile "./test/sourceGateExpected.tri" 2318 let name = gateToken 2 (ofString "f") 2319 (gatePosition 2 1 6) (gatePosition 2 2 7) 2320 body = gateAst 1 Leaf 2321 (gatePosition 2 5 10) (gatePosition 2 6 11) 2322 cycleError = parserErr 2323 (portableDiagnostic (ofNumber 31) (ofString "acyclic") 2324 (ofString "a") (gatePosition 1 1 0) 2325 (ofList [ofString "a"])) Leaf 2326 cases = 2327 [ ("main = id 42\nid x = x", "goldenFortyTwo", 2328 parserOk (ofNumber 42) Leaf) 2329 , ("id x = x\nid 42", "goldenFortyTwo", 2330 parserOk (ofNumber 42) Leaf) 2331 , ("main = 2\n3", "goldenTwo", parserOk (ofNumber 2) Leaf) 2332 , ("main = [(id 2)]\nid x = x", "goldenProgramList", 2333 parserOk (ofList [ofNumber 2]) Leaf) 2334 , ("helper = t", "goldenMissingRoot", parserErr 2335 (portableDiagnostic (ofNumber 32) (ofString "main") 2336 Leaf Leaf Leaf) Leaf) 2337 , ("a = b\nb = a\nmain = t", "goldenProgramCycle", cycleError) 2338 , ("f = t\nf = t", "goldenProgramDuplicate", parserErr 2339 (portableDiagnostic (ofNumber 30) (ofString "f") 2340 (ofString "f") (gatePosition 2 1 6) Leaf) 2341 (pairT name body)) 2342 , ("main = u", "goldenProgramUnknown", parserErr 2343 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 2344 (gatePosition 1 8 7) Leaf) 2345 (gateAst 2 (ofString "u") 2346 (gatePosition 1 8 7) (gatePosition 1 9 8))) 2347 , ("unused = (x : x x) (x : x x)\nmain = t", "goldenLeaf", 2348 parserOk Leaf Leaf) 2349 , ("main = let x = t in x", "goldenLeaf", parserOk Leaf Leaf) 2350 , ("main = x where x = t", "goldenLeaf", parserOk Leaf Leaf) 2351 , ("main = let id x = x in id 2", "goldenTwo", 2352 parserOk (ofNumber 2) Leaf) 2353 , ("main = value where value = helper t\nhelper x = x", 2354 "goldenLeaf", parserOk Leaf Leaf) 2355 , ("main = let x = x in x", "goldenLocalMissing", parserErr 2356 (portableDiagnostic (ofNumber 20) (ofString "x") Leaf 2357 (gatePosition 1 16 15) Leaf) 2358 (gateAst 2 (ofString "x") 2359 (gatePosition 1 16 15) (gatePosition 1 17 16))) 2360 , ("main = let x = t x", "goldenLetMissingIn", parserErr 2361 (portableDiagnostic (ofNumber 2) (ofNumber 13) 2362 Leaf Leaf Leaf) Leaf) 2363 , ("main = (t\n t)", "goldenMultilineGroup", 2364 parserOk (Stem Leaf) Leaf) 2365 , ("main = [(t\n t)]", "goldenMultilineList", 2366 parserOk (ofList [Stem Leaf]) Leaf) 2367 , ("main = (x\n where x = t)", "goldenLeaf", 2368 parserOk Leaf Leaf) 2369 , ("main =\n t", "goldenLeaf", parserOk Leaf Leaf) 2370 , ("main =\nt", "goldenLeaf", parserOk Leaf Leaf) 2371 , ("main =\n (x : x)\n 2", "goldenTwo", 2372 parserOk (ofNumber 2) Leaf) 2373 , ("main =\n id\n t\nid x = x", "goldenLeaf", 2374 parserOk Leaf Leaf) 2375 , ("main = value\nwhere value = t", "goldenLeaf", 2376 parserOk Leaf Leaf) 2377 , ("main =\n value\n where value = t", "goldenLeaf", 2378 parserOk Leaf Leaf) 2379 , ("main =\n", "goldenMissingDefinitionBody", parserErr 2380 (portableDiagnostic (ofNumber 40) 2381 (ofString "definition body") Leaf Leaf Leaf) Leaf) 2382 , ("main = let x =\n 2\nin x", "goldenTwo", 2383 parserOk (ofNumber 2) Leaf) 2384 , ("main = let x = t in\n x", "goldenLeaf", 2385 parserOk Leaf Leaf) 2386 , ("main = let\n id x =\n x\nin id 2", "goldenTwo", 2387 parserOk (ofNumber 2) Leaf) 2388 , ("main =\n let x =\n t\n in x", "goldenLeaf", 2389 parserOk Leaf Leaf) 2390 , ("main = t where x =\n 2", "goldenLeaf", 2391 parserOk Leaf Leaf) 2392 , ("main = id 2 where\n id x =\n x", "goldenTwo", 2393 parserOk (ofNumber 2) Leaf) 2394 , ("main = let x =\n (y : y)\n 2\nin x", "goldenTwo", 2395 parserOk (ofNumber 2) Leaf) 2396 , ("main = let id x = x in\n id\n 2", "goldenTwo", 2397 parserOk (ofNumber 2) Leaf) 2398 , ("main = let x =\n", "goldenMissingLocalValue", parserErr 2399 (portableDiagnostic (ofNumber 40) 2400 (ofString "local binding value") Leaf Leaf Leaf) Leaf) 2401 , ("main = let x = t in\n", "goldenMissingLocalBody", parserErr 2402 (portableDiagnostic (ofNumber 40) 2403 (ofString "local binding body") Leaf Leaf Leaf) Leaf) 2404 , ("main = 2 |> (x : x)", "goldenTwo", 2405 parserOk (ofNumber 2) Leaf) 2406 , ("main = (x : x) <| 2", "goldenTwo", 2407 parserOk (ofNumber 2) Leaf) 2408 , ("main = 2 |> (x : x) |> (x : x)", "goldenTwo", 2409 parserOk (ofNumber 2) Leaf) 2410 , ("main = t <| t t", "goldenPipeApplication", 2411 parserOk (Stem (Stem Leaf)) Leaf) 2412 , ("main = 2 |> id\nid x = x", "goldenTwo", 2413 parserOk (ofNumber 2) Leaf) 2414 , ("main = 2 |> id where id x = x", "goldenTwo", 2415 parserOk (ofNumber 2) Leaf) 2416 , ("main = 2 |> missing", "goldenPipeUnknown", parserErr 2417 (portableDiagnostic (ofNumber 20) (ofString "missing") 2418 Leaf (gatePosition 1 13 12) Leaf) 2419 (gateAst 2 (ofString "missing") 2420 (gatePosition 1 13 12) (gatePosition 1 20 19))) 2421 , ("main = 2 |>", "goldenPipeMissingOperand", parserErr 2422 (portableDiagnostic (ofNumber 2) (ofNumber 7) 2423 Leaf Leaf Leaf) Leaf) 2424 , ("f = 2\nf |> let id x = x in id", "goldenTwo", 2425 parserOk (ofNumber 2) Leaf) 2426 , ("main = 2\n |> (x : x)", "goldenTwo", 2427 parserOk (ofNumber 2) Leaf) 2428 , ("main = 2 |>\n (x : x)", "goldenTwo", 2429 parserOk (ofNumber 2) Leaf) 2430 , ("main = do bind\n x <- 2\n x\nbind a f = f a", 2431 "goldenTwo", parserOk (ofNumber 2) Leaf) 2432 , ("main = do unused\n 2", "goldenTwo", 2433 parserOk (ofNumber 2) Leaf) 2434 , ("main = do bind\n 1\n 2\nbind a f = f a", 2435 "goldenTwo", parserOk (ofNumber 2) Leaf) 2436 , ("main = do bind\n x <- 2\n y <- x\n y\nbind a f = f a", 2437 "goldenTwo", parserOk (ofNumber 2) Leaf) 2438 , ("main = do bind\n x <- 2\n do bind\n y <- x\n y\nbind a f = f a", 2439 "goldenTwo", parserOk (ofNumber 2) Leaf) 2440 , ("main = do bind\n x <- (y : y)\n 2\n x\nbind a f = f a", 2441 "goldenTwo", parserOk (ofNumber 2) Leaf) 2442 , ("main = let _ = 42 in do bind\n 1\n _\nbind a f = f a", 2443 "goldenOne", parserOk (ofNumber 1) Leaf) 2444 , ("main = do bind\n x <- 2", "goldenDoMissingFinal", parserErr 2445 (portableDiagnostic (ofNumber 40) 2446 (ofString "do final expression") Leaf 2447 (gatePosition 2 3 17) Leaf) Leaf) 2448 , ("main = do bind\n x <- missing\n x\nbind a f = f a", 2449 "goldenDoUnboundAction", parserErr 2450 (portableDiagnostic (ofNumber 20) (ofString "missing") 2451 Leaf (gatePosition 2 8 22) Leaf) 2452 (gateAst 2 (ofString "missing") 2453 (gatePosition 2 8 22) (gatePosition 2 15 29))) 2454 ] 2455 case decodeBundle bytes of 2456 Left diagnostic -> assertFailure diagnostic 2457 Right bundle -> do 2458 let artifact = reconstructBundleTerms (bundleNodes bundle) 2459 V.! fromIntegral (head (bundleRoots bundle)) 2460 forM_ cases $ \(source, goldenName, expected) -> do 2461 Map.lookup goldenName goldenEnv @?= Just expected 2462 assertBool ("program golden mismatch: " ++ goldenName 2463 ++ " for " ++ show source) 2464 (apply (apply artifact (ofString source)) expected == trueT) 2465 case Map.lookup "goldenProgramCycleWrongPosition" goldenEnv of 2466 Nothing -> assertFailure "missing program wrong-position control" 2467 Just wrong -> 2468 apply (apply artifact (ofString "a = b\nb = a\nmain = t")) wrong 2469 @?= falseT 2470 , testCase "output goldens match the portable Haskell bundle" $ 2471 withSystemTempDirectory "gate-unit-output-goldens" $ \dir -> do 2472 let path = dir </> "output-matches.arboricx" 2473 packCompilerFile 2474 "./test/sourceGateUnitCompiler.tri" path [pack "matchesOutput"] 2475 bytes <- BS.readFile path 2476 goldenEnv <- evaluateCompilerFile "./test/sourceGateExpected.tri" 2477 case decodeBundle bytes of 2478 Left diagnostic -> assertFailure diagnostic 2479 Right bundle -> do 2480 let artifact = reconstructBundleTerms (bundleNodes bundle) 2481 V.! fromIntegral (head (bundleRoots bundle)) 2482 cases = 2483 [ ("main = value\nvalue = t", "goldenOutputForward", 2484 parserOk (pairT Leaf (ofList 2485 [ pairT (ofString "value") Leaf 2486 , pairT (ofString "main") Leaf 2487 ])) Leaf) 2488 , ("2", "goldenOutputExpression", 2489 parserOk (pairT (ofNumber 2) Leaf) Leaf) 2490 ] 2491 forM_ cases $ \(source, goldenName, expected) -> do 2492 Map.lookup goldenName goldenEnv @?= Just expected 2493 apply (apply artifact (ofString source)) expected @?= trueT 2494 , testCase "output includes root and definitions in dependency order" $ do 2495 let source = "main = value\nvalue = t" 2496 output = foldl apply (gateUnitEnv Map.! "compileGateProgramOutput") [Leaf, ofString source] 2497 definitions = ofList 2498 [ pairT (ofString "value") Leaf 2499 , pairT (ofString "main") Leaf 2500 ] 2501 output @?= parserOk (pairT Leaf definitions) Leaf 2502 check Leaf source (parserOk Leaf Leaf) 2503 , testCase "empty output environment still carries the selected root" $ 2504 foldl apply (gateUnitEnv Map.! "compileGateProgramOutput") [Leaf, ofString "t"] @?= 2505 parserOk (pairT Leaf Leaf) Leaf 2506 , testCase "output entrypoint preserves structured diagnostics" $ 2507 foldl apply (gateUnitEnv Map.! "compileGateProgramOutput") [Leaf, ofString "helper = t"] @?= 2508 parserErr (portableDiagnostic (ofNumber 32) 2509 (ofString "main") Leaf Leaf Leaf) Leaf 2510 , testCase "public compileSource normalizes with abstract environment and source" $ do 2511 env <- evaluateCompilerFile 2512 "./test/sourcePublicCompiler.tri" 2513 Map.member "matchesSource" env @?= True 2514 let check environment source expected = do 2515 foldl apply (fixtureExports "source.unit" Map.! "compileSource") 2516 [environment, ofString source] @?= expected 2517 foldl apply (env Map.! "matchesSource") 2518 [environment, ofString source, expected] @?= trueT 2519 assertBool "test wrapper leaked imported compiler" (Map.notMember "compileSource" env) 2520 check Leaf "main = value\nvalue = t" 2521 (parserOk (pairT Leaf (ofList 2522 [pairT (ofString "value") Leaf, pairT (ofString "main") Leaf])) Leaf) 2523 check (ofList [Fork (ofString "host") (Stem Leaf)]) "host" 2524 (parserOk (pairT (Stem Leaf) Leaf) Leaf) 2525 check Leaf "helper = t" 2526 (parserErr (portableDiagnostic (ofNumber 32) 2527 (ofString "main") Leaf Leaf Leaf) Leaf) 2528 foldl apply (env Map.! "matchesSource") 2529 [Leaf, ofString "main = t", parserOk (pairT (Stem Leaf) Leaf) Leaf] @?= falseT 2530 , testCase "public compileSource is an exportable two-argument bundle" $ 2531 withSystemTempDirectory "public-source-artifact" $ \dir -> do 2532 let path = dir </> "compile-source.arboricx" 2533 packCompilerFile 2534 "./lib/source/unit.tri" path [pack "compileSource"] 2535 bytes <- BS.readFile path 2536 case decodeBundle bytes of 2537 Left diagnostic -> assertFailure diagnostic 2538 Right bundle -> do 2539 let artifact = reconstructBundleTerms (bundleNodes bundle) 2540 V.! fromIntegral (head (bundleRoots bundle)) 2541 cases = 2542 [ (Leaf, "2", parserOk (pairT (ofNumber 2) Leaf) Leaf) 2543 , (Leaf, "main = value\nvalue = t", parserOk 2544 (pairT Leaf (ofList [pairT (ofString "value") Leaf, 2545 pairT (ofString "main") Leaf])) Leaf) 2546 , (ofList [pairT (ofString "host") (Stem Leaf)], 2547 "host", parserOk (pairT (Stem Leaf) Leaf) Leaf) 2548 , (Leaf, "helper = t", parserErr 2549 (portableDiagnostic (ofNumber 32) 2550 (ofString "main") Leaf Leaf Leaf) Leaf) 2551 , (Leaf, "#", parserErr 2552 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 35) 2553 (gatePosition 1 1 0) Leaf) (ofString "#")) 2554 , (Leaf, "!import \"x\" NS", parserErr 2555 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 33) 2556 (gatePosition 1 1 0) Leaf) 2557 (ofString "!import \"x\" NS")) 2558 ] 2559 forM_ cases $ \(environment, source, expected) -> do 2560 let compiled = apply (apply artifact environment) (ofString source) 2561 compiled @?= expected 2562 -- Repeated fixed inputs have structurally identical complete 2563 -- outputs, not merely the same selected root. 2564 apply (apply artifact environment) (ofString source) @?= compiled 2565 forM_ 2566 [ ("./test/fork.tri", Fork Leaf Leaf) 2567 , ("./test/comments-1.tri", Fork (Stem Leaf) Leaf) 2568 ] $ \(fixture, root) -> do 2569 source <- readFile fixture 2570 apply (apply artifact Leaf) (ofString source) @?= 2571 parserOk (pairT root 2572 (ofList [pairT (ofString "main") root])) Leaf 2573 let externalNames = ["map", "append", "equal?", "head", "y", "triage"] 2574 external = ofList 2575 [ pairT (ofString name) (fixtureExports "base" Map.! name) 2576 | name <- externalNames ] 2577 stringSource <- readFile "./test/string.tri" 2578 apply (apply artifact external) (ofString stringSource) @?= 2579 parserOk (pairT (ofString "String test!") Leaf) Leaf 2580 mapSource <- readFile "./test/map.tri" 2581 apply (apply artifact external) (ofString mapSource) @?= 2582 parserOk (pairT trueT (ofList 2583 [ pairT (ofString "x") 2584 (ofList [ofString "Successfully concatenated two strings!"]) 2585 , pairT (ofString "main") trueT 2586 ])) Leaf 2587 , testCase "public bundle functions agree by application, not tree shape" $ 2588 withSystemTempDirectory "public-source-functions" $ \dir -> do 2589 let path = dir </> "compile-source.arboricx" 2590 packCompilerFile 2591 "./lib/source/unit.tri" path [pack "compileSource"] 2592 bytes <- BS.readFile path 2593 comparatorEnv <- evaluateCompilerFile "./test/sourcePublicCompiler.tri" 2594 case decodeBundle bytes of 2595 Left diagnostic -> assertFailure diagnostic 2596 Right bundle -> do 2597 let compiler = reconstructBundleTerms (bundleNodes bundle) 2598 V.! fromIntegral (head (bundleRoots bundle)) 2599 comparator = comparatorEnv Map.! "matchesSourceApplicationArtifact" 2600 cases = 2601 [ ("main x = x", ofNumber 2, ofNumber 2) 2602 , ("main x = t", Stem Leaf, Leaf) 2603 , ("main x = let id a = a in id x", Stem Leaf, Stem Leaf) 2604 , ("main x = do bind\n y <- x\n y\nbind a f = f a", 2605 ofNumber 2, ofNumber 2) 2606 ] 2607 forM_ cases $ \(source, input, expected) -> do 2608 foldl apply comparator 2609 [compiler, Leaf, ofString source, input, expected] @?= trueT 2610 let rootComparator = comparatorEnv Map.! "matchesSourceRootArtifact" 2611 unused = "unused = (x : x x) (x : x x)\nmain = t" 2612 unusedRoot <- Timeout.timeout (30 * 1000000) $ 2613 evaluate (foldl apply rootComparator 2614 [compiler, Leaf, ofString unused, Leaf] == trueT) 2615 unusedRoot @?= Just True 2616 , testCase "self-built compiler wrapper handles a forward reference" $ do 2617 env <- evaluateCompilerFile 2618 "./test/sourceGateUnitImport.tri" 2619 Map.lookup "main" env @?= Just (parserOk Leaf Leaf) 2620 , testCase "forward definitions with arguments evaluate before main" $ do 2621 let source = "main = id 42\nid x = x" 2622 check Leaf source (parserOk (ofNumber 42) Leaf) 2623 , testCase "final expression selects root in absence of main" $ 2624 check Leaf "id x = x\nid t" (parserOk Leaf Leaf) 2625 , testCase "a local main wins over a final expression" $ 2626 check Leaf "main = 2\n3" (parserOk (ofNumber 2) Leaf) 2627 , testCase "provided environment is visible inside definitions" $ 2628 check (ofList [Fork (ofString "host") (Stem Leaf)]) "main = host t" 2629 (parserOk (Fork Leaf Leaf) Leaf) 2630 , testCase "multiple definition arguments preserve binder order" $ 2631 let source = "main x y = x" 2632 program = foldl apply (gateUnitEnv Map.! "compileGateProgram") [Leaf, ofString source] 2633 in case program of 2634 Fork tag (Fork fun Leaf) | tag == trueT -> 2635 apply (apply fun Leaf) (Stem Leaf) @?= Leaf 2636 other -> assertFailure ("Expected compiled two-argument function, got " ++ show other) 2637 , testCase "recursive top-level function and triage compile from source" $ do 2638 let source = "triage leaf stem fork = t (t leaf stem) fork\n" 2639 ++ "main xs = y (self : rest : triage t (h : self h) (a : b : self a) rest) xs" 2640 external = ofList [Fork (ofString "y") (gateCompileBaseEnv Map.! "y")] 2641 compiled = foldl apply (gateUnitEnv Map.! "compileGateProgram") [external, ofString source] 2642 case compiled of 2643 Fork tag (Fork fun Leaf) | tag == trueT -> do 2644 apply fun Leaf @?= Leaf 2645 apply fun (Stem Leaf) @?= Leaf 2646 apply fun (Fork (Stem Leaf) Leaf) @?= Leaf 2647 other -> assertFailure ("Expected recursive source function, got " ++ show other) 2648 , testCase "recursive function, y, and triage compile entirely from source" $ do 2649 let source = "y = ((mut : wait : fun : wait mut (x : fun (wait mut x))) " 2650 ++ "(x : x x) (a0 : a1 : a2 : t (t a0) (t t a2) a1))\n" 2651 ++ "triage leaf stem fork = t (t leaf stem) fork\n" 2652 ++ "main xs = y (self : rest : triage t (h : self h) " 2653 ++ "(a : b : self a) rest) xs" 2654 compiled = foldl apply (gateUnitEnv Map.! "compileGateProgram") [Leaf, ofString source] 2655 case compiled of 2656 Fork tag (Fork fun Leaf) | tag == trueT -> do 2657 apply fun Leaf @?= Leaf 2658 apply fun (Stem Leaf) @?= Leaf 2659 apply fun (Fork (Stem Leaf) Leaf) @?= Leaf 2660 other -> assertFailure ("Expected fully sourced recursive function, got " ++ show other) 2661 , testCase "indented definition bodies preserve continuation layout" $ do 2662 let sources = 2663 [ ("main =\n t", Leaf) 2664 , ("main =\nt", Leaf) 2665 , ("main =\n (x : x)\n 2", ofNumber 2) 2666 , ("main =\n id\n t\nid x = x", Leaf) 2667 , ("main = value\nwhere value = t", Leaf) 2668 , ("main =\n value\n where value = t", Leaf) 2669 ] 2670 forM_ sources $ \(source, value) -> do 2671 check Leaf source (parserOk value Leaf) 2672 , testCase "multiline grouped applications retain expected results" $ do 2673 let sources = 2674 [ ("main = (t\n t)", Stem Leaf) 2675 , ("main = [(t\n t)]", ofList [Stem Leaf]) 2676 , ("main = (let x = t in x\n t)", Stem Leaf) 2677 , ("main = (x\n where x = t)", Leaf) 2678 ] 2679 forM_ sources $ \(source, value) -> do 2680 check Leaf source (parserOk value Leaf) 2681 , testCase "an unindented do block has a located diagnostic" $ 2682 let diagnostic = portableDiagnostic (ofNumber 40) 2683 (ofString "indented do block") (ofNumber 2) 2684 (gatePosition 2 1 15) Leaf 2685 reply = apply (gateProgramEnv Map.! "parseGateProgram") 2686 (ofString "main = do bind\nx <- 2\n x") 2687 in apply (fixtureExports "parser" Map.! "parserReplyPayload") reply @?= diagnostic 2688 , testCase "pipe followed by a local assignment stays a final expression" $ 2689 check Leaf "f = 2\nf |> let id x = x in id" 2690 (parserOk (ofNumber 2) Leaf) 2691 , testCase "explicit-bind do with nested statements retains expected results" $ do 2692 let sources = 2693 [ "main = do bind\n x <- 2\n x\nbind a f = f a" 2694 , "main = do unused\n 2" 2695 , "main = do bind\n 1\n 2\nbind a f = f a" 2696 , "main = do bind\n x <- 2\n y <- x\n y\nbind a f = f a" 2697 , "main = do bind\n x <- 2\n do bind\n y <- x\n y\nbind a f = f a" 2698 ] 2699 forM_ sources $ \source -> do 2700 check Leaf source (parserOk (ofNumber 2) Leaf) 2701 , testCase "pipe directions, precedence, and local names retain expected results" $ do 2702 let sources = 2703 [ ("main = 2 |> (x : x)", ofNumber 2) 2704 , ("main = (x : x) <| 2", ofNumber 2) 2705 , ("main = 2 |> (x : x) |> (x : x)", ofNumber 2) 2706 , ("main = t <| t t", Stem (Stem Leaf)) 2707 , ("main = 2 |> id\nid x = x", ofNumber 2) 2708 , ("main = 2 |> id where id x = x", ofNumber 2) 2709 , ("main = 2\n |> (x : x)", ofNumber 2) 2710 , ("main = 2 |>\n (x : x)", ofNumber 2) 2711 ] 2712 forM_ sources $ \(source, value) -> do 2713 check Leaf source (parserOk value Leaf) 2714 , testCase "multiline local let/where layout retains expected results" $ do 2715 let sources = 2716 [ ("main = let x =\n 2\nin x", ofNumber 2) 2717 , ("main = let x = t in\n x", Leaf) 2718 , ("main = let\n id x =\n x\nin id 2", ofNumber 2) 2719 , ("main =\n let x =\n t\n in x", Leaf) 2720 , ("main = t where x =\n 2", Leaf) 2721 , ("main = id 2 where\n id x =\n x", ofNumber 2) 2722 , ("main = let x =\n (y : y)\n 2\nin x", ofNumber 2) 2723 , ("main = let id x = x in\n id\n 2", ofNumber 2) 2724 ] 2725 forM_ sources $ \(source, value) -> do 2726 check Leaf source (parserOk value Leaf) 2727 , testCase "local bindings preserve lexical scope and argument order" $ do 2728 let sources = 2729 [ ("main = let x = 2 in x", ofNumber 2) 2730 , ("main = let id x = x in id 2", ofNumber 2) 2731 , ("main = id 2 where id x = x", ofNumber 2) 2732 , ("result where result = t", Leaf) 2733 , ("main = let x = t in (y : x) 2", Leaf) 2734 ] 2735 forM_ sources $ \(source, value) -> do 2736 check Leaf source (parserOk value Leaf) 2737 , testCase "local binding value has no self-reference" $ 2738 check Leaf "main = let x = x in x" (parserErr 2739 (portableDiagnostic (ofNumber 20) (ofString "x") Leaf 2740 (gatePosition 1 16 15) Leaf) 2741 (gateAst 2 (ofString "x") 2742 (gatePosition 1 16 15) (gatePosition 1 17 16))) 2743 , testCase "a local binder shadows a top-level definition" $ 2744 check Leaf "main = local where local = t\nlocal = 2" 2745 (parserOk Leaf Leaf) 2746 , testCase "local free names order forward-referenced definitions" $ 2747 check Leaf "main = value where value = helper t\nhelper x = x" 2748 (parserOk Leaf Leaf) 2749 , testCase "lists may refer to forward definitions" $ 2750 check Leaf "main = [(id 2)]\nid x = x" 2751 (parserOk (ofList [ofNumber 2]) Leaf) 2752 , testCase "unused divergent body is checked but not demanded" $ do 2753 let source = "unused = (x : x x) (x : x x)\nmain = t" 2754 check Leaf source (parserOk Leaf Leaf) 2755 , testCase "missing multiline definition body is a structured diagnostic" $ 2756 check Leaf "main =\n" (parserErr 2757 (portableDiagnostic (ofNumber 40) (ofString "definition body") 2758 Leaf Leaf Leaf) Leaf) 2759 , testCase "missing local value or body returns a structured diagnostic" $ do 2760 check Leaf "main = let x =\n" (parserErr 2761 (portableDiagnostic (ofNumber 40) 2762 (ofString "local binding value") Leaf Leaf Leaf) Leaf) 2763 check Leaf "main = let x = t in\n" (parserErr 2764 (portableDiagnostic (ofNumber 40) 2765 (ofString "local binding body") Leaf Leaf Leaf) Leaf) 2766 , testCase "no root is a structured diagnostic" $ 2767 check Leaf "helper = t" (parserErr 2768 (portableDiagnostic (ofNumber 32) (ofString "main") Leaf Leaf Leaf) 2769 Leaf) 2770 , testCase "cycle fails before result selection" $ 2771 check Leaf "a = b\nb = a\nmain = t" (parserErr 2772 (portableDiagnostic (ofNumber 31) (ofString "acyclic") 2773 (ofString "a") (gatePosition 1 1 0) 2774 (ofList [ofString "a"])) Leaf) 2775 ] 2776 where 2777 check external source expected = 2778 foldl apply (gateUnitEnv Map.! "compileGateProgram") [external, ofString source] 2779 @?= expected 2780 2781 -- -------------------------------------------------------------------------- 2782 -- Single-expression feasibility-gate name resolution 2783 -- -------------------------------------------------------------------------- 2784 2785 gateResolveEnv :: Env 2786 gateResolveEnv = fixtureExports "source.resolve" 2787 {-# NOINLINE gateResolveEnv #-} 2788 2789 gateCore :: Integer -> T -> T -> T -> T 2790 gateCore = gateAst 2791 2792 gateResolutionTests :: TestTree 2793 gateResolutionTests = testGroup "Feasibility-gate resolution" 2794 [ testCase "self-built resolver export executes through its wrapper" $ do 2795 env <- evaluateCompilerFile "./test/sourceGateResolveImport.tri" 2796 let bound = gateCore 2 (ofNumber 0) (gatePosition 1 5 4) (gatePosition 1 6 5) 2797 lam = gateCore 4 bound (gatePosition 1 1 0) (gatePosition 1 6 5) 2798 Map.lookup "main" env @?= Just (parserOk lam Leaf) 2799 , testCase "nearest lexical binder has index zero" $ 2800 let bound = gateCore 2 (ofNumber 0) (gatePosition 1 5 4) (gatePosition 1 6 5) 2801 in check Leaf "x : x" (parserOk 2802 (gateCore 4 bound (gatePosition 1 1 0) (gatePosition 1 6 5)) Leaf) 2803 , testCase "outer lexical binder has index one" $ 2804 let bound = gateCore 2 (ofNumber 1) (gatePosition 1 9 8) (gatePosition 1 10 9) 2805 inner = gateCore 4 bound (gatePosition 1 5 4) (gatePosition 1 10 9) 2806 in check Leaf "x : y : x" (parserOk 2807 (gateCore 4 inner (gatePosition 1 1 0) (gatePosition 1 10 9)) Leaf) 2808 , testCase "local binding lowers to an applied scoped lambda" $ 2809 let value = gateCore 1 Leaf 2810 (gatePosition 1 9 8) (gatePosition 1 10 9) 2811 bound = gateCore 2 (ofNumber 0) 2812 (gatePosition 1 14 13) (gatePosition 1 15 14) 2813 scoped = gateCore 4 bound 2814 (gatePosition 1 1 0) (gatePosition 1 15 14) 2815 in check Leaf "let x = t in x" (parserOk 2816 (gateCore 3 (pairT scoped value) 2817 (gatePosition 1 1 0) (gatePosition 1 15 14)) Leaf) 2818 , testCase "forward pipe elaborates to reversed core application" $ 2819 let function = gateCore 5 (Stem Leaf) 2820 (gatePosition 1 6 5) (gatePosition 1 7 6) 2821 argument = gateCore 6 (ofNumber 2) 2822 (gatePosition 1 1 0) (gatePosition 1 2 1) 2823 in check (ofList [Fork (ofString "f") (Stem Leaf)]) "2 |> f" (parserOk 2824 (gateCore 3 (pairT function argument) 2825 (gatePosition 1 1 0) (gatePosition 1 7 6)) Leaf) 2826 , testCase "number becomes a resolved tree literal" $ 2827 check Leaf "42" (parserOk 2828 (gateCore 6 (ofNumber 42) (gatePosition 1 1 0) (gatePosition 1 3 2)) Leaf) 2829 , testCase "string becomes a resolved tree literal" $ 2830 check Leaf "\"hi\"" (parserOk 2831 (gateCore 7 (ofString "hi") (gatePosition 1 1 0) (gatePosition 1 5 4)) Leaf) 2832 , testCase "list elements become individually resolved core nodes" $ 2833 let leaf = gateCore 1 Leaf (gatePosition 1 2 1) (gatePosition 1 3 2) 2834 number = gateCore 6 (ofNumber 2) 2835 (gatePosition 1 4 3) (gatePosition 1 5 4) 2836 in check Leaf "[t 2]" (parserOk 2837 (gateCore 8 (ofList [leaf, number]) 2838 (gatePosition 1 1 0) (gatePosition 1 6 5)) Leaf) 2839 , testCase "unresolved list element preserves its own span" $ 2840 check Leaf "[u]" (parserErr 2841 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 2842 (gatePosition 1 2 1) Leaf) 2843 (gateAst 2 (ofString "u") 2844 (gatePosition 1 2 1) (gatePosition 1 3 2))) 2845 , testCase "supplied external tree becomes a core value" $ 2846 let external = gateCore 5 (Stem Leaf) 2847 (gatePosition 1 1 0) (gatePosition 1 2 1) 2848 leaf = gateCore 1 Leaf (gatePosition 1 3 2) (gatePosition 1 4 3) 2849 in check (ofList [Fork (ofString "f") (Stem Leaf)]) "f t" (parserOk 2850 (gateCore 3 (pairT external leaf) 2851 (gatePosition 1 1 0) (gatePosition 1 4 3)) Leaf) 2852 , testCase "lexical binder shadows an external value" $ 2853 let bound = gateCore 2 (ofNumber 0) (gatePosition 1 5 4) (gatePosition 1 6 5) 2854 in check (ofList [Fork (ofString "x") (Stem Leaf)]) "x : x" (parserOk 2855 (gateCore 4 bound (gatePosition 1 1 0) (gatePosition 1 6 5)) Leaf) 2856 , testCase "unresolved name reports its original position" $ 2857 check Leaf "u" (parserErr 2858 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 2859 (gatePosition 1 1 0) Leaf) 2860 (gateAst 2 (ofString "u") (gatePosition 1 1 0) (gatePosition 1 2 1))) 2861 , testCase "grouping carries its span into the core" $ 2862 check Leaf "(t)" (parserOk 2863 (gateCore 1 Leaf (gatePosition 1 1 0) (gatePosition 1 4 3)) Leaf) 2864 ] 2865 where 2866 check environment input expected = 2867 foldl apply (gateResolveEnv Map.! "resolveGateSource") [environment, ofString input] @?= expected 2868 2869 -- -------------------------------------------------------------------------- 2870 -- Reference S/K/I compiler for the feasibility gate 2871 -- -------------------------------------------------------------------------- 2872 2873 gateCompileEnv :: Env 2874 gateCompileEnv = fixtureExports "source.compile" 2875 {-# NOINLINE gateCompileEnv #-} 2876 2877 gateCompileBaseEnv :: Env 2878 gateCompileBaseEnv = fixtureExports "base" 2879 {-# NOINLINE gateCompileBaseEnv #-} 2880 2881 gateCompilationTests :: TestTree 2882 gateCompilationTests = testGroup "Feasibility-gate source compiler" 2883 [ testCase "Haskell reducer executes the exported conformance artifact" $ 2884 withSystemTempDirectory "gate-conforms-bundle" $ \dir -> do 2885 let path = dir </> "conforms.arboricx" 2886 packCompilerFile "./test/sourceGateCompiler.tri" 2887 path [pack "conforms"] 2888 bytes <- BS.readFile path 2889 case decodeBundle bytes of 2890 Left diagnostic -> assertFailure diagnostic 2891 Right bundle -> do 2892 let artifact = reconstructBundleTerms (bundleNodes bundle) 2893 V.! fromIntegral (head (bundleRoots bundle)) 2894 corpus = 2895 [ ("t", "t", trueT) 2896 , ("t t", "t", falseT) 2897 , ("t t t", "t t t", trueT) 2898 , ("(x : x) t", "t", trueT) 2899 , ("(x : t) (t t)", "t", trueT) 2900 , ("u", "u", trueT) 2901 , ("u", "t", falseT) 2902 , (nestedIdentity 16, "t", trueT) 2903 , (nestedBinders 16, "t", trueT) 2904 ] 2905 forM_ corpus $ \(source, expectedSource, expected) -> 2906 apply (apply artifact (ofString source)) (ofString expectedSource) 2907 @?= expected 2908 , testCase "independent golden results match the Haskell artifact" $ 2909 withSystemTempDirectory "gate-golden-bundle" $ \dir -> do 2910 let path = dir </> "matches.arboricx" 2911 packCompilerFile "./test/sourceGateCompiler.tri" 2912 path [pack "matchesExpected"] 2913 bytes <- BS.readFile path 2914 goldenEnv <- evaluateCompilerFile "./test/sourceGateExpected.tri" 2915 let unknown = parserErr 2916 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 2917 (gatePosition 1 1 0) Leaf) 2918 (gateAst 2 (ofString "u") (gatePosition 1 1 0) (gatePosition 1 2 1)) 2919 unsupported = parserErr 2920 (portableDiagnostic (ofNumber 4) Leaf (ofNumber 35) 2921 (gatePosition 1 1 0) Leaf) 2922 (ofString "#") 2923 badEscape = parserErr 2924 (portableDiagnostic (ofNumber 6) (ofNumber 34) (ofNumber 113) 2925 (gatePosition 1 3 2) Leaf) 2926 (ofString "\\q\"") 2927 unfinishedString = parserErr 2928 (portableDiagnostic (ofNumber 5) (ofNumber 34) Leaf 2929 (gatePosition 1 1 0) Leaf) Leaf 2930 unfinishedBlock = parserErr 2931 (portableDiagnostic (ofNumber 5) (ofString "-|") Leaf 2932 (gatePosition 1 1 0) Leaf) Leaf 2933 unknownInList = parserErr 2934 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 2935 (gatePosition 1 2 1) Leaf) 2936 (gateAst 2 (ofString "u") 2937 (gatePosition 1 2 1) (gatePosition 1 3 2)) 2938 missingListClose = parserErr 2939 (portableDiagnostic (ofNumber 2) (ofNumber 10) Leaf Leaf Leaf) 2940 Leaf 2941 comma = parserErr 2942 (portableDiagnostic (ofNumber 2) (ofNumber 10) (ofNumber 20) 2943 (gatePosition 1 3 2) Leaf) 2944 (ofList 2945 [ gateToken 20 Leaf (gatePosition 1 3 2) (gatePosition 1 4 3) 2946 , gateToken 1 (ofString "t") (gatePosition 1 5 4) (gatePosition 1 6 5) 2947 , gateToken 10 Leaf (gatePosition 1 6 5) (gatePosition 1 7 6) 2948 ]) 2949 golden = 2950 [ ("t", "goldenLeaf", parserOk Leaf Leaf) 2951 , ("t t", "goldenStem", parserOk (Stem Leaf) Leaf) 2952 , ("t t t", "goldenFork", parserOk (Fork Leaf Leaf) Leaf) 2953 , ("(x : x) t", "goldenLeaf", parserOk Leaf Leaf) 2954 , ("42", "goldenFortyTwo", parserOk (ofNumber 42) Leaf) 2955 , ("\"hi\"", "goldenHi", parserOk (ofString "hi") Leaf) 2956 , ("|-note-| \"hi\"", "goldenHi", parserOk (ofString "hi") Leaf) 2957 , ("u", "goldenUnknown", unknown) 2958 , ("#", "goldenUnsupported", unsupported) 2959 , ("\"\\q\"", "goldenBadEscape", badEscape) 2960 , ("\"hi", "goldenUnfinishedString", unfinishedString) 2961 , ("|-oops", "goldenUnfinishedBlock", unfinishedBlock) 2962 , ("[]", "goldenLeaf", parserOk Leaf Leaf) 2963 , ("[t 2]", "goldenListTwo", 2964 parserOk (ofList [Leaf, ofNumber 2]) Leaf) 2965 , ("[t\n2]", "goldenListTwo", 2966 parserOk (ofList [Leaf, ofNumber 2]) Leaf) 2967 , ("[[t] (t t)]", "goldenNestedList", 2968 parserOk (ofList [ofList [Leaf], Stem Leaf]) Leaf) 2969 , ("[u]", "goldenUnknownInList", unknownInList) 2970 , ("[t", "goldenMissingListClose", missingListClose) 2971 , ("[t, t]", "goldenComma", comma) 2972 ] 2973 case decodeBundle bytes of 2974 Left diagnostic -> assertFailure diagnostic 2975 Right bundle -> do 2976 let artifact = reconstructBundleTerms (bundleNodes bundle) 2977 V.! fromIntegral (head (bundleRoots bundle)) 2978 forM_ golden $ \(source, name, expected) -> do 2979 Map.lookup name goldenEnv @?= Just expected 2980 apply (apply artifact (ofString source)) expected @?= trueT 2981 case Map.lookup "goldenUnknownWrongPosition" goldenEnv of 2982 Nothing -> assertFailure "missing incorrect-position control" 2983 Just wrong -> do 2984 apply (apply artifact (ofString "u")) wrong @?= falseT 2985 apply (apply artifact (ofString "t t")) 2986 (parserOk Leaf Leaf) @?= falseT 2987 , testCase "self-built resolver export executes through its wrapper" $ do 2988 env <- evaluateCompilerFile "./test/sourceGateCompileImport.tri" 2989 Map.lookup "main" env @?= Just (parserOk Leaf Leaf) 2990 , testCase "leaf compiles to raw Leaf" $ 2991 check "t" (parserOk Leaf Leaf) 2992 , testCase "decimal literal compiles to the canonical number tree" $ do 2993 check "42" (parserOk (ofNumber 42) Leaf) 2994 check "000" (parserOk Leaf Leaf) 2995 , testCase "list element boundaries distinguish atoms from grouped applications" $ do 2996 check "[t t]" (parserOk (ofList [Leaf, Leaf]) Leaf) 2997 check "[(t t)]" (parserOk (ofList [Stem Leaf]) Leaf) 2998 check "[t\n2]" (parserOk (ofList [Leaf, ofNumber 2]) Leaf) 2999 , testCase "lists compile to canonical Fork cells" $ do 3000 check "[]" (parserOk Leaf Leaf) 3001 check "[t 2]" (parserOk (ofList [Leaf, ofNumber 2]) Leaf) 3002 check "[[t] (t t)]" (parserOk 3003 (ofList [ofList [Leaf], Stem Leaf]) Leaf) 3004 check "[t\n2]" (parserOk (ofList [Leaf, ofNumber 2]) Leaf) 3005 check "(x : [x]) t" (parserOk (ofList [Leaf]) Leaf) 3006 , testCase "string literal compiles to the canonical character list" $ do 3007 check "\"hi\"" (parserOk (ofString "hi") Leaf) 3008 check "(x : x) \"hi\"" (parserOk (ofString "hi") Leaf) 3009 , testCase "comments and literals compose without a special host lexer" $ do 3010 check "42 --ok" (parserOk (ofNumber 42) Leaf) 3011 check "|-note-| \"hi\"" (parserOk (ofString "hi") Leaf) 3012 , testCase "application evaluates to raw Stem and Fork" $ do 3013 check "t t" (parserOk (Stem Leaf) Leaf) 3014 check "t t t" (parserOk (Fork Leaf Leaf) Leaf) 3015 , testCase "identity behaves correctly under arguments" $ 3016 checkFunction "x : x" [ofNumber 5] (ofNumber 5) 3017 , testCase "constant behaves correctly under arguments" $ 3018 checkFunction "x : t" [ofNumber 5] Leaf 3019 , testCase "nested lambdas preserve the outer argument" $ 3020 checkFunction "x : y : x" [ofNumber 5, ofNumber 8] (ofNumber 5) 3021 , testCase "pruned bracket agrees with the retained reference" $ do 3022 let term kind payload = Fork (ofNumber kind) (Fork payload Leaf) 3023 var = term 2 Leaf 3024 leaf = term 1 Leaf 3025 app f x = term 3 (Fork f x) 3026 terms = [var, leaf, app var leaf, app leaf var, app var var] 3027 compiled method value = apply (gateCompileEnv Map.! "sourceRunTerm") 3028 (apply (gateCompileEnv Map.! method) value) 3029 forM_ terms $ \term -> 3030 case (compiled "sourceBracketReference" term, 3031 compiled "sourceBracket" term) of 3032 (Fork referenceTag (Fork reference Leaf), 3033 Fork optimizedTag (Fork optimized Leaf)) 3034 | referenceTag == trueT && optimizedTag == trueT -> 3035 forM_ [Leaf, Stem Leaf] $ \argument -> 3036 apply optimized argument @?= apply reference argument 3037 other -> assertFailure ("Expected two bracketed trees, got " ++ show other) 3038 , testCase "star eta and retained B/C reference agree when applied" $ do 3039 let compiled method source = 3040 case foldl apply (gateResolveEnv Map.! "resolveGateSource") [Leaf, ofString source] of 3041 Fork tag (Fork core Leaf) | tag == trueT -> apply (gateCompileEnv Map.! method) core 3042 other -> error ("reference corpus failed resolution: " ++ show other) 3043 corpus = 3044 [ ("x : x", [ofNumber 5], ofNumber 5) 3045 , ("x y : x", [ofNumber 5, ofNumber 8], ofNumber 5) 3046 , ("x y : y", [ofNumber 5, ofNumber 8], ofNumber 8) 3047 , ("x : x x", [Stem Leaf], Fork Leaf (Stem Leaf)) 3048 , ("f x : f x", [Stem Leaf, Leaf], Fork Leaf Leaf) 3049 , ("f x y : f (x y)", [Stem Leaf, Stem Leaf, Leaf], 3050 Fork Leaf (Fork Leaf Leaf)) 3051 , ("x : [x 2]", [Leaf], ofList [Leaf, ofNumber 2]) 3052 , ("a : (a : a)", [Leaf, Stem Leaf], Stem Leaf) 3053 ] 3054 forM_ corpus $ \(source, arguments, expected) -> 3055 forM_ ["sourceCompileCoreReference", "sourceCompileCore", 3056 "sourceCompileCoreKiselyov"] $ \method -> 3057 case compiled method source of 3058 Fork tag (Fork fn Leaf) | tag == trueT -> 3059 foldl apply fn arguments @?= expected 3060 other -> assertFailure ("Failed to compile " ++ method ++ " " 3061 ++ source ++ ": " ++ show other) 3062 , testCase "application inside a lambda behaves correctly" $ 3063 checkFunction "f : x : f x" [Stem Leaf, ofNumber 8] (Fork Leaf (ofNumber 8)) 3064 , testCase "external tree is applied without host lookup" $ 3065 foldl apply (gateCompileEnv Map.! "compileGateSource") 3066 [ofList [Fork (ofString "f") (Stem Leaf)], ofString "f t"] @?= 3067 parserOk (Fork Leaf Leaf) Leaf 3068 , testCase "backend compiles a recursive source function" $ do 3069 let source = "xs : y (self : rest : triage t (h : self h) (a : b : self a) rest) xs" 3070 external = ofList [Fork (ofString name) (gateCompileBaseEnv Map.! name) 3071 | name <- ["y", "triage"]] 3072 compiled = foldl apply (gateCompileEnv Map.! "compileGateSource") [external, ofString source] 3073 case compiled of 3074 Fork tag (Fork compiled Leaf) | tag == trueT -> do 3075 apply compiled Leaf @?= Leaf 3076 apply compiled (Stem Leaf) @?= Leaf 3077 apply compiled (Fork (Stem Leaf) Leaf) @?= Leaf 3078 other -> assertFailure ("Expected compiled recursive function, got " ++ show other) 3079 , testCase "unresolved name stays a diagnostic" $ 3080 check "u" (parserErr 3081 (portableDiagnostic (ofNumber 20) (ofString "u") Leaf 3082 (gatePosition 1 1 0) Leaf) 3083 (gateAst 2 (ofString "u") (gatePosition 1 1 0) (gatePosition 1 2 1))) 3084 ] 3085 where 3086 nestedIdentity depth = iterate (\body -> "(x : x) (" ++ body ++ ")") "t" !! depth 3087 nestedBinders depth = "(" ++ concatMap (\i -> "x" ++ show i ++ " : ") 3088 [0 .. depth - 1] ++ "x0) " ++ unwords (replicate depth "t") 3089 compile input = foldl apply (gateCompileEnv Map.! "compileGateSource") [Leaf, ofString input] 3090 check input expected = compile input @?= expected 3091 checkFunction input args expected = case compile input of 3092 Fork tag (Fork fn Leaf) | tag == trueT -> foldl apply fn args @?= expected 3093 other -> assertFailure ("Expected compiled function, got " ++ show other) 3094 3095 -- -------------------------------------------------------------------------- 3096 -- Binary parser combinator tests 3097 -- -------------------------------------------------------------------------- 3098 3099 parserOk :: T -> T -> T 3100 parserOk val rest = Fork trueT (Fork val rest) 3101 3102 parserErr :: T -> T -> T 3103 parserErr code rest = Fork falseT (Fork code rest) 3104 3105 -- -------------------------------------------------------------------------- 3106 -- IO driver tests 3107 -- -------------------------------------------------------------------------- 3108 3109 -- A deliberately small interpreter: it records every read request, even if 3110 -- the supplied host reply is a failure. Unknown effects fail the test rather 3111 -- than being silently ignored. No filesystem or socket effects are executed. 3112 runRegistryEffects :: T -> T -> IO ([String], T) 3113 runRegistryEffects readReply = go (100 :: Int) 3114 where 3115 go 0 _ = assertFailure "registry effect trace exceeded fuel" >> pure ([], Leaf) 3116 go fuel (Fork tag payload) = case toNumber tag of 3117 Right 0 -> pure ([], payload) 3118 Right 1 -> case payload of 3119 Fork action continuation -> do 3120 (before, value) <- go (fuel - 1) action 3121 (after, answer) <- go (fuel - 1) (apply continuation value) 3122 pure (before ++ after, answer) 3123 _ -> assertFailure "malformed Bind" >> pure ([], Leaf) 3124 Right 20 -> case toString payload of 3125 Right path -> pure ([path], readReply) 3126 Left message -> assertFailure message >> pure ([], Leaf) 3127 other -> assertFailure ("unexpected registry effect: " ++ show other) >> pure ([], Leaf) 3128 go _ _ = assertFailure "malformed registry action" >> pure ([], Leaf) 3129 3130 registryContractTests :: TestTree 3131 registryContractTests = testCase "Registry hash contract: values, bounded rejection and read effects" $ 3132 withSystemTempDirectory "registry-contract" $ \dir -> do 3133 env <- evaluateCompilerFile "./lib/arboricx/server.tri" 3134 let call name = foldl apply (env Map.! name) 3135 http name = foldl apply (fixtureExports "http" Map.! name) 3136 valid = concat (replicate 4 "0123456789abcdef") 3137 hash = ofString valid 3138 diagnostic = ofString "expected 64 lowercase hexadecimal hash characters" 3139 ok value rest = Fork (Stem Leaf) (Fork value rest) 3140 err value rest = Fork Leaf (Fork value rest) 3141 root = ofString "/registry" 3142 expectedPath = "/registry/objects/012/" ++ valid ++ ".arboricx" 3143 invalid = ["", replicate 63 'a', replicate 65 'a', replicate 64 'A', 3144 'g' : tail valid, take 32 valid ++ "g" ++ drop 33 valid, 3145 take 63 valid ++ "g", "../secret", "abc/../../secret", 3146 take 63 valid ++ "/", replicate 65536 'a'] 3147 -- All host byte values, not only selected examples. 3148 forM_ [0..255] $ \byte -> do 3149 let expected = if byte `elem` map (toInteger . fromEnum) "0123456789abcdef" 3150 then Stem Leaf else Leaf 3151 call "bundleHexByte?" [ofNumber byte] @?= expected 3152 forM_ ("0123456789abcdef" :: String) $ \digit -> 3153 call "bundleHashValid?" [ofString (replicate 64 digit)] @?= Stem Leaf 3154 call "bundleHashC" [hash, ofNumber 7] @?= ok hash (ofNumber 7) 3155 forM_ invalid $ \bad -> 3156 call "bundleHashC" [ofString bad, ofNumber 7] @?= err diagnostic (ofNumber 7) 3157 -- Arbitrary malformed trees are rejected as well as canonical byte lists. 3158 forM_ [Stem Leaf, Fork (Stem Leaf) Leaf, 3159 foldr Fork (Stem Leaf) (replicate 64 (ofNumber 97)), 3160 Fork (Fork (Fork Leaf Leaf) Leaf) (ofString (tail valid))] $ \bad -> 3161 call "bundleHashValid?" [bad] @?= Leaf 3162 -- A 65th cons is sufficient evidence: neither its byte nor tail is forced. 3163 let oversized = foldr Fork 3164 (Fork (error "65th byte inspected") (error "oversized suffix inspected")) 3165 (replicate 64 (ofNumber 97)) 3166 call "bundleHashValid?" [oversized] @?= Leaf 3167 let malformedByte = foldr Fork 3168 (Fork (error "extra byte bit inspected") (error "extra byte suffix inspected")) 3169 [Stem Leaf, Leaf, Leaf, Leaf, Leaf, Stem Leaf, Stem Leaf] 3170 call "bundleHexByte?" [malformedByte] @?= Leaf 3171 let missing = err (ofString "missing") Leaf 3172 present = ok (ofString "bundle") Leaf 3173 get value = call "getBundleByHash" [root, value] 3174 serve value = call "serveBundleHash" [root, value] 3175 route value = call "getBundleRoute" 3176 [root, ofString "GET", ofString ("/_arboricx/bundle/hash/" ++ value)] 3177 router value = call "arboricxRouter" 3178 [root, ofString "GET", ofString ("/_arboricx/bundle/hash/" ++ value), Leaf, Leaf] 3179 rejected = http "badRequestResponse" [diagnostic] 3180 forM_ invalid $ \bad -> do 3181 runRegistryEffects present (get (ofString bad)) >>= (@?= ([], err (ofNumber 400) diagnostic)) 3182 forM_ [serve (ofString bad), route bad, router bad] $ \action -> 3183 runRegistryEffects present action >>= (@?= ([], rejected)) 3184 runRegistryEffects present (get hash) >>= (@?= ([expectedPath], present)) 3185 runRegistryEffects missing (get hash) >>= 3186 (@?= ([expectedPath], err (ofNumber 404) (ofString "missing"))) 3187 let response = ofString $ "HTTP/1.1 200 OK\r\n" ++ 3188 "Content-Type: application/vnd.arboricx.bundle\r\n" ++ 3189 "Content-Length: 6\r\nConnection: close\r\n\r\nbundle" 3190 forM_ [serve hash, route valid, router valid] $ \action -> do 3191 runRegistryEffects present action >>= (@?= ([expectedPath], response)) 3192 runRegistryEffects missing action >>= 3193 (@?= ([expectedPath], http "errorResponse" [ofNumber 404, ofString "missing"])) 3194 forM_ [("/_arboricx/health", http "okResponse" [ofString "OK\n"]), 3195 ("/", http "htmlResponse" [env Map.! "htmlLandingPage"])] $ \(target, expected) -> 3196 runRegistryEffects present 3197 (call "arboricxRouter" [root, ofString "GET", ofString target, Leaf, Leaf]) >>= 3198 (@?= ([], expected)) 3199 3200 registryPipelineTests :: TestTree 3201 registryPipelineTests = testGroup "Registry publication and startup" 3202 [ testCase "complete effects: atomic upload, traversal rejection, init and socket failures" $ do 3203 env <- evaluateCompilerFile "./lib/arboricx/server.tri" 3204 let call name = foldl apply (env Map.! name) 3205 root = ofString "/registry" 3206 bytes = ofBytes (BS.pack [0,255,1]) 3207 hash = "47ffa3ea45a70b8a41c2c0825df323c00a8b7a01c1ea06083cc41dddcc001123" 3208 primitive tag payload = Fork (ofNumber tag) payload 3209 ok = parserOk Leaf Leaf 3210 failed = parserErr (ofString "denied") (ofNumber 9) 3211 digest = primitive 28 bytes 3212 mkdir = primitive 25 (ofString "/registry/objects/47f") 3213 publish = primitive 33 (Fork (ofString ("/registry/objects/47f/" ++ hash ++ ".arboricx")) bytes) 3214 put = call "putBundle" [root, bytes] 3215 prefix message = parserErr (ofString message) Leaf 3216 forM_ ["putBundleWithHash", "putBundleWrite", "bundleTmpPath"] $ \name -> 3217 assertBool ("unchecked/obsolete writer still exported: " ++ name) (Map.notMember name env) 3218 runScriptedEffects put [(digest, parserOk (ofString hash) Leaf), (mkdir, ok), (publish, ok)] 3219 >>= (@?= parserOk (ofString hash) Leaf) 3220 runScriptedEffects put [(digest, failed)] >>= (@?= prefix "sha256Hex: denied") 3221 runScriptedEffects put [(digest, parserOk (ofString hash) Leaf), (mkdir, failed)] 3222 >>= (@?= prefix "createDirectory: denied") 3223 runScriptedEffects put [(digest, parserOk (ofString hash) Leaf), (mkdir, ok), (publish, failed)] 3224 >>= (@?= prefix "atomicWriteBytes: denied") 3225 forM_ [ofString "../../../secret", ofString "/etc/passwd", ofString (replicate 65 'a'), Stem Leaf] $ \bad -> 3226 runScriptedEffects put [(digest, parserOk bad Leaf)] >>= 3227 (@?= prefix "sha256Hex: expected 64 lowercase hexadecimal hash characters") 3228 runScriptedEffects (call "ensureStore" [root]) [(primitive 25 root, failed)] 3229 >>= (@?= prefix "createDirectory: denied") 3230 let sock = ofNumber 7 3231 create = primitive 70 Leaf 3232 bindSocket = primitive 72 (Fork sock (Fork (ofString "127.0.0.1") (ofNumber 9050))) 3233 listen = primitive 73 (Fork sock (ofNumber 128)) 3234 close = primitive 71 sock 3235 server = call "arboricxServer" [root, ofString "127.0.0.1", ofNumber 9050] 3236 runScriptedEffects server [(create, failed)] >>= (@?= prefix "denied") 3237 forM_ [ok, failed] $ \closed -> do 3238 runScriptedEffects server [(create, parserOk sock Leaf), (bindSocket, failed), (close, closed)] 3239 >>= (@?= prefix "denied") 3240 runScriptedEffects server [(create, parserOk sock Leaf), (bindSocket, ok), (listen, failed), (close, closed)] 3241 >>= (@?= prefix "denied") 3242 , testCase "filesystem: initialization, binary publication, replacement and concurrent uploads" $ 3243 withSystemTempDirectory "registry-publication" $ \dir -> do 3244 env <- evaluateCompilerFile "./lib/arboricx/server.tri" 3245 let call name = foldl apply (env Map.! name) 3246 rootPath = dir </> "store" 3247 root = ofString rootPath 3248 payload = BS.pack [0,255,1] 3249 hash = "47ffa3ea45a70b8a41c2c0825df323c00a8b7a01c1ea06083cc41dddcc001123" 3250 shard = rootPath </> "objects" </> "47f" 3251 path = shard </> (hash ++ ".arboricx") 3252 perms = defaultPerms { allowRead = [dir], allowWrite = [dir] } 3253 run action = runIO perms (Fork (ofString "tricuIO") (Fork (ofNumber 1) action)) 3254 upload = run (call "putBundle" [root, ofBytes payload]) 3255 expected = Right (parserOk (ofString hash) Leaf) 3256 run (call "ensureStore" [root]) >>= (@?= Right (parserOk Leaf Leaf)) 3257 run (call "ensureStore" [root]) >>= (@?= Right (parserOk Leaf Leaf)) 3258 upload >>= (@?= expected) 3259 BS.readFile path >>= (@?= payload) 3260 run (call "getBundleByHash" [root, ofString hash]) >>= (@?= Right (parserOk (ofBytes payload) Leaf)) 3261 -- Replacement is deliberate, not write-if-absent/corrupt-winner policy. 3262 BS.writeFile path (BS.pack [7]) 3263 upload >>= (@?= expected) 3264 BS.readFile path >>= (@?= payload) 3265 outputs <- forM [1,2 :: Int] $ \_ -> do 3266 output <- newEmptyMVar 3267 _ <- forkIO ((try upload :: IO (Either SomeException (Either String T))) >>= putMVar output) 3268 pure output 3269 completed <- Timeout.timeout 10000000 (mapM takeMVar outputs) 3270 case completed of 3271 Nothing -> assertFailure "concurrent registry uploads timed out" 3272 Just results -> forM_ results $ \result -> case result of 3273 Left problem -> assertFailure (show problem) 3274 Right value -> value @?= expected 3275 BS.readFile path >>= (@?= payload) 3276 listDirectory shard >>= (@?= [hash ++ ".arboricx"]) 3277 listDirectory (rootPath </> "tmp") >>= (@?= []) 3278 -- An existing file cannot masquerade as an initialized directory. 3279 let blocked = dir </> "blocked" 3280 BS.writeFile blocked BS.empty 3281 result <- run (call "ensureStore" [ofString blocked]) 3282 result @?= Right (parserErr (ofString "createDirectory: already exists") Leaf) 3283 ] 3284 3285 -- Tiny source probes run through the authenticated portable compiler fixture. 3286 -- Force complete generated trees before applying deliberately partial host data. 3287 localControlTests :: TestTree 3288 localControlTests = testGroup "Local control construction and non-strictness" 3289 [ testCase "case constructs code, skips unselected work and preserves wildcard laziness" $ do 3290 selected <- compileLocalControl [] $ unlines 3291 [ "main input = case input of" 3292 , " t = 23" 3293 , " _ = let rec loop x = loop x in loop t" ] 3294 apply selected Leaf @?= ofNumber 23 3295 ignored <- compileLocalControl [] "main input = case input of\n _ = 23" 3296 apply ignored (error "wildcard forced its subject") @?= ofNumber 23 3297 first <- compileLocalControl [] "main input = case input of\n (head, _) = head\n _ = t" 3298 apply first (Fork (ofNumber 23) (error "case forced an ignored sibling")) @?= ofNumber 23 3299 , testCase "local recursive hash worker rejects poisoned byte/tail with bounded inspection" $ do 3300 server <- evaluateCompilerFile "./lib/arboricx/server.tri" 3301 math <- evaluateCompilerFile "./lib/math/native.tri" 3302 function <- compileLocalControl 3303 [("true", Stem Leaf), ("false", Leaf), 3304 ("hex", server Map.! "bundleHexByte?"), ("subtract", math Map.! "subtract")] $ unlines 3305 [ "main hash = (let rec" 3306 , " walk [] 0 = true" 3307 , " walk _ 0 = false" 3308 , " walk [byte | rest] remaining = case (hex byte) of" 3309 , " ^true = walk rest (subtract remaining 1)" 3310 , " _ = false" 3311 , " walk _ _ = false" 3312 , " in walk hash 64)" ] 3313 apply function (ofString (replicate 64 'a')) @?= Stem Leaf 3314 let oversized = foldr Fork 3315 (Fork (error "local worker inspected byte 65") (error "local worker inspected suffix")) 3316 (replicate 64 (ofNumber 97)) 3317 malformedByte = foldr Fork 3318 (Fork (error "local worker inspected extra bit") (error "local worker inspected numeral suffix")) 3319 [Stem Leaf, Leaf, Leaf, Leaf, Leaf, Stem Leaf, Stem Leaf] 3320 apply function oversized @?= Leaf 3321 apply function (Fork malformedByte (error "local worker continued after malformed byte")) @?= Leaf 3322 ] 3323 3324 compileLocalControl :: [(String, T)] -> String -> IO T 3325 compileLocalControl bindings source = do 3326 unit <- evaluateCompilerFile "./lib/source/unit.tri" 3327 let external = ofList [Fork (ofString name) value | (name,value) <- bindings] 3328 answer = apply (apply (unit Map.! "compileStableModuleDefinitions") external) (ofString source) 3329 forceTree Leaf = () 3330 forceTree (Stem child) = forceTree child 3331 forceTree (Fork left right) = forceTree left `seq` forceTree right 3332 case answer of 3333 Fork (Stem Leaf) (Fork (Fork (Fork name function) Leaf) Leaf) -> do 3334 name @?= ofString "main" 3335 evaluate (forceTree function) 3336 pure function 3337 other -> assertFailure ("local control compilation failed: " ++ show other) >> pure Leaf 3338 3339 httpPipelineTests :: TestTree 3340 httpPipelineTests = testCase "HTTP reference pipeline: buffers, EOF, receive errors and cleanup" $ do 3341 let env = fixtureExports "http" 3342 call name = foldl apply (env Map.! name) 3343 sock = ofNumber 7 3344 recv size = pairT (ofNumber 76) (pairT sock (ofNumber size)) 3345 send bytes = pairT (ofNumber 77) (pairT sock (ofString bytes)) 3346 close = pairT (ofNumber 71) sock 3347 ok = parserOk Leaf Leaf 3348 failed = parserErr (ofString "broken") Leaf 3349 bad message = parserErr (ofNumber 400) (ofString message) 3350 call "statusPhrase" [ofNumber 413] @?= ofString "Content Too Large" 3351 forM_ [(0, "abc", "", "abc"), (2, "abc", "ab", "c")] $ \(count, input, body, rest) -> 3352 runScriptedEffects (call "readBodyExact" [sock, ofNumber count, ofString input]) [] >>= 3353 (@?= parserOk (ofString body) (ofString rest)) 3354 runScriptedEffects (call "readBodyExact" [sock, ofNumber 3, ofString "a"]) 3355 [(recv 1, parserOk (ofString "b") Leaf), (recv 1, parserOk (ofString "c") Leaf)] >>= 3356 (@?= parserOk (ofString "abc") Leaf) 3357 -- Exact-length invariant, including short reads and oversized initial buffers. 3358 forM_ [0..8 :: Int] $ \count -> 3359 forM_ [0..count+2] $ \initialSize -> do 3360 let bytes = "AbCdEfGhIjK" 3361 initial = take initialSize bytes 3362 chunks = take (count - initialSize) (drop initialSize bytes) 3363 script = [(recv 1, parserOk (ofString [byte]) Leaf) | byte <- chunks] 3364 runScriptedEffects (call "readBodyExact" [sock, ofNumber (fromIntegral count), ofString initial]) script >>= 3365 (@?= parserOk (ofString (take count bytes)) (ofString (drop count initial))) 3366 runScriptedEffects (call "readBodyExact" [sock, ofNumber 3, ofString "a"]) 3367 [(recv 1, parserOk Leaf Leaf)] >>= (@?= bad "Unexpected end of request body\n") 3368 runScriptedEffects (call "readBodyExact" [sock, ofNumber 3, Leaf]) 3369 [(recv 1, failed)] >>= (@?= bad "recv failed while reading body: broken") 3370 runScriptedEffects (call "recvUntilMax" [sock, ofString "\r\n\r\n", ofNumber 20]) 3371 [(recv 4096, parserOk (ofString "abc") Leaf), (recv 4096, failed)] >>= 3372 (@?= parserErr (ofNumber 400) (ofString "abc")) 3373 let message = "Request Header Fields Too Large\n" 3374 response431 = "HTTP/1.1 431 Request Header Fields Too Large\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: " ++ 3375 show (length message) ++ "\r\nConnection: close\r\n\r\n" ++ message 3376 runScriptedEffects (call "httpHandleReceived" [Leaf, sock, parserErr (ofNumber 431) (ofString "raw")]) 3377 [(send response431, ok), (close, ok)] >>= (@?= ok) 3378 forM_ [ok, failed, parserErr (ofString "broken") (ofString "context")] $ \sent -> 3379 forM_ [ok, failed] $ \closed -> 3380 runScriptedEffects (call "respondAndClose" [sock, ofString "response"]) 3381 [(send "response", sent), (close, closed)] >>= (@?= sent) 3382 -- Host-side negative controls for missing, wrong and leftover effects. 3383 forM_ [(recv 1, []), (recv 1, [(close, ok)]), (pairT (ofNumber 0) Leaf, [(close, ok)])] $ \(action, script) -> do 3384 observed <- try (runScriptedEffects action script) :: IO (Either SomeException T) 3385 case observed of 3386 Left _ -> pure () 3387 Right _ -> assertFailure "HTTP effect interpreter accepted a mismatched script" 3388 3389 containsAll :: String -> [String] -> Assertion 3390 containsAll text needles = 3391 forM_ needles $ \needle -> 3392 assertBool ("expected " ++ show needle ++ " in: " ++ text) (needle `isInfixOf` text) 3393 3394 3395 ioOkResult :: T -> T 3396 ioOkResult val = Fork (Stem Leaf) (Fork val Leaf) 3397 3398 ioErrResult :: String -> T 3399 ioErrResult msg = Fork Leaf (Fork (ofString msg) Leaf)