tricu

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

commit 185b02106e4bcac9e36f3e0ebd46d39c965cf2b2
parent b99605e3168245139e3e63f1ec5878c0e8ecff26
Author: James Eversole <james@eversole.co>
Date:   Fri, 16 Oct 2026 14:29:17 -0500

Resolve native module sources through discoverable workspace maps

Stage explicit source locations for the existing portable compiler ABI; preserve sibling modules with fail-closed collision checks and isolate provenance replay. Compile the registry application directly through the repository map instead of manually copying dependencies.

Validated: nix flake check; workspace_compile.py (mapped/flat identity, edit invalidation, negative controls and provenance replay); provenance.py; nix build .#arboricxServerTest; source-patterns demo returns 23. Haskell host only; no compiler artifact change.

Diffstat:
Ademos/modules/tricu.workspace | 1+
Ademos/source-build/tricu.workspace | 1+
Ademos/source-map/tricu.workspace | 1+
Ademos/source-patterns/tricu.workspace | 1+
Mdocs/build-and-run.md | 4++--
Mdocs/language-features.md | 7++++---
Mdocs/modules.md | 58+++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mdocs/tri-tools.md | 12+++++++-----
Mdocs/validation.md | 20++++++++++++++++++++
Mflake.nix | 11+----------
Msrc/Module/Provenance.hs | 9++++++---
Msrc/PortableCompiler.hs | 9++++-----
Asrc/Workspace.hs | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/Spec.hs | 4+++-
Atest/WorkspaceTests.hs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/workspace_compile.py | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtricu-apps/arboricxServer.tri | 2+-
Mtricu.cabal | 3+++
Atricu.workspace | 41+++++++++++++++++++++++++++++++++++++++++
19 files changed, 396 insertions(+), 35 deletions(-)

diff --git a/demos/modules/tricu.workspace b/demos/modules/tricu.workspace @@ -0,0 +1 @@ +tricu.workspace.v1 diff --git a/demos/source-build/tricu.workspace b/demos/source-build/tricu.workspace @@ -0,0 +1 @@ +tricu.workspace.v1 diff --git a/demos/source-map/tricu.workspace b/demos/source-map/tricu.workspace @@ -0,0 +1 @@ +tricu.workspace.v1 diff --git a/demos/source-patterns/tricu.workspace b/demos/source-patterns/tricu.workspace @@ -0,0 +1 @@ +tricu.workspace.v1 diff --git a/docs/build-and-run.md b/docs/build-and-run.md @@ -97,8 +97,8 @@ exported programs. ## Deploy the Arboricx registry (Linux) The `arboricxServer` flake output builds the image `arboricx-server:latest`. -It compiles `tricu-apps/arboricxServer.tri` and its explicit native dependencies -with the packaged stable compiler at build time. Startup runs the compiled +It compiles `tricu-apps/arboricxServer.tri` directly using the repository +`tricu.workspace` source locations and packaged stable compiler at build time. Startup runs the compiled manifest from a read-only Nix store; it does not compile source, search `tricu.workspace`, or write a compiler cache into registry storage. diff --git a/docs/language-features.md b/docs/language-features.md @@ -34,7 +34,7 @@ separate optimized backend. | Compose failure-aware work | Result/Maybe helpers, parser combinators, IO helpers | They have different continuation/remainder protocols. | | Check a runtime boundary | Contracts and argument/result annotations | Runtime checks, not types or automatic host exceptions. | | Inspect code as data | Tree constructors, `triage`, structural equality/inspection | Equal application results do not imply equal function trees. | -| Organize/share code | Explicit native imports, namespaces, exports, and reexports | No implicit prelude or ambient workspace search. | +| Organize/share code | Explicit native imports, namespaces, exports, and reexports | No implicit prelude or registry lookup. | ## Functions and tree values @@ -206,8 +206,9 @@ can be imported with an explicit alias or `*`. `!export name` selects a local binding; `!export *` exports local definitions, **not imports**. `!export * from REF` explicitly reexports and acquires that dependency. -There is no ambient workspace lookup, implicit prelude, or fallback to another -frontend after compilation failure. Dependency cycles and name/export collisions +The CLI discovers source locations through the nearest `tricu.workspace`; it +does not search registries or mutable store aliases for source imports. There is +no implicit prelude or fallback to another frontend after compilation failure. Dependency cycles and name/export collisions are errors. The native profile requires a nonempty public interface and rejects bare final expressions; export `main` for CLI evaluation. See the [module contract](modules.md.html) for exact rules. diff --git a/docs/modules.md b/docs/modules.md @@ -14,11 +14,59 @@ trees, and an Arboricx bundle carries them for distribution. main = M.add 20 22 ``` -For the CLI's flat workspace, put dependencies in the input file's directory: -`prelude.tri`, `math.tri`, and their declared dependencies. Workspace module names -start with an ASCII letter or underscore and continue with letters, digits, -underscores, or hyphens. Use [Build and run](build-and-run.md.html) to select the stable -compiler, and [Tools](tri-tools.md.html) for larger mapped library workspaces. +The CLI discovers the nearest `tricu.workspace` in the input file's directory +or its ancestors, independently of the caller's working directory. Without a +declaration, dependencies remain sibling `NAME.tri` files. Module names start +with an ASCII letter or underscore and continue with letters, digits, +underscores, or hyphens. + +### Source locations without a build directory + +A workspace declaration maps import names to authored source paths. For example, +place this at your project root (columns are separated by an actual tab): + +```text +tricu.workspace.v1 +math<TAB>libraries/math.tri +prelude<TAB>libraries/prelude.tri +``` + +Then `apps/main.tri` can import `math` and `prelude` without copying files beside +it. Paths are relative to the declaration, must end in `.tri`, and can include +`..` for explicitly shared sources. LF endings and a final newline are required; +comments and blank lines are not supported. Declarations are limited to 64 KiB; +names to 128 ASCII characters. All mapped files must exist. Direct symlinks and +nonregular input files are rejected; use stable, trusted directories. + +The repository declaration provides its native library locations, including +`native_math` and `arboricx_server`. Compile the application directly: + +```sh +result/bin/tricu arboricx compile --file tricu-apps/arboricxServer.tri \ + --store "$WORK/application" --output "$WORK/server.arboricx" \ + --compile-timeout 900 +``` + +Sibling modules remain available. A sibling and mapping with the same name must +refer to the same file; conflicting and case-colliding names fail rather than +silently shadowing one another. Parent declarations are not merged. A nearer +header-only `tricu.workspace` establishes an isolated flat workspace; the +self-contained module demos use this boundary. + +The host copies the selected source locations into a temporary flat workspace +and removes it after compilation, including on failure. Source text is unchanged; +the portable compiler still reads headers, discovers the reached dependency +closure, rejects cycles, and owns compilation and cache semantics. Only reached +modules are compiled, though all mapped and validly named sibling files are +copied. This is not registry fetching, recursive library discovery, source +rewriting, or a new compiler backend. Keep sources stable during the copy/build. +Full-hash imports and package-generated binding modules work as before. + +`eval`, `arboricx compile`, and provenance capture share these location rules. +Capture retains reached source bytes under their module names; replay needs no +original directory layout or workspace declaration. Workspace paths themselves +are not executable or receipt identities. Use [Build and run](build-and-run.md.html) +to select the stable compiler. ## Headers diff --git a/docs/tri-tools.md b/docs/tri-tools.md @@ -15,8 +15,9 @@ result/bin/tricu arboricx compile --file demos/source-patterns/app.tri \ --store "$WORK/application" --name main --output "$WORK/app.arboricx" ``` -The input file's directory is the flat workspace. Declared dependencies resolve -to `NAME.tri` there. The module basename must be a valid workspace identifier. +Dependencies use sibling `NAME.tri` files plus the nearest `tricu.workspace` +source-location declaration; see [workspace rules](modules.md.html#source-locations-without-a-build-directory). +The module basename must be a valid workspace identifier. `eval` accepts one file and selects exported `main`. `compile` packages all published exports, or the subset selected by repeatable `--name` options. Missing or duplicate selections fail. @@ -266,9 +267,10 @@ cancellation fail before replacement. See the ## Library workspaces and demos -Compiler dependencies use source-map aliases, so prepare an explicit workspace -rather than compiling `lib/source/*.tri` against an assumed directory search. -For a completed generation and a new `DEMO_DIRECTORY`: +The repository `tricu.workspace` maps native library aliases to source paths; +ordinary CLI compilation needs no manually staged directory. Bootstrap source +maps remain a separate compiler-rebuilding protocol. To reuse already compiled +dependency manifests from a completed generation in a new `DEMO_DIRECTORY`: ```sh python3 test/native_library_migration.py result/bin/tricu "$GENERATION" \ diff --git a/docs/validation.md b/docs/validation.md @@ -1101,3 +1101,23 @@ options, receipts and executables, malformed inventories, path traversal, symlinked CAS directories, an unauthorized compiler, and verification after all producer files have been removed. Verification snapshots assert no file/mtime changes. Each child has a 512 MiB GHC heap ceiling and a 30-second deadline. + +## CLI workspace source locations + +`nix flake check` includes declaration parsing, ancestor discovery, nearest-file +boundaries, collision/missing-file rejection, and temporary snapshot cleanup. +For real packaged-compiler coverage on the Haskell host: + +```sh +nix build .# +python3 test/workspace_compile.py result/bin/tricu +nix build .#arboricxServerTest --no-link +``` + +The focused harness compiles a nested application with transitive reexports from +an unrelated working directory, compares its bundle byte-for-byte with a flat +workspace build, captures and independently replays the mapped inputs beneath a +conflicting parent declaration, verifies cache invalidation after a source edit, and retains +negative cycle, missing-import, and source-collision controls. The deployment +check compiles the actual repository application via `tricu.workspace` and +exercises its server behavior. diff --git a/flake.nix b/flake.nix @@ -72,17 +72,8 @@ arboricxServerBundle = pkgs.runCommand "arboricx-server-bundle" { nativeBuildInputs = [ tricuPackage ]; } '' - mkdir workspace - ${pkgs.lib.concatMapStringsSep "\n" (name: - "cp ${./lib}/${name}.tri workspace/${name}.tri") [ - "base" "prelude" "bytes" "conversions" "io" - "parser" "http" "socket" - ]} - cp ${./lib/math/native.tri} workspace/native_math.tri - cp ${./lib/arboricx/server.tri} workspace/arboricx_server.tri - cp ${./tricu-apps/arboricxServer.tri} workspace/main.tri mkdir -p $out - tricu arboricx compile --file workspace/main.tri --store build-store \ + tricu arboricx compile --file ${./.}/tricu-apps/arboricxServer.tri --store build-store \ --output $out/server.arboricx --compile-timeout 900 \ +RTS -N1 -M8192m -RTS tricu arboricx import --file $out/server.arboricx --store $out/runtime \ diff --git a/src/Module/Provenance.hs b/src/Module/Provenance.hs @@ -2,6 +2,7 @@ module Module.Provenance ( capture, verifyPack, prepareRebuild, finishRebuild, Recipe(..) ) where +import Workspace (workspaceSources) import ContentStore.Object import ContentStore.Alias (ObjectRef(..)) import ContentStore.Filesystem (objectPath, putObject) @@ -233,9 +234,9 @@ cacheReceipt (StorePath store) name = do capture :: FilePath -> StorePath -> StorePath -> Text -> Maybe Text -> Maybe (FilePath,FilePath) -> FilePath -> IO Text capture file app tools compiler selectedReceipt project output = do - unless (takeExtension file == ".tri" && J.validBinding (T.pack (takeBaseName file))) $ fail "capture requires a flat native .tri entry" + unless (takeExtension file == ".tri" && J.validBinding (T.pack (takeBaseName file))) $ fail "capture requires a native .tri entry" let entry = T.pack (takeBaseName file) - workspace = takeDirectory file + sources <- workspaceSources file receipt <- maybe (cacheReceipt app entry) (\p -> either fail pure (validateObjectHash p) >> pure p) selectedReceipt context <- case project of Nothing -> pure Nothing @@ -254,7 +255,7 @@ capture file app tools compiler selectedReceipt project output = do toolManifest <- either fail pure (decodeManifest toolManifestBytes) let toolPins = S.fromList ((unDomain manifestDomain,compiler) : [(objectRefKind r,objectRefHash r) | ex <- moduleManifestExports toolManifest, let r = moduleExportObject ex]) combined d p = if S.member (unDomain d,p) toolPins then loadStore tools d p else load d p - c <- collect (Recipe entry receipt compiler [] ctx) combined (\name _ -> bounded 1048576 (workspace </> T.unpack name ++ ".tri")) + c <- collect (Recipe entry receipt compiler [] ctx) combined (\name _ -> maybe (fail "receipt source absent from workspace") (bounded 1048576) (M.lookup (T.unpack name) sources)) -- Publication is create-only; a failed capture may leave an incomplete directory -- with no descriptor. Never replace or merge another pack. createDirectoryIfMissing True (takeDirectory output) @@ -306,6 +307,8 @@ prepareRebuild directory pin trusted output = do _ <- putObject target d payload pure () createDirectory workspace + -- Replay must never inherit source mappings from the output directory's ancestors. + BS.writeFile (workspace </> "tricu.workspace") "tricu.workspace.v1\n" forM_ (M.toList (cReceipts c)) $ \(name,(_,r)) -> obj sourceDomain (rSource r) >>= BS.writeFile (workspace </> T.unpack name ++ ".tri") case recipeProject recipe of Nothing -> pure () diff --git a/src/PortableCompiler.hs b/src/PortableCompiler.hs @@ -6,6 +6,7 @@ module PortableCompiler ) where import ContentStore +import Workspace (withWorkspace) import ContentStore.Bundle (unpackIndexedBundleToStore, indexedTreeDomain) import ContentStore.Executable import Module.Manifest @@ -16,7 +17,7 @@ import Crypto.Hash (Digest, SHA256, hash) import Control.Exception (IOException, catch) import Control.Monad (when, unless) import System.Directory (canonicalizePath, createDirectoryIfMissing) -import System.FilePath (takeExtension, takeDirectory, takeBaseName) +import System.FilePath (takeExtension, takeBaseName) import System.Timeout (timeout) import System.IO (stderr) import GHC.Clock (getMonotonicTimeNSec) @@ -88,8 +89,7 @@ validateCompilerOptions options = do unless (compilerSeconds options >= 1 && compilerSeconds options <= 86400) $ fail "--compile-timeout must be between 1 and 86400 seconds" --- Native flat-workspace profile: the selected file supplies the directory and --- target name; local import/re-export edges select NAME.tri within that explicit base. +-- Workspace location preparation preserves the native flat-workspace ABI. -- All header staging, scope, compilation, publication and cache policy is tricu. buildPortableModule :: CompilerOptions -> Maybe FilePath -> FilePath -> IO (StorePath, T.Text) buildPortableModule options appStore input = do @@ -112,9 +112,8 @@ buildPortableResult options appStore input = do createDirectoryIfMissing True (unStorePath store0) root <- canonicalizePath (unStorePath store0) let store = StorePath root - directory = takeDirectory source name = takeBaseName source - built <- compilerDeadline options $ do + built <- compilerDeadline options $ withWorkspace input $ \directory -> do (compilerIdentity, worker) <- loadCompiler sample options store let action = foldl apply worker (map ofString [directory, root, compilerIdentity, name]) diff --git a/src/Workspace.hs b/src/Workspace.hs @@ -0,0 +1,99 @@ +-- Source location policy only: the portable compiler still owns all header, +-- dependency, scope, cycle and cache semantics. No source rewriting occurs here. +module Workspace (workspaceSources, withWorkspace, parseWorkspace) where + +import Control.Monad (foldM, forM, forM_, unless) +import qualified Data.ByteString as BS +import qualified Data.Map.Strict as M +import qualified Data.Text as T +import Data.Text.Encoding (decodeUtf8') +import Data.Char (toLower) +import Data.List (sort) +import System.Directory +import System.FilePath +import System.IO (withBinaryFile, IOMode(ReadMode)) +import System.Posix.Files (getSymbolicLinkStatus, isRegularFile) +import System.IO.Temp (withSystemTempDirectory) + +validName :: String -> Bool +validName [] = False +validName (c:cs) = first c && all (\x -> first x || x >= '0' && x <= '9' || x == '-') cs + where first x = x >= 'a' && x <= 'z' || x >= 'A' && x <= 'Z' || x == '_' + +parseWorkspace :: BS.ByteString -> Either String [(String, FilePath)] +parseWorkspace bytes = do + unless (BS.length bytes <= 65536) $ Left "workspace exceeds 64 KiB" + text <- either (const $ Left "workspace must be UTF-8") Right (decodeUtf8' bytes) + unless (T.isSuffixOf "\n" text && not (T.any (== '\r') text)) $ + Left "workspace requires LF lines and a final newline" + case T.lines text of + "tricu.workspace.v1":rows -> reverse . snd <$> foldM row ([], []) rows + _ -> Left "invalid workspace header (expected tricu.workspace.v1)" + where + row (seen, entries) line = case map T.unpack (T.splitOn "\t" line) of + [name,path] -> do + unless (validName name && length name <= 128) $ Left "invalid workspace module name" + unless (not (null path) && not (isAbsolute path) && takeExtension path == ".tri" && all (>= ' ') path) $ + Left "workspace paths must be relative .tri paths" + let key = map toLower name + unless (key `notElem` seen) $ Left ("duplicate workspace module: " ++ name) + pure (key:seen, (name,path):entries) + _ -> Left "workspace rows require NAME<TAB>PATH" + +findWorkspace :: FilePath -> IO (Maybe FilePath) +findWorkspace directory = do + let path = directory </> "tricu.workspace" + exists <- doesPathExist path + if exists then pure (Just path) + else if takeDirectory directory == directory then pure Nothing + else findWorkspace (takeDirectory directory) + +-- The nearest declaration is authoritative; parent declarations are not merged. +-- Sibling modules remain available, but may not silently shadow mapped modules. +workspaceSources :: FilePath -> IO (M.Map String FilePath) +workspaceSources input = do + regularSource input + source <- canonicalizePath input + let directory = takeDirectory source + unless (takeExtension source == ".tri" && validName (takeBaseName source)) $ + fail "Portable compilation requires a .tri module with a valid module basename" + declaration <- findWorkspace directory + mapped <- case declaration of + Nothing -> pure [] + Just path -> do + regularSource path + bytes <- withBinaryFile path ReadMode (\h -> BS.hGet h 65537) + rows <- either (fail . ((path ++ ": ") ++)) pure (parseWorkspace bytes) + forM rows $ \(name,relative) -> do + regularSource (takeDirectory path </> relative) + resolved <- canonicalizePath (takeDirectory path </> relative) + exists <- doesFileExist resolved + unless exists $ fail ("Workspace module missing: " ++ name ++ " at " ++ resolved) + pure (name,resolved) + siblings <- sort <$> listDirectory directory + local <- forM [p | p <- siblings, takeExtension p == ".tri", validName (takeBaseName p)] $ \p -> do + regularSource (directory </> p) + resolved <- canonicalizePath (directory </> p) + pure (takeBaseName p,resolved) + foldM insert M.empty (mapped ++ local) + where + insert entries (name,path) = do + let collisions = [(n,p) | (n,p) <- M.toList entries, map toLower n == map toLower name] + case collisions of + [] -> pure (M.insert name path entries) + [(n,p)] | n == name && p == path -> pure entries + _ -> fail ("Ambiguous workspace module: " ++ name ++ " (mapped and/or sibling sources conflict)") + +-- Snapshot locations into the existing compiler ABI. Temporary paths are not +-- build identities; receipts use module names and source bytes, as before. +withWorkspace :: FilePath -> (FilePath -> IO a) -> IO a +withWorkspace input action = do + sources <- workspaceSources input + withSystemTempDirectory "tricu-workspace" $ \directory -> do + forM_ (M.toList sources) $ \(name,path) -> copyFile path (directory </> name <.> "tri") + action directory + +regularSource :: FilePath -> IO () +regularSource path = do + status <- getSymbolicLinkStatus path + unless (isRegularFile status) $ fail ("Workspace inputs require regular files (no symlinks): " ++ path) diff --git a/test/Spec.hs b/test/Spec.hs @@ -1,5 +1,6 @@ module Main where +import WorkspaceTests (workspaceTests) import IOBoundaryTests (ioBoundaryTests) import PortableCompilerTests (portableCompilerTests) import ExecutableLoadingTests (executableLoadingTests) @@ -58,7 +59,8 @@ main = do tests :: TestTree tests = testGroup "Tricu Tests" - [ ioBoundaryTests + [ workspaceTests + , ioBoundaryTests , portableCompilerTests , executableLoadingTests , runtimePresentationTests diff --git a/test/WorkspaceTests.hs b/test/WorkspaceTests.hs @@ -0,0 +1,73 @@ +module WorkspaceTests (workspaceTests) where + +import Workspace +import Control.Exception (IOException, try) +import qualified Data.ByteString as BS +import qualified Data.Map.Strict as M +import System.Directory +import System.FilePath ((</>)) +import System.IO.Temp (withSystemTempDirectory) +import Test.Tasty +import Test.Tasty.HUnit + +workspaceTests :: TestTree +workspaceTests = testGroup "workspace source locations" + [ testCase "strict declarations" $ do + parseWorkspace "tricu.workspace.v1\nmath\tlib/math.tri\n" @?= Right [("math","lib/math.tri")] + mapM_ (\bytes -> case parseWorkspace bytes of + Left _ -> pure () + Right _ -> assertFailure "accepted invalid declaration") + [ "tricu.workspace.v1\nmath\ta.tri\nMath\tb.tri\n" + , "tricu.workspace.v1\nmath\ta.tri" + , "tricu.workspace.v1\nmath\t/a.tri\n" + , "tricu.workspace.v1\n../math\ta.tri\n" + , "tricu.workspace.v1\r\n" + , "tricu.workspace.v1\n\n" + , "tricu.workspace.v1\nx\ta.tri\textra\n" + , BS.pack [255] + ] + , testCase "parent discovery, exact source copies, cleanup, and nearest declaration" $ fresh $ \root -> do + let app = root </> "app"; lib = root </> "lib" + createDirectory app + createDirectory lib + BS.writeFile (app </> "main.tri") "!import math as M\n!export main\nmain = M.answer\n" + BS.writeFile (lib </> "number.tri") "!export answer\nanswer = 42\n" + BS.writeFile (root </> "tricu.workspace") "tricu.workspace.v1\nmath\tlib/number.tri\n" + sources <- workspaceSources (app </> "main.tri") + M.keys sources @?= ["main", "math"] + temp <- withWorkspace (app </> "main.tri") $ \dir -> do + BS.readFile (dir </> "math.tri") >>= (@?= "!export answer\nanswer = 42\n") + pure dir + doesPathExist temp >>= (@?= False) + BS.writeFile (app </> "tricu.workspace") "tricu.workspace.v1\n" + workspaceSources (app </> "main.tri") >>= ((@?= ["main"]) . M.keys) + , testCase "no silent local shadowing or missing mapped sources" $ fresh $ \root -> do + BS.writeFile (root </> "main.tri") "!export main\nmain = 1\n" + BS.writeFile (root </> "other.tri") "!export main\nmain = 2\n" + BS.writeFile (root </> "tricu.workspace") "tricu.workspace.v1\nmain\tother.tri\n" + rejects (workspaceSources (root </> "main.tri")) + BS.writeFile (root </> "tricu.workspace") "tricu.workspace.v1\nmissing\tabsent.tri\n" + rejects (workspaceSources (root </> "main.tri")) + , testCase "mapping the same sibling is not ambiguous" $ fresh $ \root -> do + BS.writeFile (root </> "main.tri") "!export main\nmain = 1\n" + BS.writeFile (root </> "tricu.workspace") "tricu.workspace.v1\nmain\tmain.tri\n" + workspaceSources (root </> "main.tri") >>= ((@?= ["main"]) . M.keys) + , testCase "direct source symlinks are rejected" $ fresh $ \root -> do + BS.writeFile (root </> "target.tri") "!export main\nmain = 1\n" + createFileLink (root </> "target.tri") (root </> "main.tri") + rejects (workspaceSources (root </> "main.tri")) + , testCase "temporary snapshot removed on failure" $ fresh $ \root -> do + BS.writeFile (root </> "main.tri") "!export main\nmain = 1\n" + let marker = root </> "location" + rejects $ withWorkspace (root </> "main.tri") $ \dir -> do + writeFile marker dir + fail "compiler failure" :: IO () + readFile marker >>= doesPathExist >>= (@?= False) + ] + where + fresh = withSystemTempDirectory "tricu-workspace-test" + rejects action = do + result <- try (action >> pure ()) :: IO (Either IOException ()) + case result of + Left _ -> pure () + Right _ -> assertFailure "expected workspace error" diff --git a/test/workspace_compile.py b/test/workspace_compile.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Haskell-only real-compiler workspace acceptance; no retained build artifacts. +Usage: python3 test/workspace_compile.py /absolute/path/to/tricu +""" +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile + + +def main(): + host = str(Path(sys.argv[1]).resolve()) + with tempfile.TemporaryDirectory(prefix="tricu-workspace-acceptance-") as tmp: + root = Path(tmp) + project = root / "project" + app = project / "apps" + lib = project / "lib" + app.mkdir(parents=True) + lib.mkdir() + entry = app / "main.tri" + entry.write_text("!import facade as F\n!export main\nmain = F.answer\n") + (lib / "facade-source.tri").write_text("!export * from numbers\n") + (lib / "numbers-source.tri").write_text("!export answer\nanswer = 42\n") + declaration = project / "tricu.workspace" + declaration.write_text("tricu.workspace.v1\nfacade\tlib/facade-source.tri\nnumbers\tlib/numbers-source.tri\n") + + def run(*args, success=True): + p = subprocess.run([host, *map(str, args), "+RTS", "-N1", "-RTS"], + cwd=root, text=True, capture_output=True, timeout=180) + assert (p.returncode == 0) == success, (args, p.stdout, p.stderr) + return p.stdout + + store = root / "store" + assert run("eval", entry, "--store", store, "--format", "number").strip() == "42" + mapped_bundle = root / "mapped.arboricx" + run("arboricx", "compile", "--file", entry, "--store", store, "--output", mapped_bundle) + flat = root / "flat" + flat.mkdir() + shutil.copyfile(entry, flat / "main.tri") + shutil.copyfile(lib / "facade-source.tri", flat / "facade.tri") + shutil.copyfile(lib / "numbers-source.tri", flat / "numbers.tri") + flat_bundle = root / "flat.arboricx" + run("arboricx", "compile", "--file", flat / "main.tri", "--store", root / "flat-store", "--output", flat_bundle) + assert mapped_bundle.read_bytes() == flat_bundle.read_bytes(), "location changed bundle identity" + # Capture shares the location resolver; replay is isolated even under a + # parent declaration that would otherwise conflict with retained sources. + tools = root / "tools" + shipped = Path(__file__).resolve().parent.parent / "bootstrap/build-stable.arboricx" + imported = run("arboricx", "import", "--file", shipped, "--store", tools) + compiler = re.search(r"Created module alias .* -> ([0-9a-f]{64})", imported)[1] + run("arboricx", "compile", "--file", entry, "--store", store, + "--toolchain", compiler, "--tool-store", tools, "--output", mapped_bundle) + pack = root / "inputs" + captured = run("provenance", "capture", "--file", entry, "--store", store, + "--toolchain", compiler, "--tool-store", tools, "--output", pack) + pin = re.search(r"Retained build inputs: ([0-9a-f]{64})", captured)[1] + run("provenance", "rebuild", "--pack", pack, "--pin", pin, + "--trust-compiler", compiler, "--output", project / "replayed") + # Cache must observe source edits, not just declaration paths. + (lib / "numbers-source.tri").write_text("!export answer\nanswer = 7\n") + assert run("eval", entry, "--store", store, "--format", "number").strip() == "7" + # Ordinary compiler controls remain in force through the mapped route. + (lib / "numbers-source.tri").write_text("!import facade as F\n!export answer\nanswer = F.answer\n") + run("eval", entry, "--store", store, success=False) + (lib / "numbers-source.tri").write_text("!import absent as A\n!export answer\nanswer = A.answer\n") + run("eval", entry, "--store", store, success=False) + (app / "facade.tri").write_text("!export answer\nanswer = 99\n") + run("eval", entry, "--store", store, success=False) + print("workspace compile acceptance passed") + + +if __name__ == "__main__": + main() diff --git a/tricu-apps/arboricxServer.tri b/tricu-apps/arboricxServer.tri @@ -5,7 +5,7 @@ -- Arboricx HTTP registry server. -- Build/deploy: nix build .#arboricxServer (see docs/build-and-run.md). -- The runner grants reads/writes only to ./store; dependencies are compiled --- in the explicit flat workspace prepared by flake.nix. +-- from their declared locations in the repository tricu.workspace. -- -- Endpoints: -- GET / -> HTML landing page diff --git a/tricu.cabal b/tricu.cabal @@ -87,6 +87,7 @@ executable tricu Paths_tricu REPL PortableCompiler + Workspace Runtime Runtime.Tree Runtime.Value @@ -180,6 +181,7 @@ test-suite tricu-tests PackageTests ExecutableLoadingTests PortableCompilerTests + WorkspaceTests InteractionInspectionTests ContentStore ContentStore.Alias @@ -196,6 +198,7 @@ test-suite tricu-tests Paths_tricu REPL PortableCompiler + Workspace Runtime Runtime.Tree Runtime.Value diff --git a/tricu.workspace b/tricu.workspace @@ -0,0 +1,41 @@ +tricu.workspace.v1 +arboricx lib/arboricx/arboricx.tri +arboricx_common lib/arboricx/common.tri +arboricx_dispatch lib/arboricx/dispatch.tri +arboricx_manifest lib/arboricx/manifest.tri +arboricx_nodes lib/arboricx/nodes.tri +arboricx_server lib/arboricx/server.tri +base lib/base.tri +binary lib/binary.tri +bytes lib/bytes.tri +conversions lib/conversions.tri +graph lib/graph.tri +http lib/http.tri +intensional lib/intensionalContracts.tri +io lib/io.tri +lexer lib/lexer.tri +native_math lib/math/native.tri +parser lib/parser.tri +patterns lib/patterns.tri +prelude lib/prelude.tri +socket lib/socket.tri +source_ast lib/source/ast.tri +source_build lib/source/build.tri +source_build_stable lib/source/build-stable.tri +source_bundle lib/source/bundle.tri +source_compile lib/source/compile.tri +source_core lib/source/core.tri +source_equations lib/source/equations.tri +source_export_cache lib/source/export-cache.tri +source_lexer lib/source/lexer.tri +source_lock lib/source/lock.tri +source_map lib/source/map.tri +source_module lib/source/module.tri +source_object lib/source/object.tri +source_order lib/source/order.tri +source_parser lib/source/parser.tri +source_pattern lib/source/pattern.tri +source_program lib/source/program.tri +source_resolve lib/source/resolve.tri +source_stable lib/source/stable.tri +source_unit lib/source/unit.tri