tricu

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

commit 583f6d6d52331001ec375226710d0f53f1722f32
parent 256bfc928b59d4acf9cb6731e7c2d7f23c9906ea
Author: James Eversole <james@eversole.co>
Date:   Wed, 14 Oct 2026 18:26:11 -0500

Require complete provider identity for retained fixture reuse

Diffstat:
Mdocs/validation.md | 25++++++++++++++++++-------
Mtest/ReferenceFixtures.hs | 4++--
Mtest/Spec.hs | 63+++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mtest/SupportFixture.hs | 86+++++++++++++++++++++++++++++++++++++------------------------------------------
Mtest/repair_support_dependencies.py | 2+-
Mtest/repair_support_fixture.py | 2+-
6 files changed, 111 insertions(+), 71 deletions(-)

diff --git a/docs/validation.md b/docs/validation.md @@ -158,8 +158,8 @@ compilation. `test/ParserValueTests.hs` does the same for binary and finite-stre parser protocols, including consumed/committed flags, rollback, zero-width repetition, and poisoned-field controls. HTTP framing/error expectations and no-socket-access controls remain independent of the compiled implementations. Mixed-cohort reuse verification in -`SupportFixture` still depends on recovery parsing/free-variable analysis; direct -artifact invocation does not remove that separate provenance dependency. +`SupportFixture` uses complete provider identity; it has no recovery parser or +free-variable-analysis dependency. `test/IODriverTests.hs` uses explicit ABI trees for primitive/scheduler controls and portable-compiled programs for library sequencing, state, concurrency and @@ -431,11 +431,22 @@ published/rebuilt kernel equality. Support-provider manifests in receipts must match the selected cohort; an unchanged consumer body is not sufficient evidence for reusing an artifact linked against an older provider. -Origin-retained modules still require unchanged source fingerprints/output trees; -checks use authenticated snapshot bodies to verify lexical free references, -namespaces, imports, reexports, and collisions. Prefer rebuilding affected consumers -to relaxing these reuse checks. Future source changes need explicit candidate -coverage and genuine fixture publication at acceptance. +Origin-retained modules require unchanged source fingerprints/output trees and +identical **complete interfaces of every imported or reexported provider**. +Comparison uses exact constructor/child interning across the bundles, not local +node indices or probabilistic tree hashes. Provider membership, every export name +and every executable tree must match; this also preserves namespace and collision +outcomes. Unrelated providers may change. Changes to unused or shadowed imports +now require rebuilding: the former free-name-based reuse optimization is retired. + +This is an identity proof relative to an authenticated, accepted original cohort, +not a source validator or a new compilation receipt. The verifier recognizes the +fixture's simple named-provider imports and reexports; unsupported directives, +including build-only `!source` dependencies, fail closed and require rebuilding. +No body parser or scope inference is involved. Prefer rebuilding affected consumers +to relaxing these checks. The current 39-module cohort is entirely rebuilt, so +this stricter reuse policy does not require regenerating its fixtures. Future +source changes still need candidate coverage and genuine publication at acceptance. For a justified narrow repair, `test/repair_support_dependencies.py` materializes full original dependency manifests (and asserts their original identities), and diff --git a/test/ReferenceFixtures.hs b/test/ReferenceFixtures.hs @@ -1,6 +1,6 @@ -- Authenticated retained library artifacts. This module does not compile source --- or recreate source import scopes. SupportFixture still uses recovery parsing --- for mixed-cohort reuse proofs; that dependency is explicit, not a fallback. +-- or recreate source import scopes. SupportFixture proves conservative reuse +-- by complete provider identity, without recovery parsing or scope analysis. module ReferenceFixtures ( compilerFixture, supportFixture, compiledLibraries, fixtureExports ) where diff --git a/test/Spec.hs b/test/Spec.hs @@ -17,7 +17,7 @@ import Research import TreeObservation (forceTreeShared) import qualified NodeTable as Nodes import Wire -import SupportFixture (verifyReuseScopes, verifySupportSources, verifyRelocatedSupportSources, +import SupportFixture (canonicalEnvs, verifyReuseProviders, verifySupportSources, verifyRelocatedSupportSources, verifySupportExports, verifySupportDependencies, readFixtureSourceBytes, readTestSource, readPinnedSource) import ContentStore import ContentStore.Bundle @@ -3764,25 +3764,60 @@ compilerFixtureTests = testGroup "self-built reference compiler fixture" case outcome of Left _ -> pure () Right _ -> assertFailure "reference cohort widened beyond six foundations" - , testCase "mixed support reuse rejects changed used kernel, not unused changes" $ do + , testCase "mixed support reuse requires all provider trees, including unused ones" $ do let before = Map.fromList [("P.used",1),("P.unused",2)] source = "!import P as X\n!export f\nf x = X.used x\n" - verifyReuseScopes before (Map.insert "P.unused" 3 before) source @?= Right () - assertBool "changed used import accepted" - (case verifyReuseScopes before (Map.insert "P.used" 4 before) source of Left _ -> True; _ -> False) - assertBool "changed full export set accepted" - (case verifyReuseScopes before (Map.insert "P.extra" 4 before) source of Left _ -> True; _ -> False) - , testCase "mixed support reuse respects lexical binders and reexports" $ do + verifyReuseProviders before before source @?= Right () + forM_ ["P.used", "P.unused"] $ \name -> + verifyReuseProviders before (Map.insert name 3 before) source @?= + Left "provider kernels changed: P" + forM_ [Map.insert "P.extra" 4 before, Map.delete "P.unused" before, Map.empty] $ \after -> + verifyReuseProviders before after source @?= Left "full export set changed: P" + verifyReuseProviders Map.empty before source @?= Left "missing original provider: P" + , testCase "mixed support reuse no longer exempts shadowed imports" $ do let before = Map.fromList [("P.used",1),("P.unused",2)] shadowed = "!import P as X\n!export f\nf used = used\n" - verifyReuseScopes before (Map.insert "P.used" 4 before) shadowed @?= Right () - assertBool "changed reexport accepted" - (case verifyReuseScopes before (Map.insert "P.used" 4 before) "!export * from P\n" of Left _ -> True; _ -> False) + verifyReuseProviders before before shadowed @?= Right () + verifyReuseProviders before (Map.insert "P.used" 4 before) shadowed @?= + Left "provider kernels changed: P" + , testCase "mixed support reuse checks standalone reexports and all import forms" $ do + let before = Map.fromList [("P.used",1),("P.unused",2),("PQ.other",3)] + forM_ ["!import P", "!import P *", "!import P as X", "!export * from P", + " !import P *", "!import P *\n!export * from P"] $ \header -> do + let source = header ++ "\n!export f\nf x = x\n" + verifyReuseProviders before before source @?= Right () + verifyReuseProviders before (Map.insert "PQ.other" 4 before) source @?= Right () + verifyReuseProviders before (Map.insert "P.used" 4 before) source @?= + Left "provider kernels changed: P" + verifyReuseProviders before before "!export * from Missing\n" @?= + Left "missing original provider: Missing" , testCase "mixed support reuse rejects changed namespace collision outcomes" $ do let before = Map.fromList [("P.used",1),("Q.used",1)] - assertBool "new conflicting duplicate import accepted" - (case verifyReuseScopes before (Map.insert "Q.used" 2 before) - "!import P *\n!import Q *\n!export f\nf x = x\n" of Left _ -> True; _ -> False) + source = "!import P *\n!import Q *\n!export f\nf x = x\n" + verifyReuseProviders before before source @?= Right () + verifyReuseProviders before (Map.insert "Q.used" 2 before) source @?= + Left "provider kernels changed: Q" + , testCase "mixed support reuse rejects unsupported dependency directives" $ do + let before = Map.singleton "P.used" 1 + forM_ ["!import", "!import P as", "!import P * extra", "!import P as *", + "!import \"P\" *", "!export * from", "!export * from P extra", + "!source P", "!unknown P", "!import " ++ replicate 64 'a' ++ " *"] $ \header -> + assertBool ("accepted unsupported reuse header: " ++ header) + (case verifyReuseProviders before before (header ++ "\n") of Left _ -> True; _ -> False) + , testCase "mixed support reuse compares canonical trees, not bundle-local node indices" $ do + let original = buildBundle [("P.used", Stem Leaf), ("P.unused", Fork Leaf Leaf)] + reordered = buildBundle [("Q.extra", ofNumber 42), ("P.unused", Fork Leaf Leaf), + ("P.used", Stem Leaf)] + changed = buildBundle [("P.used", Stem Leaf), ("P.unused", Fork Leaf (Stem Leaf))] + forM_ [original, reordered, changed] $ \bundle -> verifyBundle bundle @?= Right () + case canonicalEnvs [original, reordered, changed] of + [before, same, different] -> do + verifyReuseProviders before same "!import P *\n" @?= Right () + verifyReuseProviders before different "!import P *\n" @?= + Left "provider kernels changed: P" + _ -> assertFailure "incorrect canonical environment count" + , testCase "mixed support reuse permits provider-free unchanged programs" $ + verifyReuseProviders Map.empty Map.empty "-- no dependencies\n!export main\nmain = t\n" @?= Right () , testCase "support publication allows new rebuilt sources, not relabeled retained sources" $ do let old = [["module","http","http","lib/http.tri","old-source"]] new = [["module","http","http","lib/http.tri","new-source"]] diff --git a/test/SupportFixture.hs b/test/SupportFixture.hs @@ -1,12 +1,9 @@ -- Test-only mixed-origin fixture verification. No source compilation/evaluation: --- parse/freeVars, exact DAG interning, complete namespaces, receipt authentication. -module SupportFixture (verifyMixedSupport, verifyReuseScopes, verifySupportSources, +-- complete provider identity, exact DAG interning and receipt authentication. +module SupportFixture (verifyMixedSupport, verifyReuseProviders, verifySupportSources, verifySupportExports, verifySupportDependencies, verifyRelocatedSupportSources, canonicalEnvs, readFixtureSourceBytes, readTestSource, readPinnedSource) where -import Eval (freeVars) -import Parser (parseTricu) -import Research (TricuAST(..)) import Wire import ContentStore (hashObject, Domain(..)) import Control.Monad (forM_, unless) @@ -77,47 +74,44 @@ canonicalEnvs = snd . foldl' bundle (M.empty, []) ident = M.findWithDefault (M.size table) key table in (M.insert key ident table, ids Q.|> ident) --- The caller supplies original/current compiler+support kernels. A retained --- module must have identical full export sets/collision outcomes and identical --- used imports/reexports. Locals and lambda/let/annotated binders are excluded --- with the same parser/freeVars used by the host frontend, not text scanning. -verifyReuseScopes :: Kernels -> Kernels -> String -> Either String () -verifyReuseScopes before after source = do - imports <- mapM importRow [words row | row <- lines source, "!import " `isPrefixOf` row] - let asts = parseTricu (unlines [if "!" `isPrefixOf` row then "" else row | row <- lines source]) - locals = S.fromList [name | ast <- asts, name <- case ast of - SDef name _ _ -> [name]; SDefAnn name _ _ _ -> [name]; _ -> []] - free = foldMap freeVars asts S.\\ locals - owned env dep = M.fromList [(drop (length dep + 1) k, v) | - (k,v) <- M.toList env, (dep ++ ".") `isPrefixOf` k] - scoped env = M.fromListWith (++) [(spelling qualifier name, [(dep ++ "." ++ name, root)]) | - (dep,qualifier) <- imports, (name,root) <- M.toList (owned env dep)] - old = scoped before; new = scoped after - exports = [row | row <- map words (lines source), take 1 row == ["!export"]] - external = [name | [_,name] <- exports, name /= "*", name `S.notMember` locals] - reexports = [dep | [_,"*","from",dep] <- exports] - used = S.toList free ++ external - forM_ imports $ \(dep,_) -> do - require (not (M.null (owned before dep))) ("missing original provider " ++ dep) - require (M.keysSet (owned before dep) == M.keysSet (owned after dep)) ("full export set changed: " ++ dep) - require (M.keysSet old == M.keysSet new) "full namespace changed" - forM_ (M.keys old) $ \name -> do - let roots env = S.fromList (map snd (env M.! name)) - require ((S.size (roots old) == 1) == (S.size (roots new) == 1)) ("collision outcome changed: " ++ name) - forM_ used $ \name -> case (M.lookup name old, M.lookup name new) of - (Just a, Just b) -> require (S.fromList a == S.fromList b) ("used kernel changed: " ++ name) - _ -> Left ("unresolved free/exported name: " ++ name) - forM_ reexports $ \dep -> do - require (not (M.null (owned before dep))) ("missing reexport provider: " ++ dep) - require (owned before dep == owned after dep) ("reexport kernels changed: " ++ dep) +-- A reuse proof, not a source validator: the caller authenticates unchanged +-- source/output against the accepted original cohort first. Identical complete +-- provider interfaces imply identical namespaces, collisions and bound trees, +-- regardless of which names the body uses or shadows. Even unused changes now +-- require rebuilding. IDs must come from one shared canonicalEnvs interning run. +-- +-- Recognize only the fixture's simple named-provider directives. Reexports +-- acquire a provider even without !import. Unsupported directives (including +-- build-only !source, whose input identity is not a runtime interface) fail +-- closed and require rebuilding; there is no body parsing or scope inference. +verifyReuseProviders :: Kernels -> Kernels -> String -> Either String () +verifyReuseProviders before after source = do + dependencies <- concat <$> mapM directive + [row | line <- lines source, let row = words line, + first : _ <- [row], "!" `isPrefixOf` first] + forM_ (S.toList (S.fromList dependencies)) $ \dep -> do + let old = owned before dep + new = owned after dep + require (not (M.null old)) ("missing original provider: " ++ dep) + require (M.keysSet old == M.keysSet new) ("full export set changed: " ++ dep) + require (old == new) ("provider kernels changed: " ++ dep) where + owned env dep = M.filterWithKey (\name _ -> (dep ++ ".") `isPrefixOf` name) env require ok message = if ok then Right () else Left message - spelling "*" name = name - spelling qualifier name = qualifier ++ "." ++ name - importRow [_,dep] = Right (dep,dep) - importRow [_,dep,"*"] = Right (dep,"*") - importRow [_,dep,"as",qualifier] = Right (dep,qualifier) - importRow _ = Left "unsupported mixed-cohort native import" + directive ["!import", dep] = provider dep + directive ["!import", dep, "*"] = provider dep + directive ["!import", dep, "as", qualifier] + | identifier qualifier = provider dep + directive ["!export", "*", "from", dep] = provider dep + directive ["!export", name] + | name == "*" || identifier name = Right [] + directive _ = Left "unsupported conservative reuse directive; rebuild required" + provider dep + | identifier dep = Right [dep] + | otherwise = Left "unsupported conservative reuse provider; rebuild required" + identifier (first : rest) = letter first && all (\c -> letter c || c >= '0' && c <= '9') rest + identifier [] = False + letter c = c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' -- Recompiled bodies may change, but changing a retained fingerprint would -- relabel an old executable. Cohort identity/membership is not a rewrite API. @@ -168,7 +162,7 @@ verifySupportExports expected manifest = do -- A previously built consumer is not current merely because its own source is -- unchanged. Rebuild conservatively if any selected rebuilt support provider's --- full manifest changed. Origin-reused modules keep verifyReuseScopes below. +-- full manifest changed. Origin-reused modules keep verifyReuseProviders below. verifySupportDependencies :: M.Map String String -> [[String]] -> Either String () verifySupportDependencies providers rows = do let locals = [dep | ["local",dep,_,_] <- rows] @@ -228,7 +222,7 @@ verifyMixedSupport metadata currentBytes compilerBytes = do unless (owned ns alias == owned os alias) $ error ("retained output changed: " ++ alias) source <- readTestSource path either (error . (("unsafe support reuse " ++ alias ++ ": ") ++)) pure - (verifyReuseScopes before after source) + (verifyReuseProviders before after source) else unless (owned ns alias == owned rebuilt alias) $ error ("rebuilt output mismatch: " ++ alias) forM_ builtRows $ \(alias,receiptPin,manifestPin,compilerPin) -> do diff --git a/test/repair_support_dependencies.py b/test/repair_support_dependencies.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Materialize full immutable old support providers; never compile sources. -Used only with the separately checked exact-kernel reuse proof. Records retain +Used only with the separately checked complete-provider identity reuse proof. Records retain original receipts and manifests; this does not impersonate the new compiler. """ import argparse diff --git a/test/repair_support_fixture.py b/test/repair_support_fixture.py @@ -3,7 +3,7 @@ Original snapshots stay immutable. Rebuilt modules get their actual source digest, full manifest and original compilation receipt; no interface-compatibility promise. -Unrebuilt origin modules still require the fast gate's exact-kernel reuse proof. +Unrebuilt origin modules still require the fast gate's complete-provider identity reuse proof. Multiple preparation directories may be combined with explicit --replace aliases. """ import argparse