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:
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