tricu

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

commit 38e85b4a5e328be257e4de84a368fa0e643378e6
parent 37e852e6e348fcf4245fa47b4d46879de6dbb63c
Author: James Eversole <james@eversole.co>
Date:   Wed, 14 Oct 2026 15:37:01 -0500

Separate source-free runtime from recovery frontend

Diffstat:
MAGENTS.md | 4++++
Mdocs/compiler.md | 10+++++++++-
Mdocs/validation.md | 8++++++++
Mflake.nix | 25+++++++++++++++++++++++--
Msrc/ContentStore/Arboricx.hs | 2+-
Msrc/ContentStore/Bundle.hs | 2+-
Msrc/ContentStore/Resolver.hs | 10+++++-----
Msrc/IODriver.hs | 2+-
Msrc/Main.hs | 14+++++++-------
Msrc/REPL.hs | 2+-
Msrc/Research.hs | 308+------------------------------------------------------------------------------
Asrc/Runtime.hs | 10++++++++++
Asrc/Runtime/Merkle.hs | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Runtime/Presentation.hs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Runtime/Tree.hs | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/Runtime/Value.hs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/TreeObservation.hs | 2+-
Msrc/Wire.hs | 4++--
Mtest/Spec.hs | 17++++++++++++++++-
Mtricu.cabal | 26+++++++++++++++-----------
20 files changed, 436 insertions(+), 339 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -112,3 +112,7 @@ head (map f xs) → From lib/base.tri -- line comment ``` + +## Agent Coordination + +You are only permitted to use `intercom` or agent orchestration tools available to you with *explicit user approval*. diff --git a/docs/compiler.md b/docs/compiler.md @@ -180,7 +180,15 @@ complete-interface cache rules. ## Haskell evaluation boundary -`Research.T` has lazy constructor fields, and `Research.apply` implements the +The production host imports `Runtime`, whose implementation is split into +`Runtime.Tree` (trees/reduction), `Runtime.Value` (canonical values), +`Runtime.Presentation` (display), and `Runtime.Merkle` (native DAG helpers). +`Research` is a recovery-only facade containing the historical Haskell frontend +syntax. Tests and benchmarks still use it; the installed executable does not. +The `ast` display format renders tree constructors directly, without constructing +source ASTs. + +`Runtime.Tree.T` has lazy constructor fields, and `Runtime.Tree.apply` implements the kernel rules without an explicit application/thunk node. Pending reductions live in Haskell thunks. Constructor application returns an outer Stem/Fork without forcing its children; constant application ignores its discarded argument; diff --git a/docs/validation.md b/docs/validation.md @@ -7,6 +7,14 @@ For representation-sensitive work, see [Compiler architecture](compiler.md.html) ## Fast gates +The normal Nix host package builds from inputs that exclude `Research`, the +Haskell lexer/parser/compiler/source loader, and recovery workspace resolution. +This makes accidental production dependencies on the recovery frontend build +failures. The test and benchmark components retain those inputs during migration. +The runtime-presentation tests compare constructor display with the recovery AST +oracle and an independently written golden; runtime demand tests exercise the +same extracted reducer used by production. + ```sh nix flake check python3 test/unit.py diff --git a/flake.nix b/flake.nix @@ -28,6 +28,18 @@ ]; }; + # The installed host must build with recovery frontend files absent. + # Tests/benchmarks deliberately retain them until their migration finishes. + runtimeSource = pkgs.lib.cleanSourceWith { + src = tricuSource; + filter = path: type: + !(builtins.any (relative: pkgs.lib.hasSuffix "/${relative}" path) [ + "src/Research.hs" "src/Eval.hs" "src/FileEval.hs" + "src/Lexer.hs" "src/Parser.hs" "src/Frontend" + "src/Module/Resolver.hs" "src/Module/Workspace.hs" + ]); + }; + haskellPackages = pkgs.haskellPackages; hsLib = pkgs.haskell.lib; @@ -38,7 +50,7 @@ tricuMuslStatic = staticHsLib.justStaticExecutables ( staticHsLib.dontCheck ( - staticHaskellPackages.callCabal2nix packageName tricuSource {} + staticHaskellPackages.callCabal2nix packageName runtimeSource {} ) ); @@ -60,7 +72,7 @@ tricuPackage = hsLib.dontCheck ( - haskellPackages.callCabal2nix packageName tricuSource {} + haskellPackages.callCabal2nix packageName runtimeSource {} ); # Compile once with the packaged stable compiler. Runtime needs neither @@ -314,6 +326,15 @@ packages.tricu-js = tricuJs; packages.tricu-js-tests = tricuJsTests; + checks.runtimeBoundary = pkgs.runCommand "tricu-runtime-source-boundary" {} '' + test -f ${runtimeSource}/src/Runtime/Tree.hs + for path in Research.hs Eval.hs FileEval.hs Lexer.hs Parser.hs \ + Frontend Module/Resolver.hs Module/Workspace.hs; do + test ! -e "${runtimeSource}/src/$path" + done + mkdir -p $out + ''; + checks.${packageName} = tricuPackageTests; checks.default = tricuPackageTests; # Fast packaging/filesystem checks only, no compiler or application runs. diff --git a/src/ContentStore/Arboricx.hs b/src/ContentStore/Arboricx.hs @@ -16,7 +16,7 @@ module ContentStore.Arboricx import ContentStore.Filesystem import ContentStore.Object -import Research +import Runtime import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BL diff --git a/src/ContentStore/Bundle.hs b/src/ContentStore/Bundle.hs @@ -18,7 +18,7 @@ import ContentStore.Arboricx import ContentStore.Alias (ObjectRef(..)) import ContentStore.Object import ContentStore.Filesystem (putObject, objectPath, getVerifiedObject) -import Research (T) +import Runtime (T) import Wire import Control.Monad (foldM, forM, forM_, unless) diff --git a/src/ContentStore/Resolver.hs b/src/ContentStore/Resolver.hs @@ -12,8 +12,8 @@ import ContentStore.Arboricx import ContentStore.Filesystem import ContentStore.Object import Module.Manifest -import Research (Node(..), T, deserializeNode) -import qualified Research +import Runtime (Node(..), T, deserializeNode) +import qualified Runtime import Data.ByteString (ByteString) import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef') @@ -104,12 +104,12 @@ resolveTreeFromObjects resolver h = do Nothing -> return Nothing Just bytes -> return (Just (deserializeNode bytes)) - hydrate NLeaf = return (Just Research.Leaf) - hydrate (NStem child) = fmap Research.Stem <$> hydrateHash child + hydrate NLeaf = return (Just Runtime.Leaf) + hydrate (NStem child) = fmap Runtime.Stem <$> hydrateHash child hydrate (NFork left right) = do l <- hydrateHash left r <- hydrateHash right - return $ Research.Fork <$> l <*> r + return $ Runtime.Fork <$> l <*> r hydrateHash nodeHash = do mChild <- resolveNode resolver nodeHash diff --git a/src/IODriver.hs b/src/IODriver.hs @@ -13,7 +13,7 @@ module IODriver , runIOWithYieldStatsHook ) where -import Research (T(..), EvaluatedForm(Decode), formatT, apply, toString, toNumber, ofString, ofNumber, ofBytes, toBytes, ofList) +import Runtime (T(..), EvaluatedForm(Decode), formatT, apply, toString, toNumber, ofString, ofNumber, ofBytes, toBytes, ofList) import ContentStore.Arboricx (treeTermDomain, decodeTreeTermShared, encodeTreeTerm) import ContentStore.Object (hashObject) import ContentStore.Bundle (indexedTreeDomain, decodeIndexedTree, encodeIndexedTree) diff --git a/src/Main.hs b/src/Main.hs @@ -11,7 +11,7 @@ import Module.Manifest import System.Exit (die, exitFailure) import IODriver (IOPermissions(..), runIO, YieldStats(..), runIOWithYieldStatsHook) import REPL (portableRepl) -import Research (T(..), EvaluatedForm(..), formatT, exportDag, apply, ofString, ofNumber, toString) +import Runtime (T(..), EvaluatedForm(..), formatT, exportDag, apply, ofString, ofNumber, toString) import qualified Wire import Wire (encodeBundle, buildBundleShared, defaultExportNames, Bundle(..)) import TreeObservation (forceTreeShared) @@ -974,10 +974,10 @@ loadVerifiedManifest store manifestHash = do (decodeManifest manifestPayload) pure manifest -loadVerifiedExport :: StorePath -> T.Text -> T.Text -> IO (ObjectHash, Research.T) +loadVerifiedExport :: StorePath -> T.Text -> T.Text -> IO (ObjectHash, Runtime.T) loadVerifiedExport = loadVerifiedExportWith (const $ pure ()) -loadVerifiedExportWith :: (String -> IO ()) -> StorePath -> T.Text -> T.Text -> IO (ObjectHash, Research.T) +loadVerifiedExportWith :: (String -> IO ()) -> StorePath -> T.Text -> T.Text -> IO (ObjectHash, Runtime.T) loadVerifiedExportWith sample store manifestHash name = do manifest <- loadVerifiedManifest store manifestHash sample "manifest-verified" @@ -1027,7 +1027,7 @@ packagedCompiler store = do [ModuleExport name (ObjectRef (unDomain indexedTreeDomain) root) "arboricx.abi.tree.v1" | (name, root) <- roots]) -portableExports :: CompilerOptions -> Maybe FilePath -> FilePath -> IO [(String, Research.T)] +portableExports :: CompilerOptions -> Maybe FilePath -> FilePath -> IO [(String, Runtime.T)] portableExports options selected path = do (store, pin) <- buildPortableModule options selected path manifest <- loadVerifiedManifest store pin @@ -1138,7 +1138,7 @@ profileSample label counts = do BL.hPutStr stderr "\n" -- Presentation of the canonical Result envelope; the underlying tree survives. -renderInvocationResult :: EvaluatedForm -> Research.T -> Either String String +renderInvocationResult :: EvaluatedForm -> Runtime.T -> Either String String renderInvocationResult form (Fork (Stem Leaf) (Fork value _)) = Right $ "ok: " ++ case (form, value) of (Decode, Leaf) -> "[]" @@ -1149,7 +1149,7 @@ renderInvocationResult form (Fork Leaf (Fork problem _)) = _ -> formatT form problem renderInvocationResult _ _ = Left "Invalid Result envelope; use the tree view to inspect it" -storedArgument :: StorePath -> StoredArgument -> IO Research.T +storedArgument :: StorePath -> StoredArgument -> IO Runtime.T storedArgument _ (StringArgument value) = pure (ofString value) storedArgument _ (TreeFileArgument path) = do payload <- BS.readFile path @@ -1252,7 +1252,7 @@ runExportDag opts = do store <- resolveStorePath (exportStore opts) root <- resolveStoreTarget store target term <- loadExecutableObject store root - let (rootIdx, nodes) = Research.exportDag term + let (rootIdx, nodes) = Runtime.exportDag term output = unlines $ show rootIdx : map (\(tag, refs) -> unwords (tag : map show refs)) nodes diff --git a/src/REPL.hs b/src/REPL.hs @@ -2,7 +2,7 @@ -- recovery frontend. Each successful submission is a transaction on source state. module REPL where -import Research (EvaluatedForm(..), Env, T, formatT) +import Runtime (EvaluatedForm(..), Env, T, formatT) import ContentStore (StorePath(..), AliasKind(..), ObjectRef(..), readAlias, unDomain) import Module.Manifest (manifestDomain) import Control.Exception (SomeException, AsyncException, catch, displayException, evaluate, fromException, throwIO) diff --git a/src/Research.hs b/src/Research.hs @@ -1,23 +1,7 @@ -{-# LANGUAGE PatternSynonyms #-} +-- Recovery frontend facade. Production modules must import Runtime instead. +module Research (module Runtime, module Research) where -module Research where - -import Crypto.Hash (hash, SHA256, Digest) -import Data.ByteArray (convert) -import Data.ByteString.Base16 (decode, encode) -import Data.List (intercalate) -import Data.Map () -import Data.Text (Text, replace) -import Data.Text.Encoding (decodeUtf8, encodeUtf8) -import Data.Word (Word8) -import qualified Data.ByteString as BS -import qualified Data.Map as Map -import qualified Data.Set as Set -import qualified Data.Text as T - --- Tree Calculus Types -data T = Leaf | Stem T | Fork T T - deriving (Show, Eq, Ord) +import Runtime -- Contract source annotations for @ and =@ syntax. ViewExpr carries the -- surface syntax of a contract until Frontend.ContractDesugar turns it into a @@ -87,293 +71,7 @@ data LToken | LIndent Int deriving (Eq, Show, Ord) --- Output formats -data EvaluatedForm = Tree | FSL | AST | Ternary | Ascii | Decode | Number | StringLit - deriving (Show) - --- Environment containing previously evaluated TC terms -type Env = Map.Map String T - --- Merkle DAG Node types --- Each Tree Calculus node becomes a content-addressed object. -type MerkleHash = Text - -data Node - = NLeaf - | NStem MerkleHash - | NFork MerkleHash MerkleHash - deriving (Show, Eq, Ord) - --- | Canonical serialization of a Node for hashing. --- Leaf: 0x00 --- Stem: 0x01 || child_hash (32 bytes) --- Fork: 0x02 || left_hash (32 bytes) || right_hash (32 bytes) -serializeNode :: Node -> BS.ByteString -serializeNode NLeaf = BS.pack [0x00] -serializeNode (NStem h) = BS.pack [0x01] <> go (decode (encodeUtf8 h)) - where go (Left _) = error "Research.serializeNode: invalid hex hash" - go (Right bs) = bs -serializeNode (NFork l r) = BS.pack [0x02] <> go (decode (encodeUtf8 l)) <> go (decode (encodeUtf8 r)) - where go (Left _) = error "Research.serializeNode: invalid hex hash" - go (Right bs) = bs - --- | Hash a node per the Merkle content-addressing spec. --- hash = SHA256( "arboricx.merkle.node.v1" <> 0x00 <> node_payload ) -nodeHash :: Node -> MerkleHash -nodeHash node = decodeUtf8 (encode (sha256WithPrefix (serializeNode node))) - where sha256WithPrefix payload = - convert . (hash :: BS.ByteString -> Digest SHA256) $ utf8Tag <> BS.pack [0x00] <> payload - utf8Tag = BS.pack $ map fromIntegral $ BS.unpack "arboricx.merkle.node.v1" - --- | Deserialize a Node from canonical bytes. -deserializeNode :: BS.ByteString -> Node -deserializeNode bs = - case BS.uncons bs of - Just (0x00, rest) - | BS.null rest -> NLeaf - - Just (0x01, rest) - | BS.length rest == 32 -> - NStem $ decodeUtf8 (encode rest) - - Just (0x02, rest) - | BS.length rest == 64 -> - let (l, r) = BS.splitAt 32 rest - in NFork (decodeUtf8 (encode l)) (decodeUtf8 (encode r)) - - _ -> errorWithoutStackTrace "invalid merkle node payload" - --- --------------------------------------------------------------------------- --- ByteString / bytestream marshalling via existing Tree Calculus conventions --- --------------------------------------------------------------------------- - --- | Encode a single byte (Word8) as a Tree Calculus number (0..255). -ofByte :: Word8 -> T -ofByte = ofNumber . fromIntegral - --- | Decode a Tree Calculus number as a single byte (Word8). --- Rejects values outside the range 0..255. -toByte :: T -> Either String Word8 -toByte t = case toNumber t of - Left err -> Left err - Right n - | n >= 0 && n <= 255 -> Right (fromIntegral n) - | otherwise -> Left ("Byte value out of range: " ++ show n) - --- | Encode a ByteString as a Tree Calculus list of Byte trees. -ofBytes :: BS.ByteString -> T -ofBytes = ofList . map ofByte . BS.unpack - --- | Decode a Tree Calculus list of Byte trees as a ByteString. --- Rejects non-list trees and elements that are not valid byte values (0..255). -toBytes :: T -> Either String BS.ByteString -toBytes t = case toList t of - Left err -> Left err - Right bs -> BS.pack <$> mapM toByte bs - --- | Convert a canonical Arboricx node payload (ByteString) to a Tree --- representation (a list of Byte trees). -nodePayloadToTreeBytes :: BS.ByteString -> T -nodePayloadToTreeBytes = ofBytes - --- | Convert a Tree representation of a node payload back to ByteString. -treeBytesToNodePayload :: T -> Either String BS.ByteString -treeBytesToNodePayload = toBytes - --- | Convert a MerkleHash (hex-encoded) to a Tree of its 32 raw bytes. -hashToTreeBytes :: MerkleHash -> Either String T -hashToTreeBytes h = case decode (encodeUtf8 h) of - Left _ -> Left "Invalid hex MerkleHash" - Right raw - | BS.length raw == 32 -> Right (ofBytes raw) - | otherwise -> Left "Hash raw bytes must be 32 bytes" - --- | Convert a Tree of 32 Byte trees back to a MerkleHash (hex string). -treeBytesToHash :: T -> Either String MerkleHash -treeBytesToHash t = case toList t of - Left err -> Left err - Right bytes - | length bytes == 32 -> do - raw <- BS.pack <$> mapM toByte bytes - Right $ decodeUtf8 (encode raw) - | otherwise -> Left "Expected exactly 32 byte elements for hash" - --- | Build a Merkle DAG from a Tree Calculus term. -buildMerkle :: T -> Node -buildMerkle Leaf = NLeaf -buildMerkle (Stem t) = NStem (nodeHash child) - where child = buildMerkle t -buildMerkle (Fork l r) = NFork (nodeHash left) (nodeHash right) - where - left = buildMerkle l - right = buildMerkle r - --- Triage Calculus Reduction Rules -{- - The t operator is left associative. - 1. t t a b -> a - 2. t (t a) b c -> a c (b c) - 3a. t (t a b) c t -> a - 3b. t (t a b) c (t u) -> b u - 3c. t (t a b) c (t u v) -> c u v --} -apply :: T -> T -> T -apply (Fork Leaf a) _ = a -apply (Fork (Stem a) b) c = apply (apply a c) (apply b c) -apply (Fork (Fork _a _b) _c) Leaf = _a -apply (Fork (Fork _a _b) _c) (Stem u) = apply _b u -apply (Fork (Fork _a _b) _c) (Fork u v) = apply (apply _c u) v --- Left associative `t` -apply Leaf b = Stem b -apply (Stem a) b = Fork a b - --- Booleans -_false :: T -_false = Leaf - -_true :: T -_true = Stem Leaf - -_not :: T -_not = Fork (Fork _true (Fork Leaf _false)) Leaf - --- Marshalling -ofString :: String -> T -ofString str = ofList $ map (ofNumber . toInteger . fromEnum) str - -ofNumber :: Integer -> T -ofNumber 0 = Leaf -ofNumber n = - Fork - (if odd n then Stem Leaf else Leaf) - (ofNumber (n `div` 2)) - -ofList :: [T] -> T -ofList = foldr Fork Leaf - -toNumber :: T -> Either String Integer -toNumber Leaf = Right 0 -toNumber (Fork Leaf rest) = case toNumber rest of - Right n -> Right (2 * n) - Left err -> Left err -toNumber (Fork (Stem Leaf) rest) = case toNumber rest of - Right n -> Right (1 + 2 * n) - Left err -> Left err -toNumber _ = Left "Invalid Tree Calculus number" - -toChar :: Integer -> Either String Char -toChar n - | n < 0 = Left "Negative character code" - | n > 0x10FFFF = Left "Character code out of Unicode range" - | n >= 0xD800 && n <= 0xDFFF = Left "Surrogate character code not allowed" - | otherwise = Right (toEnum (fromInteger n)) - -toString :: T -> Either String String -toString tc = do - list <- toList tc - nums <- mapM toNumber list - mapM toChar nums - -toList :: T -> Either String [T] -toList Leaf = Right [] -toList (Fork x rest) = case toList rest of - Right xs -> Right (x : xs) - Left err -> Left err -toList _ = Left "Invalid Tree Calculus list" - --- Outputs -formatT :: EvaluatedForm -> T -> String -formatT Tree = toSimpleT . show -formatT FSL = show -formatT AST = show . toAST -formatT Ternary = toTernaryString -formatT Ascii = toAscii -formatT Decode = decodeResult -formatT Number = either (\e -> "<not-number: " ++ e ++ ">") show . toNumber -formatT StringLit = either (\e -> "<not-string: " ++ e ++ ">") show . toString - -toSimpleT :: String -> String -toSimpleT s = T.unpack - $ replace "Fork" "t" - $ replace "Stem" "t" - $ replace "Leaf" "t" - $ T.pack s - -toTernaryString :: T -> String -toTernaryString Leaf = "0" -toTernaryString (Stem t) = "1" ++ toTernaryString t -toTernaryString (Fork t1 t2) = "2" ++ toTernaryString t1 ++ toTernaryString t2 - toAST :: T -> TricuAST toAST Leaf = TLeaf toAST (Stem a) = TStem (toAST a) toAST (Fork a b) = TFork (toAST a) (toAST b) - -toAscii :: T -> String -toAscii tree = go tree "" True - where - go :: T -> String -> Bool -> String - go Leaf prefix isLast = - prefix ++ (if isLast then "`-- " else "|-- ") ++ "Leaf\n" - go (Stem t) prefix isLast = - prefix ++ (if isLast then "`-- " else "|-- ") ++ "Stem\n" - ++ go t (prefix ++ (if isLast then " " else "| ")) True - go (Fork left right) prefix isLast = - prefix ++ (if isLast then "`-- " else "|-- ") ++ "Fork\n" - ++ go left (prefix ++ (if isLast then " " else "| ")) False - ++ go right (prefix ++ (if isLast then " " else "| ")) True - -decodeResult :: T -> String -decodeResult Leaf = "t" -decodeResult tc = - case (toString tc, toList tc, toNumber tc) of - (Right s, _, _) | all isCommonChar s -> "\"" ++ s ++ "\"" - (_, _, Right n) -> show n - (_, Right xs@(_:_), _) -> "[" ++ intercalate ", " (map decodeResult xs) ++ "]" - (_, Right [], _) -> "[]" - _ -> formatT Tree tc - where - isCommonChar c = - let n = fromEnum c - in (n >= 32 && n <= 126) - || n == 9 - || n == 10 - || n == 13 - --- --------------------------------------------------------------------------- --- DAG node-table export (for host-language kernel embedding) --- --------------------------------------------------------------------------- - --- | Export a term's Merkle DAG as a topologically-sorted node table. --- Children appear before parents so all index references are forward. --- Returns (root index, list of (tag, [child_indices])). -exportDag :: T -> (Int, [(String, [Int])]) -exportDag term = - let (root, acc, _) = collectDag term [] Set.empty - -- acc is in reverse post-order (children first, root last) - ordered = reverse acc - idxMap = Map.fromList [(h, i) | (i, (h, _)) <- zip [0..] ordered] - rootIdx = idxMap Map.! root - lines_ = map (formatNode idxMap . snd) ordered - in (rootIdx, lines_) - where - collectDag :: T -> [(MerkleHash, Node)] -> Set.Set MerkleHash -> (MerkleHash, [(MerkleHash, Node)], Set.Set MerkleHash) - collectDag Leaf acc seen = - let h = nodeHash NLeaf - in if Set.member h seen then (h, acc, seen) else (h, (h, NLeaf) : acc, Set.insert h seen) - collectDag (Stem t) acc seen = - let (ch, acc', seen') = collectDag t acc seen - node = NStem ch - h = nodeHash node - in if Set.member h seen' then (h, acc', seen') else (h, (h, node) : acc', Set.insert h seen') - collectDag (Fork l r) acc seen = - let (lh, acc', seen') = collectDag l acc seen - (rh, acc'', seen'') = collectDag r acc' seen' - node = NFork lh rh - h = nodeHash node - in if Set.member h seen'' then (h, acc'', seen'') else (h, (h, node) : acc'', Set.insert h seen'') - - formatNode :: Map.Map MerkleHash Int -> Node -> (String, [Int]) - formatNode _ NLeaf = ("leaf", []) - formatNode idxMap (NStem ch) = ("stem", [idxMap Map.! ch]) - formatNode idxMap (NFork l r) = ("fork", [idxMap Map.! l, idxMap Map.! r]) diff --git a/src/Runtime.hs b/src/Runtime.hs @@ -0,0 +1,10 @@ +-- Source-free host API. Recovery syntax must never be reexported here. +module Runtime + ( module Runtime.Tree, module Runtime.Value + , module Runtime.Presentation, module Runtime.Merkle + ) where + +import Runtime.Tree +import Runtime.Value +import Runtime.Presentation +import Runtime.Merkle diff --git a/src/Runtime/Merkle.hs b/src/Runtime/Merkle.hs @@ -0,0 +1,137 @@ +-- Native Merkle codecs and DAG export, independent of source compilation. +module Runtime.Merkle where + +import Runtime.Tree +import Runtime.Value +import Crypto.Hash (hash, SHA256, Digest) +import Data.ByteArray (convert) +import Data.ByteString.Base16 (decode, encode) +import Data.Text (Text) +import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import qualified Data.ByteString as BS +import qualified Data.Map as Map +import qualified Data.Set as Set + +-- Merkle DAG Node types +-- Each Tree Calculus node becomes a content-addressed object. +type MerkleHash = Text + +data Node + = NLeaf + | NStem MerkleHash + | NFork MerkleHash MerkleHash + deriving (Show, Eq, Ord) + +-- | Canonical serialization of a Node for hashing. +-- Leaf: 0x00 +-- Stem: 0x01 || child_hash (32 bytes) +-- Fork: 0x02 || left_hash (32 bytes) || right_hash (32 bytes) +serializeNode :: Node -> BS.ByteString +serializeNode NLeaf = BS.pack [0x00] +serializeNode (NStem h) = BS.pack [0x01] <> go (decode (encodeUtf8 h)) + where go (Left _) = error "Runtime.serializeNode: invalid hex hash" + go (Right bs) = bs +serializeNode (NFork l r) = BS.pack [0x02] <> go (decode (encodeUtf8 l)) <> go (decode (encodeUtf8 r)) + where go (Left _) = error "Runtime.serializeNode: invalid hex hash" + go (Right bs) = bs + +-- | Hash a node per the Merkle content-addressing spec. +-- hash = SHA256( "arboricx.merkle.node.v1" <> 0x00 <> node_payload ) +nodeHash :: Node -> MerkleHash +nodeHash node = decodeUtf8 (encode (sha256WithPrefix (serializeNode node))) + where sha256WithPrefix payload = + convert . (hash :: BS.ByteString -> Digest SHA256) $ utf8Tag <> BS.pack [0x00] <> payload + utf8Tag = BS.pack $ map fromIntegral $ BS.unpack "arboricx.merkle.node.v1" + +-- | Deserialize a Node from canonical bytes. +deserializeNode :: BS.ByteString -> Node +deserializeNode bs = + case BS.uncons bs of + Just (0x00, rest) + | BS.null rest -> NLeaf + + Just (0x01, rest) + | BS.length rest == 32 -> + NStem $ decodeUtf8 (encode rest) + + Just (0x02, rest) + | BS.length rest == 64 -> + let (l, r) = BS.splitAt 32 rest + in NFork (decodeUtf8 (encode l)) (decodeUtf8 (encode r)) + + _ -> errorWithoutStackTrace "invalid merkle node payload" + +-- | Convert a canonical Arboricx node payload (ByteString) to a Tree +-- representation (a list of Byte trees). +nodePayloadToTreeBytes :: BS.ByteString -> T +nodePayloadToTreeBytes = ofBytes + +-- | Convert a Tree representation of a node payload back to ByteString. +treeBytesToNodePayload :: T -> Either String BS.ByteString +treeBytesToNodePayload = toBytes + +-- | Convert a MerkleHash (hex-encoded) to a Tree of its 32 raw bytes. +hashToTreeBytes :: MerkleHash -> Either String T +hashToTreeBytes h = case decode (encodeUtf8 h) of + Left _ -> Left "Invalid hex MerkleHash" + Right raw + | BS.length raw == 32 -> Right (ofBytes raw) + | otherwise -> Left "Hash raw bytes must be 32 bytes" + +-- | Convert a Tree of 32 Byte trees back to a MerkleHash (hex string). +treeBytesToHash :: T -> Either String MerkleHash +treeBytesToHash t = case toList t of + Left err -> Left err + Right bytes + | length bytes == 32 -> do + raw <- BS.pack <$> mapM toByte bytes + Right $ decodeUtf8 (encode raw) + | otherwise -> Left "Expected exactly 32 byte elements for hash" + +-- | Build a Merkle DAG from a Tree Calculus term. +buildMerkle :: T -> Node +buildMerkle Leaf = NLeaf +buildMerkle (Stem t) = NStem (nodeHash child) + where child = buildMerkle t +buildMerkle (Fork l r) = NFork (nodeHash left) (nodeHash right) + where + left = buildMerkle l + right = buildMerkle r + +-- --------------------------------------------------------------------------- +-- DAG node-table export (for host-language kernel embedding) +-- --------------------------------------------------------------------------- + +-- | Export a term's Merkle DAG as a topologically-sorted node table. +-- Children appear before parents so all index references are forward. +-- Returns (root index, list of (tag, [child_indices])). +exportDag :: T -> (Int, [(String, [Int])]) +exportDag term = + let (root, acc, _) = collectDag term [] Set.empty + -- acc is in reverse post-order (children first, root last) + ordered = reverse acc + idxMap = Map.fromList [(h, i) | (i, (h, _)) <- zip [0..] ordered] + rootIdx = idxMap Map.! root + lines_ = map (formatNode idxMap . snd) ordered + in (rootIdx, lines_) + where + collectDag :: T -> [(MerkleHash, Node)] -> Set.Set MerkleHash -> (MerkleHash, [(MerkleHash, Node)], Set.Set MerkleHash) + collectDag Leaf acc seen = + let h = nodeHash NLeaf + in if Set.member h seen then (h, acc, seen) else (h, (h, NLeaf) : acc, Set.insert h seen) + collectDag (Stem t) acc seen = + let (ch, acc', seen') = collectDag t acc seen + node = NStem ch + h = nodeHash node + in if Set.member h seen' then (h, acc', seen') else (h, (h, node) : acc', Set.insert h seen') + collectDag (Fork l r) acc seen = + let (lh, acc', seen') = collectDag l acc seen + (rh, acc'', seen'') = collectDag r acc' seen' + node = NFork lh rh + h = nodeHash node + in if Set.member h seen'' then (h, acc'', seen'') else (h, (h, node) : acc'', Set.insert h seen'') + + formatNode :: Map.Map MerkleHash Int -> Node -> (String, [Int]) + formatNode _ NLeaf = ("leaf", []) + formatNode idxMap (NStem ch) = ("stem", [idxMap Map.! ch]) + formatNode idxMap (NFork l r) = ("fork", [idxMap Map.! l, idxMap Map.! r]) diff --git a/src/Runtime/Presentation.hs b/src/Runtime/Presentation.hs @@ -0,0 +1,76 @@ +module Runtime.Presentation where + +import Runtime.Tree +import Runtime.Value +import Data.List (intercalate) +import Data.Text (replace) +import qualified Data.Text as T + +-- Output formats +data EvaluatedForm = Tree | FSL | AST | Ternary | Ascii | Decode | Number | StringLit + deriving (Show) + +-- Outputs +formatT :: EvaluatedForm -> T -> String +formatT Tree = toSimpleT . show +formatT FSL = show +formatT AST = renderTreeAST +formatT Ternary = toTernaryString +formatT Ascii = toAscii +formatT Decode = decodeResult +formatT Number = either (\e -> "<not-number: " ++ e ++ ">") show . toNumber +formatT StringLit = either (\e -> "<not-string: " ++ e ++ ">") show . toString + +toSimpleT :: String -> String +toSimpleT s = T.unpack + $ replace "Fork" "t" + $ replace "Stem" "t" + $ replace "Leaf" "t" + $ T.pack s + +toTernaryString :: T -> String +toTernaryString Leaf = "0" +toTernaryString (Stem t) = "1" ++ toTernaryString t +toTernaryString (Fork t1 t2) = "2" ++ toTernaryString t1 ++ toTernaryString t2 + +-- Preserve the historical constructor display without depending on frontend ASTs. +renderTreeAST :: T -> String +renderTreeAST term = go False term + where + go _ Leaf = "TLeaf" + go nested (Stem a) = wrap nested ("TStem " ++ go True a) + go nested (Fork a b) = wrap nested ("TFork " ++ go True a ++ " " ++ go True b) + wrap True text = "(" ++ text ++ ")" + wrap False text = text + +toAscii :: T -> String +toAscii tree = go tree "" True + where + go :: T -> String -> Bool -> String + go Leaf prefix isLast = + prefix ++ (if isLast then "`-- " else "|-- ") ++ "Leaf\n" + go (Stem t) prefix isLast = + prefix ++ (if isLast then "`-- " else "|-- ") ++ "Stem\n" + ++ go t (prefix ++ (if isLast then " " else "| ")) True + go (Fork left right) prefix isLast = + prefix ++ (if isLast then "`-- " else "|-- ") ++ "Fork\n" + ++ go left (prefix ++ (if isLast then " " else "| ")) False + ++ go right (prefix ++ (if isLast then " " else "| ")) True + +decodeResult :: T -> String +decodeResult Leaf = "t" +decodeResult tc = + case (toString tc, toList tc, toNumber tc) of + (Right s, _, _) | all isCommonChar s -> "\"" ++ s ++ "\"" + (_, _, Right n) -> show n + (_, Right xs@(_:_), _) -> "[" ++ intercalate ", " (map decodeResult xs) ++ "]" + (_, Right [], _) -> "[]" + _ -> formatT Tree tc + where + isCommonChar c = + let n = fromEnum c + in (n >= 32 && n <= 126) + || n == 9 + || n == 10 + || n == 13 + diff --git a/src/Runtime/Tree.hs b/src/Runtime/Tree.hs @@ -0,0 +1,41 @@ +-- Lazy runtime trees and kernel reduction; no source-language representation. +module Runtime.Tree where + +import qualified Data.Map as Map + +-- Tree Calculus Types +data T = Leaf | Stem T | Fork T T + deriving (Show, Eq, Ord) + +-- Environment containing previously evaluated TC terms +type Env = Map.Map String T + +-- Triage Calculus Reduction Rules +{- + The t operator is left associative. + 1. t t a b -> a + 2. t (t a) b c -> a c (b c) + 3a. t (t a b) c t -> a + 3b. t (t a b) c (t u) -> b u + 3c. t (t a b) c (t u v) -> c u v +-} +apply :: T -> T -> T +apply (Fork Leaf a) _ = a +apply (Fork (Stem a) b) c = apply (apply a c) (apply b c) +apply (Fork (Fork _a _b) _c) Leaf = _a +apply (Fork (Fork _a _b) _c) (Stem u) = apply _b u +apply (Fork (Fork _a _b) _c) (Fork u v) = apply (apply _c u) v +-- Left associative `t` +apply Leaf b = Stem b +apply (Stem a) b = Fork a b + +-- Booleans +_false :: T +_false = Leaf + +_true :: T +_true = Stem Leaf + +_not :: T +_not = Fork (Fork _true (Fork Leaf _false)) Leaf + diff --git a/src/Runtime/Value.hs b/src/Runtime/Value.hs @@ -0,0 +1,75 @@ +-- Canonical runtime value encodings. Keep demand behavior explicit. +module Runtime.Value where + +import Runtime.Tree +import Data.Word (Word8) +import qualified Data.ByteString as BS + +-- | Encode a single byte (Word8) as a Tree Calculus number (0..255). +ofByte :: Word8 -> T +ofByte = ofNumber . fromIntegral + +-- | Decode a Tree Calculus number as a single byte (Word8). +-- Rejects values outside the range 0..255. +toByte :: T -> Either String Word8 +toByte t = case toNumber t of + Left err -> Left err + Right n + | n >= 0 && n <= 255 -> Right (fromIntegral n) + | otherwise -> Left ("Byte value out of range: " ++ show n) + +-- | Encode a ByteString as a Tree Calculus list of Byte trees. +ofBytes :: BS.ByteString -> T +ofBytes = ofList . map ofByte . BS.unpack + +-- | Decode a Tree Calculus list of Byte trees as a ByteString. +-- Rejects non-list trees and elements that are not valid byte values (0..255). +toBytes :: T -> Either String BS.ByteString +toBytes t = case toList t of + Left err -> Left err + Right bs -> BS.pack <$> mapM toByte bs + +-- Marshalling +ofString :: String -> T +ofString str = ofList $ map (ofNumber . toInteger . fromEnum) str + +ofNumber :: Integer -> T +ofNumber 0 = Leaf +ofNumber n = + Fork + (if odd n then Stem Leaf else Leaf) + (ofNumber (n `div` 2)) + +ofList :: [T] -> T +ofList = foldr Fork Leaf + +toNumber :: T -> Either String Integer +toNumber Leaf = Right 0 +toNumber (Fork Leaf rest) = case toNumber rest of + Right n -> Right (2 * n) + Left err -> Left err +toNumber (Fork (Stem Leaf) rest) = case toNumber rest of + Right n -> Right (1 + 2 * n) + Left err -> Left err +toNumber _ = Left "Invalid Tree Calculus number" + +toChar :: Integer -> Either String Char +toChar n + | n < 0 = Left "Negative character code" + | n > 0x10FFFF = Left "Character code out of Unicode range" + | n >= 0xD800 && n <= 0xDFFF = Left "Surrogate character code not allowed" + | otherwise = Right (toEnum (fromInteger n)) + +toString :: T -> Either String String +toString tc = do + list <- toList tc + nums <- mapM toNumber list + mapM toChar nums + +toList :: T -> Either String [T] +toList Leaf = Right [] +toList (Fork x rest) = case toList rest of + Right xs -> Right (x : xs) + Left err -> Left err +toList _ = Left "Invalid Tree Calculus list" + diff --git a/src/TreeObservation.hs b/src/TreeObservation.hs @@ -3,7 +3,7 @@ module TreeObservation (forceTreeShared) where import Control.Exception (evaluate) import qualified Data.IntMap.Strict as IntMap -import Research (T(..)) +import Runtime (T(..)) import System.Mem.StableName (StableName, makeStableName, hashStableName) data Visit = Enter T | Leave (StableName T) diff --git a/src/Wire.hs b/src/Wire.hs @@ -30,7 +30,7 @@ module Wire , defaultExportNames ) where -import Research hiding (Node) +import Runtime hiding (Node) import Control.Monad (foldM, forM_, unless, when) import Control.Exception (evaluate) @@ -515,7 +515,7 @@ bundleFromIndexedRoots finalNodes namedRoots = mkRoot 0 idx = BundleRoot idx "default" mkRoot _ idx = BundleRoot idx "root" --- Streaming canonical preorder -> postorder DAG. No expanded Research.T is +-- Streaming canonical preorder -> postorder DAG. No expanded Runtime.T is -- constructed. Global interning preserves buildBundle's left-before-right, -- first-occurrence numbering, including sharing between separate exports. -- Memory: unique DAG nodes + maximum tree depth + caller's current input chunk. diff --git a/test/Spec.hs b/test/Spec.hs @@ -76,7 +76,8 @@ allTestLibsEnv = unsafePerformIO $ do tests :: TestTree tests = testGroup "Tricu Tests" - [ interactionInspectionTests + [ runtimePresentationTests + , interactionInspectionTests , packageTests , lexer , parser @@ -128,6 +129,20 @@ tests = testGroup "Tricu Tests" -- Demand is tested with exceptions rather than unbounded divergent reductions. -- Physical sharing below is a Haskell implementation check, not a tree ABI promise. +-- Independent compatibility oracle: production rendering no longer imports ASTs. +runtimePresentationTests :: TestTree +runtimePresentationTests = testGroup "Runtime presentation" + [ testCase "AST constructor display agrees with recovery oracle" $ do + let trees 0 = [Leaf] + trees n = let children = trees (n - 1) + in Leaf : map Stem children ++ [Fork a b | a <- children, b <- children] + forM_ (trees (3 :: Int)) $ \term -> + formatT AST term @?= show (toAST term) + , testCase "AST nested constructor golden" $ + formatT AST (Fork (Stem Leaf) (Fork Leaf Leaf)) @?= + "TFork (TStem TLeaf) (TFork TLeaf TLeaf)" + ] + evaluationDemandTests :: TestTree evaluationDemandTests = localOption (mkTimeout 30000000) $ testGroup "Evaluation demand boundaries" [ testCase "constant application discards its argument" $ diff --git a/tricu.cabal b/tricu.cabal @@ -56,14 +56,11 @@ executable tricu , filepath , fsnotify , haskeline - , megaparsec , memory , mtl , network , stm , temporary - , tasty - , tasty-hunit , text , time , transformers @@ -84,18 +81,15 @@ executable tricu ContentStore.Filesystem ContentStore.Object ContentStore.Resolver - Eval - FileEval - Frontend.ContractDesugar IODriver - Lexer Module.Manifest - Module.Resolver - Module.Workspace - Parser Paths_tricu REPL - Research + Runtime + Runtime.Tree + Runtime.Value + Runtime.Presentation + Runtime.Merkle TreeObservation Wire default-language: Haskell2010 @@ -154,6 +148,11 @@ benchmark tricu-bench Parser Paths_tricu Research + Runtime + Runtime.Tree + Runtime.Value + Runtime.Presentation + Runtime.Merkle TreeObservation Wire @@ -228,6 +227,11 @@ test-suite tricu-tests Paths_tricu REPL Research + Runtime + Runtime.Tree + Runtime.Value + Runtime.Presentation + Runtime.Merkle TreeObservation Wire SupportFixture