tricu

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

commit 17179a4c821d04b16c76684557f0b069d1a9e8bf
parent 34fcf78b10ce2bb4532c8a2ac8d1fd5a453a7b52
Author: James Eversole <james@eversole.co>
Date:   Fri, 16 Oct 2026 21:08:13 -0500

Use direct outer constructor dispatch in stable pattern lowering

Keep optimized and sequential fallback representations unchanged. Candidate controls cover direct-tree targets, ordered/partial demand, scoped captures and fully constructed closed-divergence guards. Prepare an authenticated transition worker for the agreed single Haskell generation review gate; compiler acceptance and publication remain pending.

Diffstat:
Mdocs/validation.md | 15+++++++++++++--
Mlib/source/pattern.tri | 22++++++++++++++++------
Mlib/source/resolve.tri | 17+++++++++++++++--
Mlib/source/stable.tri | 2+-
Mtest/PatternLowering.hs | 45+++++++++++++++++++++++++++++++++++++++++----
Mtest/pattern_lowering.py | 24+++++++++++++++++++++---
6 files changed, 107 insertions(+), 18 deletions(-)

diff --git a/docs/validation.md b/docs/validation.md @@ -759,6 +759,7 @@ incomplete. These probes neither generate compilers nor publish fixtures. ```sh python3 test/pattern_lowering.py result/bin/tricu \ + --reference-unit "$RETAINED_SOURCE_UNIT_BUNDLE" \ --directory /tmp/tricu-pattern-candidate ``` @@ -772,9 +773,19 @@ The Haskell control driver is compiled through Nix. Checks reuse the 75-case independent equation corpus, exercise constructor/equality workloads on finite and poisoned inputs, and verify split-column ordering, nested failure, unreached pins and whole-child captures. Two exact code-generation targets compare ordinary -equations with independently authored delayed dispatchers; the retained compiler +equations with independently authored direct dispatchers; the retained compiler must fail those optimization targets. Non-pattern controls must retain their -original trees. The existing 46-case local-control suite also runs on the candidate. +original trees. Optimized-backend function trees must also remain identical; +stable function construction must tolerate unselected closed divergence while +application to the divergent branch must not return. The existing 46-case +local-control suite also runs on the candidate. + +Select an authenticated retained `source_unit` bundle with unqualified exports +for `--reference-unit`; its digest is recorded separately from the shipped +compiler used to construct the candidate. `--prepare-worker` additionally rebuilds +all affected consumers through `source_map` and emits a pruned stable transition +`worker.arboricx`. This is candidate preparation, not a full generation or +fixed-point acceptance. `--resume` permits diagnostic checkpoint reuse only. Fresh-process cost probes interleave reference/candidate executions, discard a warmup, and retain three samples per role. Identical explicit recursion and full diff --git a/lib/source/pattern.tri b/lib/source/pattern.tri @@ -118,6 +118,12 @@ spBody node ctx = (let rec spCase leaf stem fork value = (spApp4 (spLit spSelect) (spLam (spShift leaf)) (spLam stem) (spLam (spLam fork)) value) +-- Stable abstraction keeps applications residual beneath the argument binders. +-- Only the factored outer split uses this direct selector. Nested tests and +-- sequential failures retain spCase/spIf and their continuation protocol. +-- Leaf has no extra binder here, so do NOT shift its body as spCase does. +spDirectCase leaf stem fork value = (spApp4 (spLit triage) + leaf (spLam stem) (spLam (spLam fork)) value) spIf yes no condition = (spApp3 (spLit lazyBool) (spLam (spShift yes)) (spLam (spShift no)) condition) @@ -223,18 +229,18 @@ spKnownEquations equations column children ctx = (lazyList (_ : spLit t) (spShiftContext ctx) (snd equation) (spApp (spVar 0) (spLit t)))) (spLam (spShift (spKnownEquations rest column children ctx)))) (spKnownRowIrrefutable (fst equation) column)) equations) -spSplitEquations equations column ctx = (let +spSplitEquationsWith select equations column ctx = (let rows kind = filter (equation : spRootAccepts kind (nth column (fst equation))) equations stemCtx = spShiftContext ctx forkCtx = spShiftContext stemCtx - in spCase + in select (spKnownEquations (rows 1) column [] ctx) (spKnownEquations (rows 2) column [(spVar 0)] stemCtx) (spKnownEquations (rows 3) column [(spVar 1) (spVar 0)] forkCtx) (nth column (spPending ctx))) -spEquations equations ctx = (lazyMaybe +spEquationsWith select equations ctx = (lazyMaybe (_ : spSequentialEquations equations ctx) - (column : spSplitEquations equations column ctx) + (column : spSplitEquationsWith select equations column ctx) (spFindSplit equations (fst (fst equations)) 0)) spWrap patterns body = (lazyList (_ : body) (_ rest : spLam (spWrap rest body)) patterns) @@ -287,7 +293,7 @@ spValidateEquations equations arity = (lazyList (sourceCoreStart (snd equation))) t) (equal? (length (fst equation)) arity)) equations) -sourceLowerEquations equations = (lazyList +sourceLowerEquationsWith select equations = (lazyList (_ : err (spDiagnostic "nonempty equations" t t) t) (first rest : let patterns = fst first in let arity = length patterns in @@ -296,7 +302,11 @@ sourceLowerEquations equations = (lazyList (sourceCoreStart (snd first))) t) (_ : lazyResult (problem tail : err problem tail) (_ tail : ok (spWrap patterns - (spEquations equations (spContext patterns t (spArguments patterns)))) t) + (spEquationsWith select equations (spContext patterns t (spArguments patterns)))) t) (spValidateEquations equations arity)) (isZero_? arity)) equations) + +-- Selection is explicit; the optimized backend retains delayed construction. +sourceLowerEquations = sourceLowerEquationsWith spCase +sourceLowerStableEquations = sourceLowerEquationsWith spDirectCase diff --git a/lib/source/resolve.tri b/lib/source/resolve.tri @@ -157,15 +157,17 @@ sourceResolveEquation resolve equation binders environment = (lazyResult (append (reverse (sourceEquationCollect (fst equation) sourceEquationCaptures)) binders) environment)) (sourceResolveElements (fst equation) sourceResolvePattern binders environment)) -sourceResolveEquations resolve node binders environment = (lazyResult +sourceResolveEquationsWith lower resolve node binders environment = (lazyResult (diagnostic remainder : err diagnostic remainder) (_ unused : lazyResult (diagnostic remainder : err diagnostic remainder) (equations unused : lazyResult (diagnostic remainder : err diagnostic remainder) (core unused : ok (sourceCoreWithSpan core (sourceAstStart node) (sourceAstEnd node)) t) - (sourceLowerEquations equations)) + (lower equations)) (sourceResolveElements (sourceAstPayload node) (sourceResolveEquation resolve) binders environment)) (sourceEquationCheckGroup node)) +sourceResolveEquations = sourceResolveEquationsWith sourceLowerEquations + sourceResolveOrdinary resolve node binders environment = (case sourceAstKind node of ^sourceAstLeafKind = ok (sourceCoreLeaf (sourceAstStart node) (sourceAstEnd node)) t ^sourceAstNameKind = sourceResolveName node binders environment @@ -222,3 +224,14 @@ sourceResolve node binders environment = (case sourceAstKind node of ^sourceAstCaseKind = sourceResolveCase sourceResolve node binders environment ^sourceAstRecKind = sourceResolveRec sourceResolve node binders environment _ = sourceResolveOrdinary sourceResolve node binders environment) + +-- Keep the ordinary visitor/backend unchanged. The stable visitor intercepts +-- equation groups, including those reached through local/case/recursive bodies; +-- all other resolution, lexical scope and diagnostic ordering are shared. +sourceResolveStable node binders environment = (case sourceAstKind node of + ^sourceAstCaseKind = sourceResolveCase sourceResolveStable node binders environment + ^sourceAstRecKind = sourceResolveRec sourceResolveStable node binders environment + ^sourceAstEquationsKind = sourceResolveEquationsWith sourceLowerStableEquations + sourceResolveStable node binders environment + _ = sourceResolveOrdinary sourceResolveStable node binders environment) +resolveStableGateAst environment node = sourceResolveStable node t environment diff --git a/lib/source/stable.tri b/lib/source/stable.tri @@ -109,7 +109,7 @@ orderStableProgram environment program = (stableBind (definitions : orderGateProgram environment (pair definitions (sourceProgramFinal program)))) sourceCompileStableDefinition environment definition = (let recursive = sourceDefinitionSelfRecursive definition in - stableBind (resolveGateAst environment (lazyBool + stableBind (resolveStableGateAst environment (lazyBool (_ : sourceRecursiveBody definition) (_ : sourceDefinitionBody definition) recursive)) (core : stableBind (stableCompileCore core) (tree : ok (lazyBool (_ : stableFix tree) (_ : tree) recursive) t))) diff --git a/test/PatternLowering.hs b/test/PatternLowering.hs @@ -14,6 +14,7 @@ import Control.Exception import System.Directory import Data.List (isSuffixOf,isPrefixOf) import System.Environment +import System.Timeout (timeout) load path = do bytes <- BS.readFile path @@ -36,8 +37,9 @@ main = do [directory] <- getArgs candidate <- load(directory++"/source_unit.arboricx") reference <- load "test/fixtures/compiler-library.arboricx" + retained <- load(directory++"/reference-unit.arboricx") let unit = candidate Map.! "compileStableModuleDefinitions" - old = reference Map.! "source_unit.compileStableModuleDefinitions" + old = retained Map.! "compileStableModuleDefinitions" helpers = ofList [Fork (ofString (drop (length prefix) name)) term | (name,term)<-Map.toList reference, prefix<-["prelude.","parser."],prefix `isPrefixOf` name] oracle source = bindings (compile old helpers ("main = "++source)) Map.! "main" corpusBytes <- BS.readFile(directory++"/equations.json") @@ -97,10 +99,10 @@ main = do putStrLn("program "++file++": finite/demand controls passed") -- Independent code-generation targets: ordinary readable equations must now - -- construct the same tree as the hand-written delayed dispatcher. The retained + -- construct the same tree as the hand-written direct dispatcher. The retained -- compiler must fail that target, rather than being a vacuous semantic control. let canonical tree = encodeBundle <$> buildBundleShared [("root",tree)] - forM_ [("copy-equations","copy-delayed"),("pick-equations","pick-delayed")] $ \(authored,manual) -> do + forM_ [("copy-equations","copy-triage"),("pick-equations","pick-triage")] $ \(authored,manual) -> do fresh <- load(output++"/"++authored++".arboricx") >>= canonical . (Map.! "main") wanted <- load(baseline++"/"++manual++".arboricx") >>= canonical . (Map.! "main") previous <- load(baseline++"/"++authored++".arboricx") >>= canonical . (Map.! "main") @@ -109,7 +111,7 @@ main = do fresh <- load(output++"/"++name++".arboricx") >>= canonical . (Map.! "main") previous <- load(baseline++"/"++name++".arboricx") >>= canonical . (Map.! "main") unless(fresh==previous)(error("non-pattern control changed: "++name)) - putStrLn "exact delayed-dispatch targets, wrong-compiler and unchanged-control checks passed" + putStrLn "exact direct-dispatch targets, wrong-compiler and unchanged-control checks passed" let function source = do let term=bindings(compile unit Leaf source) Map.! "main" forceTreeShared term @@ -133,3 +135,38 @@ main = do Fork first _ | first==ofNumber 23 -> pure () _->error "known constructor lost whole-child capture" putStrLn "split-column, nested-failure, pin and whole-capture demand controls passed" + + -- Ineligible splits must retain the old sequential representation exactly. + forM_ ["wanted = t\nmain ^wanted = t t\nmain _ = t", + "main whole{t} = t t\nmain _ = t", + "main 23 = t t\nmain _ = t", + "main _ t = t\nmain (t _) _ = t t\nmain _ _ = t t t"] $ \source -> do + fresh <- canonical (bindings(compile unit Leaf source) Map.! "main") + previous <- canonical (bindings(compile old Leaf source) Map.! "main") + unless(fresh==previous)(error "sequential fallback representation changed") + putStrLn "sequential fallback identity controls passed" + -- Stable-only change: ordinary optimized lowering retains exact function + -- trees. Avoid its known closed-recursion construction hazard in this control. + forM_ ["main t = t\nmain (t _) = t t\nmain _ = t t t", + "main x = case x of\n (t a b) = a\n _ = t", + "main x = let\n f t = x\n f _ = t\n in f x"] $ \source -> do + let fresh = bindings(compile (candidate Map.! "compileModuleDefinitions") Leaf source) Map.! "main" + previous = bindings(compile (retained Map.! "compileModuleDefinitions") Leaf source) Map.! "main" + a <- canonical fresh + b <- canonical previous + unless(a==b)(error "optimized representation changed") + -- Fully publishing a function must not execute a closed divergent branch. + -- Both direct top-level matching and a local case exercise stable routing. + forM_ ["spin x = spin x\nmain t = spin t\nmain other = other", + "spin x = spin x\nmain input = case input of\n t = spin t\n other = other", + "spin x = spin x\nmain input = let\n f t = spin t\n f other = other\n in f input"] $ \source -> do + term <- function source + unless(apply term (Stem Leaf)==Stem Leaf)(error "unselected closed divergence") + case apply term (Stem poison) of + Stem _ -> pure () + _ -> error "guard inspected ignored child" + negative <- timeout 200000 (evaluate (apply term Leaf)) + case negative of + Nothing -> pure () + Just _ -> error "demanded divergent branch returned" + putStrLn "optimized identity and stable closed-divergence construction controls passed" diff --git a/test/pattern_lowering.py b/test/pattern_lowering.py @@ -128,13 +128,19 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('tricu', type=Path) parser.add_argument('--directory', type=Path, required=True) + parser.add_argument('--reference-unit', type=Path, required=True, + help='explicit retained source_unit bundle (unqualified exports)') parser.add_argument('--resume', action='store_true', help='diagnostic checkpoint reuse, not fresh-generation acceptance') parser.add_argument('--skip-costs', action='store_true') + parser.add_argument('--prepare-worker', action='store_true', + help='also rebuild affected map-builder consumers for an explicit transition worker') args = parser.parse_args() root, host = args.directory.resolve(), args.tricu.resolve() root.mkdir(parents=True, exist_ok=args.resume) workspace, store = root / 'workspace', root / 'store' workspace.mkdir(exist_ok=args.resume) + # Do not inherit a parent checkout map when the probe lives under it. + (workspace / "tricu.workspace").write_text("tricu.workspace.v1\n") metadata = verify_compiler() rows = {row[2]: row for row in verify(ROOT / 'test/fixtures')} report = dict(complete=False, resumed=args.resume, compiler_sha256=metadata['sha256'], @@ -175,7 +181,7 @@ def main(): for dep in entries[alias][1]: visit(dep) needed.append(alias) - visit('source_unit') + visit('source_map' if args.prepare_worker else 'source_unit') affected = {'source_pattern'} for alias in needed: if any(dep in affected for dep in entries[alias][1]): @@ -188,6 +194,10 @@ def main(): report['sources'][alias] = sha(data) if alias not in affected: assert snapshot_bytes(ROOT / 'test/fixtures', rows[alias][4]) == data, ('stale reuse', alias) + reference = root / 'reference-unit.arboricx' + reference.write_bytes(args.reference_unit.read_bytes()) + imported(reference) # authenticate the explicitly selected reference bundle + report['reference_unit_sha256'] = sha(reference.read_bytes()) tool = imported(ROOT / metadata['bundle']) payload = (store / 'objects' / tool[:3] / tool).read_bytes() assert digest('arboricx.module-manifest.v1', payload) == tool @@ -212,6 +222,16 @@ def main(): subject = root / 'source_unit.arboricx' run(['compile', '--file', workspace / 'source_unit.tri', '--toolchain', tool, '--tool-store', store, '--store', store, '--output', subject, '--compile-timeout', '600'], 630) + if args.prepare_worker: + full_worker = root / 'source_map.arboricx' + run(['compile', '--file', workspace / 'source_map.tri', '--toolchain', tool, + '--tool-store', store, '--store', store, '--output', full_worker, + '--compile-timeout', '600'], 630) + worker = root / 'worker.arboricx' + merge_bundles(worker, [(full_worker, [('sourceMapBuildStable', 'sourceMapBuildStable')])], + prune_unreachable=True) + imported(worker) + report['worker_sha256'] = sha(worker.read_bytes()) report['modules'] = audit(workspace, store, producer, report['reused']) assert imported(subject) == report['modules']['source_unit']['manifest'], 'tested/published unit mismatch' report['candidate_sha256'] = sha(subject.read_bytes()) @@ -294,8 +314,6 @@ def cost_probes(root, run, imported, report, save): # oracles for these exact sources have already run in the Haskell driver. from stable_bundle import byte_tree reference = root / 'reference-unit.arboricx' - merge_bundles(reference, [(ROOT / 'test/fixtures/compiler-library.arboricx', - [('source_unit.compileStableModuleDefinitions', 'compileStableModuleDefinitions')])], prune_unreachable=True) compilers = {'reference': imported(reference), 'candidate': imported(root / 'source_unit.arboricx')} environment = root / 'empty-environment.tree' environment.write_bytes(b'\0')