diff --git a/LICENSE-EXCEPTION b/LICENSE-EXCEPTION
--- a/LICENSE-EXCEPTION
+++ b/LICENSE-EXCEPTION
@@ -1,81 +1,118 @@
-Progmasoft Linking Exception, Version 1.1
+Progmasoft Linking Exception, Version 2.0
 
-This Exception is an additional permission to the Mozilla Public License, version 2.0 ("MPL-2.0"). It is intended to preserve MPL-2.0's file-level obligations for Covered Software while removing doubt about combining that software with independent code. Capitalized terms not defined here have the meanings given in MPL-2.0.
+This Exception is an additional permission granted with the Mozilla Public License, version 2.0 ("MPL-2.0"). It keeps MPL-2.0's file-level copyleft for Covered Software and removes doubt about four things: combining Covered Software with other work, what compilers do to code, what programs built with Covered Software tools owe, and the interfaces other work needs in order to use Covered Software. It does not change the text of MPL-2.0. Capitalized terms not defined here have the meanings given in MPL-2.0, and "You" has the meaning given there.
 
 1. Definitions
 
-"Independent Component" means software contained in one or more Source Code Form files that neither contain Covered Software nor are Modifications of Covered Software. A component does not cease to be independent merely because compilation, static or dynamic linking, loading, packaging, or deployment places its Executable Form in the same file, process, address space, archive, image, or distribution as Covered Software.
+"Independent Work" means software or other material, in any form, that is neither Covered Software nor a Modification. Sections 3 and 5 say what does not change that.
 
-"Combined Work" means a Larger Work that combines Covered Software with one or more Independent Components by any technical means, including static or dynamic linking, loading, embedding, plugins, extensions, language bindings, an API, ABI, FFI boundary, command-line interface, protocol, inter-process communication mechanism, or file format.
+"Tool" means Covered Software that You run to produce material: a compiler, linker, code generator, build system, package manager, formatter, or similar program.
 
-"Output" means material produced by executing Covered Software, including generated source, object code, executable code, intermediate representations, documentation, reports, diagnostics, formatted text, and analysis results.
+"Input" means the material You give a Tool to process, other than the Tool's own files.
 
-2. Additional Permission for Combined Works
+"Output" means the material a Tool writes when it runs, including source code, intermediate representations, object code, executable code, packages, documentation, reports, diagnostics, formatted text, and analysis results.
 
-Subject to compliance with MPL-2.0 for the Covered Software, each recipient has a worldwide, royalty-free, non-exclusive, irrevocable additional copyright permission to create, reproduce, use, modify, display, perform, make available, and distribute a Combined Work under terms of the recipient's choice, including proprietary or closed-source terms.
+"Generated Code" means the part of an Output that a Tool copies, expands, or derives from its own Covered Software in its ordinary operation, rather than from Input. Examples are templates, parser skeletons, entry points, stubs, thunks, tables, metadata, instantiated generics, and project scaffolding.
 
-Those chosen terms may govern Independent Components and other portions of the Combined Work, but must not restrict or alter any recipient's rights in the Covered Software under MPL-2.0 and this Exception. No distributor is required to relicense an Independent Component merely because it forms part of a Combined Work.
+"Runtime Material" means Covered Software that (a) the copyright holders of a Tool identify, in the documentation they publish, as that Tool's runtime library or standard library, and (b) is compiled, linked, embedded into, or placed beside an Output because the Output uses it when it runs. A license text or other file installed for documentation only is not Runtime Material.
 
-3. Boundary of Covered Software
+"Interface Material" means declarations, type and data layouts, constants, enumerations, macros, function and method signatures, attributes, wire and file formats, and the short inline definitions that an interface needs in order to be used, to the minimum extent necessary to call, implement, extend, bind to, or interoperate with Covered Software.
 
-Linking, loading, calling, communicating with, packaging, installing, or distributing an Independent Component with Covered Software does not, by itself:
+2. Larger Works
 
-(a) make the Independent Component Covered Software or a Modification;
-(b) subject the Independent Component's Source Code Form to MPL-2.0; or
-(c) expand MPL-2.0 beyond files that are Covered Software or Modifications under MPL-2.0.
+You may create, reproduce, use, modify, make available, and distribute a Larger Work that combines Covered Software with Independent Work under terms of Your choice, including proprietary terms, provided You comply with MPL-2.0 for the Covered Software as this Exception adjusts it. The combination may be made by any technical means, including static or dynamic linking, loading, embedding, plugins, language bindings, an API, ABI, or FFI boundary, a command-line interface, a protocol, inter-process communication, or a file format. It does not matter whether the works end up in one file, process, address space, archive, image, or deployment. The word "separate" in the definition of Larger Work in MPL-2.0 Section 1.7 refers to Source Code Form.
 
-If Source Code Form from Covered Software is copied into another file, or a new Source Code Form file contains Covered Software, MPL-2.0 determines whether that file is a Modification. Labels, file extensions, build boundaries, code generation, or later separation of files do not avoid that determination.
+Your terms may govern the Independent Work and the Larger Work as a whole. They must not limit or alter a recipient's rights in the Source Code Form of the Covered Software.
 
-4. Outputs
+Neither MPL-2.0 nor this Exception requires You, for a Larger Work, to provide the Source Code Form of Independent Work, to provide object files or anything else that allows relinking, to allow reverse engineering of Independent Work, or to provide installation information, keys, or credentials.
 
-An Output does not become Covered Software merely because Covered Software produced, transformed, linked, or processed it. The copyright license chosen for input material does not change merely because Covered Software processes that material.
+3. What Compilation Does Not Change
 
-If an Output contains copyrightable Source Code Form or other copyrightable material copied or derived from Covered Software, this Exception does not remove MPL-2.0 obligations from that incorporated material. Facts, ideas, interfaces, names, signatures, diagnostics, and other material not protected by copyright are not converted into Covered Software by this Exception.
+The following do not make Independent Work into Covered Software or a Modification, and do not subject its Source Code Form to MPL-2.0:
 
-5. Preservation of MPL-2.0 Obligations
+(a) calling, linking to, loading, subclassing, implementing, or otherwise using Covered Software through its interfaces;
+(b) including, importing, or referring to a Covered Software header, module, or package from a file of Independent Work; and
+(c) macro expansion, template or generic instantiation, specialization, inlining, link-time or whole-program optimization, just-in-time compilation, or any comparable compiler transformation that places Covered Software in the compiled form of Independent Work.
 
-This Exception does not remove, replace, waive, or narrow any MPL-2.0 obligation applicable to Covered Software. In particular:
+Material placed in a compiled form under (c) is the Executable Form of the Covered Software it came from. Section 4 or Section 5 releases it when it is Generated Code, Runtime Material, or Interface Material. Otherwise MPL-2.0 Section 3.2 applies to it, and Section 7 says how to meet that Section.
 
-(a) Covered Software in Source Code Form and every Modification remain subject to MPL-2.0;
-(b) distribution in Executable Form remains subject to MPL-2.0 Section 3.2, including the obligation to inform recipients how to obtain the corresponding Source Code Form when that section applies;
-(c) notices protected by MPL-2.0 Section 3.4 must be retained; and
-(d) this Exception grants no permission to withhold Source Code Form that MPL-2.0 requires to be made available.
+If You copy Covered Software in Source Code Form into a file, other than as Section 4 or Section 5 allows, that file is a Modification. Labels, file extensions, build boundaries, or later separation of files do not change that.
 
-Distribution of a Combined Work under terms of choice is permission for the Combined Work as a whole and its Independent Components. It is not permission to relicense a Covered Software file.
+4. Generated Code and Programs Built with Tools
 
-6. Packaging and Deployment
+4.1 Output is not Covered Software merely because a Tool produced it. Running a Tool over Input does not change the license of the Input, and material in an Output that comes from Input has the same license as that Input.
 
-Placing Covered Software and an Independent Component in the same archive, package, installer, application bundle, container image, firmware image, distribution medium, hosted deployment, or process does not change their respective copyright licenses. Network interaction or operation as separate processes is not required for independence.
+4.2 You may use, modify, and distribute Generated Code, in any form, under terms of Your choice. MPL-2.0 Sections 3.1 through 3.4 do not apply to it. You need not offer its source, keep its notices, or attribute it.
 
-7. Direct Grant to Recipients
+4.3 You may distribute Runtime Material in Executable Form as part of, or together with, an Output that uses it, under terms of Your choice. MPL-2.0 Sections 3.1 through 3.4 do not apply to that distribution. The Source Code Form of Runtime Material stays under MPL-2.0, and this Section does not permit distributing Runtime Material on its own. This Section covers Runtime Material You have not changed, and a Modification of it only when You make that Modification available under Section 7.
 
-The additional permission in this Exception is granted directly by the copyright holders applying it to every recipient of the applicable Covered Software. A distributor need not sublicense this Exception. Distribution through one or more intermediaries does not reduce the permission, and termination of an upstream distributor's rights does not terminate rights already received by a compliant downstream recipient.
+4.4 Limits. This Section 4 does not apply to:
 
-8. Termination and Reinstatement
+(a) Covered Software given to a Tool as Input, unless it is Runtime Material. Compiling, formatting, or transforming Covered Software with a Tool leaves its obligations in place;
+(b) material that an unmodified Tool would not write for the same Input and options, unless You make the change that causes it available under Section 7. Changing a Tool so that it copies more of its Covered Software into Output does not widen this Section; or
+(c) an Output produced for the purpose of obtaining a Tool's Covered Software rather than for using the Tool. Such material remains subject to MPL-2.0.
 
-This Exception has no separate patent-litigation, field-of-use, combination, or compatibility termination condition. A recipient may exercise it only while entitled to exercise the corresponding MPL-2.0 copyright rights. If those rights terminate under MPL-2.0 Section 5.1, exercise of this Exception for the affected Covered Software is suspended to the same extent and is reinstated when, and to the extent that, the MPL-2.0 rights are reinstated.
+4.5 Intermediate representations and every other kind of Output are treated alike. An Output keeps the permissions of this Section after it leaves You. If Your rights in a Tool end, that does not affect Output You have already made or the rights of anyone who received it.
 
-A recipient may elect to use Covered Software under MPL-2.0 without relying on this Exception.
+4.6 These permissions lift conditions; they do not end the license. Generated Code and Runtime Material stay licensed to You under MPL-2.0 Section 2, and MPL-2.0 Sections 6 and 7 continue to apply to them.
 
-9. Other Rights
+5. Interface Material and Examples
 
-This Exception grants copyright permission only. It grants no trademark, trade dress, service mark, publicity, confidential-information, or patent rights. Patent rights, if any, arise only under MPL-2.0 or a separately applicable patent grant. Nothing in this Exception limits rights arising independently under exhaustion, fair use, fair dealing, implied license, estoppel, or other applicable law.
+You may copy Interface Material into Independent Work, and translate it into another language, to the extent needed to use or interoperate with Covered Software. The result is not a Modification, and MPL-2.0 Section 3 does not apply to it, in Source Code Form or in Executable Form. This covers header files, bindings, and declarations in another language. It does not cover the implementation behind an interface.
 
+You may use, copy, and adapt code that the documentation of Covered Software presents as an example or as a starting point for Your own project, under terms of Your choice and without any obligation of MPL-2.0 Section 3.
+
+6. What This Exception Keeps
+
+Except where Sections 4 and 5 say otherwise:
+
+(a) Covered Software in Source Code Form, and every Modification, stay under MPL-2.0 and cannot be relicensed through this Exception;
+(b) distributing Covered Software in Executable Form stays subject to MPL-2.0 Section 3.2, as Section 7 explains;
+(c) notices protected by MPL-2.0 Section 3.4 stay in place in Source Code Form; and
+(d) nothing here allows You to withhold Source Code Form that MPL-2.0 requires You to make available.
+
+7. Telling Recipients How to Obtain Source
+
+For Covered Software You have not changed, You satisfy MPL-2.0 Section 3.2(a) by giving recipients of the Executable Form a notice that names the Covered Software and its version and gives an address where the corresponding Source Code Form is publicly available. You may put that notice in documentation, in an about screen, in a notice file, or in a software bill of materials. The copyright holders' own public release of that version is a sufficient source; You need not host a copy, and You are not in breach if that release later stops being available.
+
+For a Modification, the same notice must give an address where the Source Code Form of the exact version You distributed can be obtained by reasonable means and in a timely manner. An address that follows a branch or otherwise changes over time is not enough.
+
+8. Contributors and Direct Grant
+
+Every Contributor who distributes Covered Software that carries an identification under Section 12 grants this Exception for that Contributor's Contributions.
+
+This Exception is granted directly to every recipient of Covered Software, for the duration of the copyright in it. A distributor need not sublicense it. Distribution through intermediaries does not reduce it, and the end of a distributor's rights does not end rights already received by others.
+
+You may use Covered Software under MPL-2.0 without relying on this Exception. As an exception to MPL-2.0 Section 3.4, You may remove the identification of this Exception from a file You modify; Your version of that file is then offered by You under MPL-2.0 alone. Earlier copies keep this Exception.
+
+9. Termination
+
+This Exception has no termination condition of its own. You may rely on Sections 2, 3, and 5 only while You hold the corresponding rights under MPL-2.0. If those rights end under MPL-2.0 Section 5.1 or 5.2, those permissions end for the affected Covered Software to the same extent, and return if and to the extent those rights are reinstated. This does not reach Independent Work, nor the rights of recipients that MPL-2.0 Section 5.3 preserves, nor Section 4.5.
+
 10. Relationship to MPL-2.0
 
-MPL-2.0 and this Exception are read together. If this Exception and MPL-2.0 cannot both be given effect with respect to the creation or distribution of a Combined Work, this Exception controls only to the minimum extent necessary to provide the additional permission stated in Sections 2 through 6. MPL-2.0 controls in every other respect.
+MPL-2.0 and this Exception are read together. This Exception is a permission from copyright holders and is not a modified version of MPL-2.0. Where the two cannot both be given effect, this Exception controls only as far as needed to give the permissions in Sections 2 through 5 and 7 through 9, and MPL-2.0 controls in every other respect. This Exception continues to apply if You use Covered Software under a later version of the Mozilla Public License as MPL-2.0 Section 10.2 allows.
 
-Nothing here changes whether Covered Software is "Incompatible With Secondary Licenses" or changes the operation of MPL-2.0 Section 3.3 for a work governed by a Secondary License.
+MPL-2.0 Sections 6, 7, 8, and 9 apply to this Exception. It is provided without warranty.
 
-11. Applicability and Version
+Nothing here changes whether Covered Software is "Incompatible With Secondary Licenses" or changes the operation of MPL-2.0 Section 3.3 for a work governed by a Secondary License. When You distribute under a Secondary License as that Section allows, the Secondary License governs that distribution.
 
-This Exception applies only to a file that expressly identifies it with this exact SPDX license expression:
+11. Other Rights
 
-MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+This Exception concerns copyright only. It grants no trademark, trade dress, service mark, publicity, or confidential-information rights, and no patent rights beyond those MPL-2.0 or a separately applicable patent grant provides. Nothing here limits rights that arise independently under exhaustion, fair use, fair dealing, implied license, estoppel, or other applicable law.
 
-Reference to Version 1.1 does not incorporate any later version. A copy of this text supplied in a repository or distribution identifies the permission; the per-file expression determines which files receive it.
+12. Applicability and Versions
 
-12. Severability
+This Exception applies to Covered Software identified with this SPDX license expression, or an expression that contains it:
 
-If any provision of this Exception is held unenforceable, it shall be reformed only to the minimum extent necessary to make it enforceable. If reformation is unavailable, that provision is severed and the remaining provisions continue to the maximum extent permitted by law.
+MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
+The identification may be in the file itself, in a companion license file, in a machine-readable file such as REUSE.toml that assigns the expression to that file, or in a notice of the project that names the file or the directory holding it. A compiled form has the identification of the source it was compiled from and does not need to carry it.
+
+Progmasoft may publish later versions of this Exception. You may use Covered Software under the version that identifies it or under any later version Progmasoft publishes.
+
+Progmasoft also grants this version for its own Contributions in every release it has made under Version 1.0 or Version 1.1 of this Exception. For those releases You may rely on either version.
+
+13. Severability
+
+If any provision of this Exception is held unenforceable, it shall be reformed only to the minimum extent necessary to make it enforceable. If reformation is unavailable, that provision is severed and the remaining provisions continue to the maximum extent permitted by law.
diff --git a/src/Progmasoft/Tala/Compiler.hs b/src/Progmasoft/Tala/Compiler.hs
--- a/src/Progmasoft/Tala/Compiler.hs
+++ b/src/Progmasoft/Tala/Compiler.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Composition of the target-independent Tala frontend stages.
 
@@ -15,8 +15,12 @@
     , NamespaceArtifacts (..)
     , ProjectFrontendArtifacts (..)
     , compileToCorePrep
+    , compileToCorePrepWith
     , compileEntryToCorePrep
+    , compileEntryToCorePrepWith
     , compileProjectToCorePrep
+    , compileProjectToCorePrepWith
+    , compileTestsToCorePrep
     , compileTemplateSpecializations
     , projectEntryCore
     , validateEntryPoint
@@ -33,10 +37,12 @@
 import Progmasoft.Tala.Core.Verifier
 import Progmasoft.Tala.Desugarer
 import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Directives
 import Progmasoft.Tala.Frontend
 import Progmasoft.Tala.Template.Discovery
 import Progmasoft.Tala.Template.Specialization
 import Progmasoft.Tala.Template.Specialization.Verifier
+import Progmasoft.Tala.TestRunner
 
 {- | Artifacts for one semantic namespace after all of its physical source
 units have been merged. Keeping every stage visible makes stage ownership and
@@ -78,14 +84,24 @@
 
 -- | Run the source pipeline through verified, optimized CorePrep for one file.
 compileToCorePrep :: CompilerInput -> Either [Diagnostic] FrontendArtifacts
-compileToCorePrep input = analyzeSemantics input >>= compileSemanticToCorePrep
+compileToCorePrep = compileToCorePrepWith hostDirectiveEnvironment
 
+-- | 'compileToCorePrep' with the conditional directives decided for a target.
+compileToCorePrepWith :: DirectiveEnvironment -> CompilerInput -> Either [Diagnostic] FrontendArtifacts
+compileToCorePrepWith environment input =
+    analyzeSyntaxWith environment input >>= compileParsedToCorePrep . syntaxParsedAST
+
 -- | Compile one source file and validate its namespace-qualified entry class.
 compileEntryToCorePrep :: QualifiedName -> CompilerInput -> Either [Diagnostic] FrontendArtifacts
-compileEntryToCorePrep entry input = do
-    artifacts <- compileToCorePrep input
+compileEntryToCorePrep = compileEntryToCorePrepWith hostDirectiveEnvironment
+
+-- | 'compileEntryToCorePrep' with the conditional directives decided for a target.
+compileEntryToCorePrepWith ::
+    DirectiveEnvironment -> QualifiedName -> CompilerInput -> Either [Diagnostic] FrontendArtifacts
+compileEntryToCorePrepWith environment entry input = do
+    artifacts <- compileToCorePrepWith environment input
     validateEntryPoint entry (artifactTypedAST artifacts)
-    pure artifacts
+    pure (keepEntryMain entry artifacts)
 
 {- | Materialize an explicit batch of resolved template demands and lower the
 resulting closed declaration view to verified Core. Demand discovery and
@@ -110,20 +126,48 @@
 weakening any stage boundary.
 -}
 compileProjectToCorePrep :: QualifiedName -> [CompilerInput] -> Either [Diagnostic] ProjectFrontendArtifacts
-compileProjectToCorePrep entry inputs = do
+compileProjectToCorePrep = compileProjectToCorePrepWith hostDirectiveEnvironment
+
+-- | 'compileProjectToCorePrep' with the conditional directives decided for a target.
+compileProjectToCorePrepWith ::
+    DirectiveEnvironment -> QualifiedName -> [CompilerInput] -> Either [Diagnostic] ProjectFrontendArtifacts
+compileProjectToCorePrepWith environment entry inputs =
+    collectResults (map (parseUnit environment) inputs) >>= compileParsedProject entry
+
+{- | Compile the sources as a test build: @Fact@ holds in their conditions,
+and the entry is the runner of their @[Fact]@ methods, which the driver adds
+to the namespace of the tests.
+-}
+compileTestsToCorePrep :: DirectiveEnvironment -> [CompilerInput] -> Either [Diagnostic] ProjectFrontendArtifacts
+compileTestsToCorePrep environment inputs = do
+    parsedUnits <- collectResults (map (parseUnit environment {directiveTesting = True}) inputs)
+    cases <- findTestCases [(syntaxNamespace tree, syntaxDeclarations tree) | (_, ParsedAST tree) <- parsedUnits]
+    (namespace, entry, runner) <- testRunner cases
+    -- The runner belongs to the file of the first test, so the source
+    -- catalog names no file that does not exist.
+    let runnerUnit = (sourceFile (declarationSpan runner), ParsedAST (SyntaxTree (Just namespace) [runner]))
+    compileParsedProject entry (parsedUnits ++ [runnerUnit])
+
+parseUnit :: DirectiveEnvironment -> CompilerInput -> Either [Diagnostic] (FilePath, ParsedAST)
+parseUnit environment input = do
+    syntax <- analyzeSyntaxWith environment input
+    pure (compilerSourceFile input, syntaxParsedAST syntax)
+
+compileParsedProject :: QualifiedName -> [(FilePath, ParsedAST)] -> Either [Diagnostic] ProjectFrontendArtifacts
+compileParsedProject entry parsedUnits = do
     namespaceName <- entryNamespaceName entry
-    parsedUnits <- collectResults (map parseUnit inputs)
     if null parsedUnits
         then Left [compilerProblem "TAE0010" Nothing "project source set is empty"]
         else do
             compiled <- collectResults (map compileNamespace (groupParsedUnits parsedUnits))
             selected <- selectNamespace namespaceName compiled
             validateEntryPoint entry (artifactTypedAST (artifactFrontend selected))
-            pure (ProjectFrontendArtifacts entry compiled selected)
-    where
-        parseUnit input = do
-            syntax <- analyzeSyntax input
-            pure (compilerSourceFile input, syntaxParsedAST syntax)
+            pure
+                ( ProjectFrontendArtifacts
+                    entry
+                    compiled
+                    selected {artifactFrontend = keepEntryMain entry (artifactFrontend selected)}
+                )
 
 -- | Retrieve the selected namespace's optimized Core module for native lowering.
 projectEntryCore :: ProjectFrontendArtifacts -> CoreModule
@@ -296,6 +340,39 @@
             [] -> Left [entryProblem "TAE0003" Nothing ("entry class was not found: " ++ identifierText className)]
             [declaration] -> validateEntryMethod declaration
             _ -> Left [entryProblem "TAE0013" Nothing ("entry class is ambiguous: " ++ identifierText className)]
+
+{- | The native stages know the entry of a process as the function spelled
+@Main@ of the module, and a class other than the entry may have a method of
+that name, such as the program a test build tests. Such a method is ordinary,
+so it gives up the spelling. A spelling is not identity: its calls name its
+symbol, and they keep working.
+-}
+keepEntryMain :: QualifiedName -> FrontendArtifacts -> FrontendArtifacts
+keepEntryMain entry artifacts =
+    artifacts
+        { artifactCore = respell (artifactCore artifacts)
+        , artifactOptimizedCore = respell (artifactOptimizedCore artifacts)
+        }
+    where
+        respell core = core {coreModuleFunctions = map ordinary (coreModuleFunctions core)}
+        ordinary function
+            | isOtherMain (coreFunctionName function) =
+                function {coreFunctionName = (coreFunctionName function) {resolvedSpelling = Identifier "$Main"}}
+            | otherwise = function
+        isOtherMain name =
+            resolvedSpelling name == Identifier "Main" && Just (resolvedSymbol name) /= entrySymbol
+        entrySymbol = case (entryClassName entry, artifactTypedAST artifacts) of
+            (Right className, TypedAST (SyntaxTree _ declarations)) ->
+                case matchingTypes className declarations of
+                    [TypeDeclaration {typeMembers = members}] ->
+                        case [ resolvedSymbol (declarationName member)
+                             | member@FunctionDeclaration {} <- members
+                             , resolvedSpelling (declarationName member) == Identifier "Main"
+                             ] of
+                            [symbol] -> Just symbol
+                            _ -> Nothing
+                    _ -> Nothing
+            _ -> Nothing
 
 matchingTypes :: Identifier -> [Declaration ResolvedName Type] -> [Declaration ResolvedName Type]
 matchingTypes className declarations =
diff --git a/src/Progmasoft/Tala/Diagnostic/SideChannel.hs b/src/Progmasoft/Tala/Diagnostic/SideChannel.hs
--- a/src/Progmasoft/Tala/Diagnostic/SideChannel.hs
+++ b/src/Progmasoft/Tala/Diagnostic/SideChannel.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Write validated frontend diagnostics to the private native-driver channel.
 
diff --git a/src/Progmasoft/Tala/Driver/Command.hs b/src/Progmasoft/Tala/Driver/Command.hs
--- a/src/Progmasoft/Tala/Driver/Command.hs
+++ b/src/Progmasoft/Tala/Driver/Command.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Private command model shared by the Haskell shared-library boundary and
 the small command-line adapter. It returns owned bytes instead of opening an
@@ -21,6 +21,7 @@
 import Progmasoft.Tala.Core (CoreModule)
 import Progmasoft.Tala.Core.Wire
 import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Directives
 import Progmasoft.Tala.SourceSet
 import Progmasoft.Tala.SourceWarnings
 
@@ -46,9 +47,10 @@
     | FrontendFailure String
 
 data FrontendCommand
-    = CompileFile FilePath
-    | CompileFiles QualifiedName [FilePath]
-    | CompileProject FilePath QualifiedName [FilePath] [FilePath]
+    = CompileFile DirectiveEnvironment FilePath
+    | CompileFiles DirectiveEnvironment QualifiedName [FilePath]
+    | CompileTests DirectiveEnvironment [FilePath]
+    | CompileProject DirectiveEnvironment FilePath QualifiedName [FilePath] [FilePath]
     | ListProjectSources FilePath [FilePath] [FilePath]
 
 data CommandOptions = CommandOptions
@@ -58,10 +60,12 @@
     , optionSourceRoots :: [FilePath]
     , optionExcludes :: [FilePath]
     , optionListSources :: Bool
+    , optionTest :: Bool
+    , optionTargetSystem :: Maybe String
     }
 
 emptyOptions :: CommandOptions
-emptyOptions = CommandOptions [] Nothing Nothing [] [] False
+emptyOptions = CommandOptions [] Nothing Nothing [] [] False False Nothing
 
 {- | Parse the deliberately narrow private argument vocabulary. Values remain
 separate from option names so file names and glob patterns cannot be
@@ -86,6 +90,10 @@
     "--list-sources"
         | optionListSources options -> Left "--list-sources may be specified only once"
         | otherwise -> parseOptions options {optionListSources = True} remaining
+    "--test"
+        | optionTest options -> Left "--test may be specified only once"
+        | otherwise -> parseOptions options {optionTest = True} remaining
+    "--target-os" -> uniqueValue name optionTargetSystem setTargetSystem options remaining
     _ -> Left ("unknown private frontend option: " ++ name)
 
 uniqueValue ::
@@ -114,38 +122,56 @@
         | null value -> Left (name ++ " value is empty")
         | otherwise -> parseOptions (setter value options) rest
 
-setProjectRoot, setEntry :: String -> CommandOptions -> CommandOptions
+setProjectRoot, setEntry, setTargetSystem :: String -> CommandOptions -> CommandOptions
 setProjectRoot value options = options {optionProjectRoot = Just value}
 setEntry value options = options {optionEntry = Just value}
+setTargetSystem value options = options {optionTargetSystem = Just value}
 
+{- | The environment of the conditional directives: the target's system,
+which the native driver names after the target triple, and whether tests are
+compiled. Without a system the host's is used.
+-}
+directiveEnvironment :: CommandOptions -> Either String DirectiveEnvironment
+directiveEnvironment options = do
+    system <- case optionTargetSystem options of
+        Nothing -> Right (directiveSystem hostDirectiveEnvironment)
+        Just name -> maybe (Left ("--target-os names no known system: " ++ name)) Right (operatingSystemNamed name)
+    Right (DirectiveEnvironment system (optionTest options))
+
 addSourceFile, addSourceRoot, addExclude :: String -> CommandOptions -> CommandOptions
 addSourceFile value options = options {optionSourceFiles = optionSourceFiles options ++ [value]}
 addSourceRoot value options = options {optionSourceRoots = optionSourceRoots options ++ [value]}
 addExclude value options = options {optionExcludes = optionExcludes options ++ [value]}
 
 finishCommand :: CommandOptions -> Either String FrontendCommand
-finishCommand options = case ( optionSourceFiles options
-                             , optionProjectRoot options
-                             , optionEntry options
-                             , optionListSources options
-                             ) of
-    (sources@(_ : _), Nothing, entryText, False)
-        | not (null (optionSourceRoots options) && null (optionExcludes options)) ->
-            Left "file compilation cannot declare project roots or exclusions"
-        | otherwise -> case (entryText, sources) of
-            (Just written, _) -> fmap (`CompileFiles` sources) (parseQualifiedName written)
-            (Nothing, [source]) -> Right (CompileFile source)
-            (Nothing, _) -> Left "several source files require --entry"
-    ([], Just root, Just entryText, False) -> do
-        entry <- parseQualifiedName entryText
-        if null (optionSourceRoots options)
-            then Left "project compilation requires at least one --source-root"
-            else Right (CompileProject root entry (optionSourceRoots options) (optionExcludes options))
-    ([], Just root, Nothing, True)
-        | not (null (optionSourceRoots options)) ->
-            Right (ListProjectSources root (optionSourceRoots options) (optionExcludes options))
-        | otherwise -> Left "project source listing requires at least one --source-root"
-    _ -> Left "arguments must describe exactly one file compile, project compile, or project source listing"
+finishCommand options =
+    directiveEnvironment options >>= \environment -> case ( optionSourceFiles options
+                                                          , optionProjectRoot options
+                                                          , optionEntry options
+                                                          , optionListSources options
+                                                          ) of
+        (sources@(_ : _), Nothing, entryText, False)
+            | not (null (optionSourceRoots options) && null (optionExcludes options)) ->
+                Left "file compilation cannot declare project roots or exclusions"
+            | optionTest options -> case entryText of
+                Nothing -> Right (CompileTests environment sources)
+                Just _ -> Left "a test build has its own entry and takes no --entry"
+            | otherwise -> case (entryText, sources) of
+                (Just written, _) -> fmap (\entry -> CompileFiles environment entry sources) (parseQualifiedName written)
+                (Nothing, [source]) -> Right (CompileFile environment source)
+                (Nothing, _) -> Left "several source files require --entry"
+        ([], Just root, Just entryText, False)
+            | optionTest options -> Left "a test build names its source files"
+            | otherwise -> do
+                entry <- parseQualifiedName entryText
+                if null (optionSourceRoots options)
+                    then Left "project compilation requires at least one --source-root"
+                    else Right (CompileProject environment root entry (optionSourceRoots options) (optionExcludes options))
+        ([], Just root, Nothing, True)
+            | not (null (optionSourceRoots options)) ->
+                Right (ListProjectSources root (optionSourceRoots options) (optionExcludes options))
+            | otherwise -> Left "project source listing requires at least one --source-root"
+        _ -> Left "arguments must describe exactly one file compile, project compile, or project source listing"
 
 {- | Load every explicitly named source. A file named twice is one source;
 the first failure of any file rejects the whole set, as project loading does.
@@ -185,37 +211,46 @@
 
 executeCommand :: FrontendCommand -> IO FrontendOutcome
 executeCommand command = case command of
-    CompileFile source -> do
+    CompileFile environment source -> do
         loaded <- loadSourceFile source
         case loaded of
             Left diagnostics -> pure (FrontendDiagnostics diagnostics)
             Right document ->
                 let warnings = loadedSourceWarnings document
-                 in case compileToCorePrep (CompilerInput (loadedSourceRelativePath document) (loadedSourceText document)) of
+                 in case compileToCorePrepWith environment (CompilerInput (loadedSourceRelativePath document) (loadedSourceText document)) of
                         Left diagnostics -> pure (FrontendDiagnostics (diagnostics ++ warnings))
                         Right artifacts -> encodeCoreOutcome warnings (artifactOptimizedCore artifacts)
-    CompileFiles entry sources -> do
+    CompileTests environment sources -> do
         loaded <- loadSourceFiles sources
         case loaded of
             Left diagnostics -> pure (FrontendDiagnostics diagnostics)
             Right documents ->
                 let warnings = concatMap loadedSourceWarnings documents
+                 in case compileTestsToCorePrep environment (map toProjectCompilerInput documents) of
+                        Left diagnostics -> pure (FrontendDiagnostics (diagnostics ++ warnings))
+                        Right artifacts -> encodeCoreOutcome warnings (projectEntryCore artifacts)
+    CompileFiles environment entry sources -> do
+        loaded <- loadSourceFiles sources
+        case loaded of
+            Left diagnostics -> pure (FrontendDiagnostics diagnostics)
+            Right documents ->
+                let warnings = concatMap loadedSourceWarnings documents
                     inputs = map toProjectCompilerInput documents
                     -- One source keeps the single-file pipeline, whose Core
                     -- carries no source catalog; several are one program.
                     compiled = case inputs of
-                        [input] -> fmap artifactOptimizedCore (compileEntryToCorePrep entry input)
-                        _ -> fmap projectEntryCore (compileProjectToCorePrep entry inputs)
+                        [input] -> fmap artifactOptimizedCore (compileEntryToCorePrepWith environment entry input)
+                        _ -> fmap projectEntryCore (compileProjectToCorePrepWith environment entry inputs)
                  in case compiled of
                         Left diagnostics -> pure (FrontendDiagnostics (diagnostics ++ warnings))
                         Right core -> encodeCoreOutcome warnings core
-    CompileProject root entry roots excludes -> do
+    CompileProject environment root entry roots excludes -> do
         loaded <- loadSourceSet (SourceSetRequest root roots excludes)
         case loaded of
             Left diagnostics -> pure (FrontendDiagnostics diagnostics)
             Right documents ->
                 let warnings = concatMap loadedSourceWarnings documents
-                 in case compileProjectToCorePrep entry (map toProjectCompilerInput documents) of
+                 in case compileProjectToCorePrepWith environment entry (map toProjectCompilerInput documents) of
                         Left diagnostics -> pure (FrontendDiagnostics (diagnostics ++ warnings))
                         Right artifacts -> encodeCoreOutcome warnings (projectEntryCore artifacts)
     ListProjectSources root roots excludes -> do
diff --git a/src/Progmasoft/Tala/Pipeline/Stage.hs b/src/Progmasoft/Tala/Pipeline/Stage.hs
--- a/src/Progmasoft/Tala/Pipeline/Stage.hs
+++ b/src/Progmasoft/Tala/Pipeline/Stage.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Stage names shared by the compiler driver's internal pipeline reporting.
 Only Core, Xpp, and Xmm currently have explicit emission extensions.
diff --git a/src/Progmasoft/Tala/TestRunner.hs b/src/Progmasoft/Tala/TestRunner.hs
new file mode 100644
--- /dev/null
+++ b/src/Progmasoft/Tala/TestRunner.hs
@@ -0,0 +1,148 @@
+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+
+{- | The runner of a test build.
+
+A test build has no @Main@ of its own. The driver finds the methods with
+the @[Fact]@ attribute in the parsed sources and adds a class to their
+namespace whose @Main@ calls each of them in source order, closes each with
+@$Test.End@ under its name, and ends with @$Test.Finish@. The class is
+ordinary source to every later stage. Its name, @$Tests@, starts with @$@,
+which no identifier does, so it cannot meet a class of the program.
+
+A test is a static method without parameters that returns @void@. Facts of
+instance methods need objects, and facts in several namespaces need the
+multi-module link unit; both are refused as not implemented yet.
+-}
+module Progmasoft.Tala.TestRunner
+    ( TestCase (..)
+    , runnerClassName
+    , findTestCases
+    , testRunner
+    ) where
+
+import Data.List (nub)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+
+-- | One @[Fact]@ method, as the runner calls and names it.
+data TestCase = TestCase
+    { testNamespace :: QualifiedName
+    , testClass :: Identifier
+    , testMethod :: Identifier
+    , testDisplayName :: String
+    , testSpan :: SourceSpan
+    }
+    deriving stock (Eq, Ord, Read, Show)
+
+-- | The name of the class the driver adds to a test build.
+runnerClassName :: Identifier
+runnerClassName = Identifier "$Tests"
+
+{- | The tests of parsed source units, each given by its namespace and its
+declarations, in source order; and the problems of facts that cannot run.
+-}
+findTestCases :: [(Maybe QualifiedName, [Declaration Identifier ()])] -> Either [Diagnostic] [TestCase]
+findTestCases units = case concat [problems | Left problems <- found] of
+    [] -> Right [testCase | Right testCase <- found]
+    problems -> Left problems
+    where
+        found = concatMap unitCases units
+        unitCases (namespace, declarations) = concatMap (declarationCases namespace) declarations
+        declarationCases namespace declaration = case declaration of
+            TypeDeclaration {declarationName = className, typeMembers = members} ->
+                [ classify namespace className (method, isStatic, parameters, returnSyntax) fact
+                | FunctionDeclaration
+                    { declarationName = method
+                    , declarationIsStatic = isStatic
+                    , declarationParameters = parameters
+                    , declarationReturnSyntax = returnSyntax
+                    , declarationFact = Just fact
+                    } <-
+                    members
+                ]
+            TemplateTypeDeclaration {typeMembers = members} ->
+                [ Left [problem (factSpan fact) "TAE0016" "a [Fact] method of a template class is not compiled yet"]
+                | FunctionDeclaration {declarationFact = Just fact} <- members
+                ]
+            _ -> []
+        classify namespace className (method, isStatic, parameters, returnSyntax) fact = case namespace of
+            Nothing -> Left [problem (factSpan fact) "TAE0019" "a [Fact] method must stand in a namespace, which names the test build"]
+            Just name
+                | not isStatic ->
+                    Left [problem (factSpan fact) "TAE0016" "an instance [Fact] method is not compiled yet; declare the test static"]
+                | not (null parameters) -> Left [problem (factSpan fact) "TAE0017" "a [Fact] method takes no parameters"]
+                | not (spellsVoid returnSyntax) -> Left [problem (factSpan fact) "TAE0018" "a [Fact] method returns void"]
+                | otherwise ->
+                    Right
+                        ( TestCase
+                            name
+                            className
+                            method
+                            (maybe (identifierText className ++ "." ++ identifierText method) id (factName fact))
+                            (factSpan fact)
+                        )
+
+{- | The namespace of a test build, its entry and the declaration of its
+runner class. The tests must stand in one namespace, and there must be at
+least one.
+-}
+testRunner :: [TestCase] -> Either [Diagnostic] (QualifiedName, QualifiedName, Declaration Identifier ())
+testRunner cases = case (cases, nub (map testNamespace cases)) of
+    ([], _) ->
+        Left [Diagnostic TypeCheckerStage Error "TAE0015" Nothing "no [Fact] method was found; a test build needs at least one"]
+    (first : _, [namespace@(QualifiedName parts)]) ->
+        Right (namespace, QualifiedName (parts ++ [runnerClassName]), runner (testSpan first))
+    (_, namespaces) ->
+        Left
+            [ Diagnostic
+                TypeCheckerStage
+                Error
+                "TAE0020"
+                Nothing
+                ( "[Fact] methods stand in several namespaces ("
+                    ++ unwords (map render namespaces)
+                    ++ "); a test build of several namespaces is not compiled yet"
+                )
+            ]
+    where
+        render (QualifiedName parts) = foldr1 (\part rest -> part ++ "." ++ rest) (map identifierText parts)
+        runner spanValue =
+            let statement expression = ExpressionStatement spanValue expression True
+                call receiver method arguments =
+                    CallExpression
+                        spanValue
+                        (MemberAccessExpression spanValue (NameExpression spanValue receiver ()) (Identifier method) ())
+                        arguments
+                        ()
+                text value = LiteralExpression spanValue (StringLiteral value) ()
+                testReceiver = Identifier "$Test"
+                body =
+                    concat
+                        [ [ statement (call (testClass testCase) (identifierText (testMethod testCase)) [])
+                          , statement (call testReceiver "End" [text (testDisplayName testCase)])
+                          ]
+                        | testCase <- cases
+                        ]
+                        ++ [statement (call testReceiver "Finish" [])]
+                main =
+                    FunctionDeclaration
+                        spanValue
+                        (Identifier "Main")
+                        ()
+                        (ExplicitType (Identifier "void"))
+                        []
+                        (Block body)
+                        True
+                        PublicAccess
+                        Nothing
+             in TypeDeclaration spanValue runnerClassName () [main]
+
+spellsVoid :: TypeSyntax -> Bool
+spellsVoid syntax = case syntax of
+    ExplicitType (Identifier "void") -> True
+    QualifiedTypeSyntax (QualifiedName [Identifier "void"]) [] -> True
+    _ -> False
+
+problem :: SourceSpan -> String -> String -> Diagnostic
+problem spanValue code message = Diagnostic TypeCheckerStage Error code (Just spanValue) message
diff --git a/tala-compiler.cabal b/tala-compiler.cabal
--- a/tala-compiler.cabal
+++ b/tala-compiler.cabal
@@ -1,8 +1,8 @@
 cabal-version: 3.12
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 name: tala-compiler
-version: 0.1.0
+version: 0.2.0
 synopsis: Target-independent Tala compiler driver
 description:
     The target-independent driver of the Tala compiler: it runs the
@@ -11,7 +11,7 @@
     This is an implementation package of the Tala compiler. Its modules may
     change in any release; tools should depend on tala-tooling instead.
     .
-    The source is also offered under the Progmasoft Linking Exception 1.1
+    The source is also offered under the Progmasoft Linking Exception 2.0
     (LICENSE-EXCEPTION), an additional permission to MPL-2.0 for works that
     combine this package with independent code.
 license: MPL-2.0
@@ -32,11 +32,13 @@
         Progmasoft.Tala.Driver.Command
         Progmasoft.Tala.Diagnostic.SideChannel
         Progmasoft.Tala.Pipeline.Stage
+        Progmasoft.Tala.TestRunner
     reexported-modules:
         tala-syntax:Progmasoft.Tala.AST,
         tala-syntax:Progmasoft.Tala.CharacterLiteral,
         tala-syntax:Progmasoft.Tala.Diagnostic,
         tala-syntax:Progmasoft.Tala.Diagnostic.Protocol,
+        tala-syntax:Progmasoft.Tala.Directives,
         tala-syntax:Progmasoft.Tala.FloatingLiteral,
         tala-syntax:Progmasoft.Tala.Lexer,
         tala-syntax:Progmasoft.Tala.NestingLimits,
@@ -88,9 +90,9 @@
         base >=4.20 && <4.23,
         bytestring >=0.12 && <0.14,
         text >=2.1 && <2.3,
-        tala-core >=0.1.0 && <0.2,
-        tala-frontend >=0.1.0 && <0.2,
-        tala-syntax >=0.1.0 && <0.2
+        tala-core >=0.2.0 && <0.3,
+        tala-frontend >=0.2.0 && <0.3,
+        tala-syntax >=0.2.0 && <0.3
     default-language: GHC2024
 
 test-suite tala-compiler-tests
@@ -119,6 +121,7 @@
         BranchingTests
         LazyEvaluationTests
         ConsoleTests
+        TestBuildTests
         EffectTests
         QualifiedNameTests
         FormatSweepTests
@@ -166,8 +169,8 @@
         containers >=0.7 && <0.9,
         directory >=1.3 && <1.4,
         filepath >=1.5 && <1.6,
-        tala-frontend >=0.1.0 && <0.2,
-        tala-compiler >=0.1.0 && <0.2
+        tala-frontend >=0.2.0 && <0.3,
+        tala-compiler >=0.2.0 && <0.3
     default-language: GHC2024
 
 source-repository head
diff --git a/test/AssignmentExpressionTests.hs b/test/AssignmentExpressionTests.hs
--- a/test/AssignmentExpressionTests.hs
+++ b/test/AssignmentExpressionTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Vertical tests for assignments and increments used as values:
 @target = value@, @target op= value@, @++target@, @target++@ and
diff --git a/test/BranchingDiagnosticTests.hs b/test/BranchingDiagnosticTests.hs
--- a/test/BranchingDiagnosticTests.hs
+++ b/test/BranchingDiagnosticTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Where the diagnostics of @match@, @if@ expressions, @guard@ and nested
 blocks point, and that damaged spellings of those forms are always answered.
diff --git a/test/BranchingEvaluationCases.hs b/test/BranchingEvaluationCases.hs
--- a/test/BranchingEvaluationCases.hs
+++ b/test/BranchingEvaluationCases.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | The programs that "BranchingTests" runs, with the value each must return.
 
diff --git a/test/BranchingOracleTests.hs b/test/BranchingOracleTests.hs
--- a/test/BranchingOracleTests.hs
+++ b/test/BranchingOracleTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Differential tests for the lowering of @match@.
 
diff --git a/test/BranchingTests.hs b/test/BranchingTests.hs
--- a/test/BranchingTests.hs
+++ b/test/BranchingTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Vertical tests for @match@, for @if@ used as an expression, for
 @guard@, and for a block written as a statement.
diff --git a/test/ClosureTests.hs b/test/ClosureTests.hs
--- a/test/ClosureTests.hs
+++ b/test/ClosureTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | End-to-end tests for callable literals and closure conversion.
 
diff --git a/test/CompileTimeParityTests.hs b/test/CompileTimeParityTests.hs
--- a/test/CompileTimeParityTests.hs
+++ b/test/CompileTimeParityTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module CompileTimeParityTests (compileTimeParityTests) where
 
diff --git a/test/ConditionalExpressionTests.hs b/test/ConditionalExpressionTests.hs
--- a/test/ConditionalExpressionTests.hs
+++ b/test/ConditionalExpressionTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Vertical tests for the conditional forms and the statement forms that
 landed with them: @condition ? first : second@, @left ?: fallback@, compound
diff --git a/test/ConsoleTests.hs b/test/ConsoleTests.hs
--- a/test/ConsoleTests.hs
+++ b/test/ConsoleTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Console output, strings and the output format grammar.
 
diff --git a/test/CoreInliningTests.hs b/test/CoreInliningTests.hs
--- a/test/CoreInliningTests.hs
+++ b/test/CoreInliningTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module CoreInliningTests (coreInliningTests) where
 
diff --git a/test/CoreInterpreter.hs b/test/CoreInterpreter.hs
--- a/test/CoreInterpreter.hs
+++ b/test/CoreInterpreter.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | A small reference evaluator for scalar Core, used only by tests.
 
@@ -30,10 +30,10 @@
 import Data.Bits (complement, shiftL, shiftR, xor, (.&.), (.|.))
 import Data.Char (chr, isDigit)
 import Data.Ratio ((%))
-import RuntimeText
 import Progmasoft.Tala.AST
 import Progmasoft.Tala.Core
 import Progmasoft.Tala.RuntimeCall
+import RuntimeText
 
 -- | A runtime value of the supported subset.
 data Value
@@ -84,8 +84,13 @@
     , cells :: [(Int, Value)]
     , nextCell :: Int
     , writes :: [(Bool, String)]
-    -- ^ Every console write, latest first: whether it went to standard
-    -- error, and the text.
+    {- ^ Every console write, latest first: whether it went to standard
+    error, and the text.
+    -}
+    , tests :: (Integer, Integer, Integer)
+    {- ^ The test runtime: failed expectations of the current test, and the
+    tests that passed and failed.
+    -}
     }
 
 -- | Whether no step is left.
@@ -119,7 +124,7 @@
 runFunctionWriting :: Int -> CoreModule -> String -> [Value] -> Maybe (Value, Written)
 runFunctionWriting steps moduleValue name arguments = do
     function <- firstJust [function | function <- coreModuleFunctions moduleValue, functionSpelling function == name]
-    (value, budget) <- call moduleValue (Budget steps [] 0 []) function arguments
+    (value, budget) <- call moduleValue (Budget steps [] 0 [] (0, 0, 0)) function arguments
     let inOrder = reverse (writes budget)
     Just
         ( value
@@ -435,9 +440,35 @@
             let toError = target `elem` [consoleError, consoleErrorLine]
                 ended = if target `elem` [consoleOutputLine, consoleErrorLine] then value ++ lineTerminator else value
              in Just (UnitValue, budget {writes = (toError, ended) : writes budget})
+    -- The test runtime writes what "Talac/Runtime/Text/Testing.cpp" writes.
+    (TestFail, [TextValue report]) ->
+        let (current, passed, failed) = tests budget
+         in written (report ++ lineTerminator) budget {tests = (current + 1, passed, failed)}
+    (TestEnd, [TextValue name]) ->
+        let (current, passed, failed) = tests budget
+            verdict
+                | current == 0 = "PASS " ++ name
+                | otherwise =
+                    "FAIL " ++ name ++ " (" ++ show current ++ (if current == 1 then " failed expectation)" else " failed expectations)")
+            counts = if current == 0 then (0, passed + 1, failed) else (0, passed, failed + 1)
+         in written (verdict ++ lineTerminator) budget {tests = counts}
+    (TestFinish, []) ->
+        let (_, passed, failed) = tests budget
+            total = passed + failed
+         in written
+                ( show total
+                    ++ (if total == 1 then " test, " else " tests, ")
+                    ++ show passed
+                    ++ " passed, "
+                    ++ show failed
+                    ++ " failed"
+                    ++ lineTerminator
+                )
+                budget
     _ -> Nothing
     where
         text value = Just (TextValue value, budget)
+        written value after = Just (UnitValue, after {writes = (False, value) : writes after})
 
 applyPrimitive :: CorePrimitive -> Type -> [Value] -> Maybe Value
 applyPrimitive primitive valueType values = case (primitive, values) of
diff --git a/test/CoreLinearInliningTests.hs b/test/CoreLinearInliningTests.hs
--- a/test/CoreLinearInliningTests.hs
+++ b/test/CoreLinearInliningTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module CoreLinearInliningTests (coreLinearInliningTests) where
 
diff --git a/test/CoreOptimizerSourceTests.hs b/test/CoreOptimizerSourceTests.hs
--- a/test/CoreOptimizerSourceTests.hs
+++ b/test/CoreOptimizerSourceTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module CoreOptimizerSourceTests (coreOptimizerSourceTests) where
 
diff --git a/test/CoreOptimizerTests.hs b/test/CoreOptimizerTests.hs
--- a/test/CoreOptimizerTests.hs
+++ b/test/CoreOptimizerTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module CoreOptimizerTests (coreOptimizerTests) where
 
diff --git a/test/CoreVerifierTests.hs b/test/CoreVerifierTests.hs
--- a/test/CoreVerifierTests.hs
+++ b/test/CoreVerifierTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module CoreVerifierTests (coreVerifierTests) where
 
diff --git a/test/DiagnosticProtocolTests.hs b/test/DiagnosticProtocolTests.hs
--- a/test/DiagnosticProtocolTests.hs
+++ b/test/DiagnosticProtocolTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module DiagnosticProtocolTests (diagnosticProtocolTests) where
 
diff --git a/test/DiagnosticSideChannelTests.hs b/test/DiagnosticSideChannelTests.hs
--- a/test/DiagnosticSideChannelTests.hs
+++ b/test/DiagnosticSideChannelTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module DiagnosticSideChannelTests (diagnosticSideChannelTests) where
 
diff --git a/test/EffectTests.hs b/test/EffectTests.hs
--- a/test/EffectTests.hs
+++ b/test/EffectTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Which expressions the compiler takes to have an effect.
 
diff --git a/test/EnumTests.hs b/test/EnumTests.hs
--- a/test/EnumTests.hs
+++ b/test/EnumTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Tests for classic enums.
 
diff --git a/test/FallThroughTests.hs b/test/FallThroughTests.hs
--- a/test/FallThroughTests.hs
+++ b/test/FallThroughTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | How a function ends when its body ends.
 
diff --git a/test/FloatingOptimizerTests.hs b/test/FloatingOptimizerTests.hs
--- a/test/FloatingOptimizerTests.hs
+++ b/test/FloatingOptimizerTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module FloatingOptimizerTests (floatingOptimizerTests) where
 
diff --git a/test/FormatSweepTests.hs b/test/FormatSweepTests.hs
--- a/test/FormatSweepTests.hs
+++ b/test/FormatSweepTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Every conversion that can be written, against the rules stated a second time.
 
diff --git a/test/InferredReturnTests.hs b/test/InferredReturnTests.hs
--- a/test/InferredReturnTests.hs
+++ b/test/InferredReturnTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Tests for return types that are inferred, and for callables that are
 called.
diff --git a/test/IntegerEvaluationTests.hs b/test/IntegerEvaluationTests.hs
--- a/test/IntegerEvaluationTests.hs
+++ b/test/IntegerEvaluationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module IntegerEvaluationTests (integerEvaluationTests) where
 
diff --git a/test/IntegerFlowTests.hs b/test/IntegerFlowTests.hs
--- a/test/IntegerFlowTests.hs
+++ b/test/IntegerFlowTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module IntegerFlowTests (integerFlowTests) where
 
diff --git a/test/IterationTests.hs b/test/IterationTests.hs
--- a/test/IterationTests.hs
+++ b/test/IterationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module IterationTests (iterationTests) where
 
diff --git a/test/LazyEvaluationTests.hs b/test/LazyEvaluationTests.hs
--- a/test/LazyEvaluationTests.hs
+++ b/test/LazyEvaluationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Tests for evaluation by need.
 
diff --git a/test/LoopExpressionTests.hs b/test/LoopExpressionTests.hs
--- a/test/LoopExpressionTests.hs
+++ b/test/LoopExpressionTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Vertical tests for loops used as expressions and the value-carrying
 @break@ that supplies their value.
diff --git a/test/LoopFlowOracleTests.hs b/test/LoopFlowOracleTests.hs
--- a/test/LoopFlowOracleTests.hs
+++ b/test/LoopFlowOracleTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Small concrete Core executor used only as an independent loop-fact oracle.
 
diff --git a/test/LoopFlowTests.hs b/test/LoopFlowTests.hs
--- a/test/LoopFlowTests.hs
+++ b/test/LoopFlowTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Regression tests for loop-carried Core facts.
 
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 module Main (main) where
 
 import AssignmentExpressionTests (assignmentExpressionTests)
@@ -9,6 +9,7 @@
 import ClosureTests (closureTests)
 import CompileTimeParityTests (compileTimeParityTests)
 import ConditionalExpressionTests (conditionalExpressionTests)
+import ConsoleTests (consoleTests)
 import Control.Exception (finally)
 import CoreInliningTests (coreInliningTests)
 import CoreLinearInliningTests (coreLinearInliningTests)
@@ -19,29 +20,42 @@
 import Data.Word (Word8)
 import DiagnosticProtocolTests (diagnosticProtocolTests)
 import DiagnosticSideChannelTests (diagnosticSideChannelTests)
+import EffectTests (effectTests)
 import EnumTests (enumTests)
+import FallThroughTests (fallThroughTests)
 import FloatingOptimizerTests (floatingOptimizerTests)
-import IntegerEvaluationTests (integerEvaluationTests)
+import FormatSweepTests (formatSweepTests)
 import InferredReturnTests (inferredReturnTests)
+import IntegerEvaluationTests (integerEvaluationTests)
 import IntegerFlowTests (integerFlowTests)
 import IterationTests (iterationTests)
 import LazyEvaluationTests (lazyEvaluationTests)
-import ConsoleTests (consoleTests)
-import EffectTests (effectTests)
-import FormatSweepTests (formatSweepTests)
-import RuntimeCallTests (runtimeCallTests)
-import FallThroughTests (fallThroughTests)
-import MemoizeTests (memoizeTests)
 import LoopExpressionTests (loopExpressionTests)
 import LoopFlowOracleTests (loopFlowOracleTests)
 import LoopFlowTests (loopFlowTests)
+import MemoizeTests (memoizeTests)
 import MonomorphizationTests (monomorphizationTests)
 import NestingLimitTests (nestingLimitTests)
 import Numeric (readHex)
 import NumericTests (numericTests)
 import ParserContractTests (parserContractTests)
 import PatternTests (patternTests)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Artifact
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.CorePrep.Wire
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser (Token (..), TokenKind (..))
+import Progmasoft.Tala.Pipeline.Stage
+import Progmasoft.Tala.Resolver.NameResolution
 import QualifiedNameTests (qualifiedNameTests)
+import RuntimeCallTests (runtimeCallTests)
 import ScalarWireTests (scalarWireTests)
 import ShortCircuitTests (shortCircuitTests)
 import SourceSetTests (sourceSetTests)
@@ -66,21 +80,8 @@
 import TemplateSpecializationPlannerTests (templateSpecializationPlannerTests)
 import TemplateTests (templateTests)
 import TemplateVerifierTests (templateVerifierTests)
+import TestBuildTests (testBuildTests)
 import TypeClassificationTests (typeClassificationTests)
-import Progmasoft.Tala.AST
-import Progmasoft.Tala.Compiler
-import Progmasoft.Tala.Core
-import Progmasoft.Tala.Core.Artifact
-import Progmasoft.Tala.Core.CorePrep
-import Progmasoft.Tala.Core.CorePrep.Verifier
-import Progmasoft.Tala.Core.CorePrep.Wire
-import Progmasoft.Tala.Core.Verifier
-import Progmasoft.Tala.Core.Wire
-import Progmasoft.Tala.Diagnostic
-import Progmasoft.Tala.Lexer
-import Progmasoft.Tala.Parser (Token (..), TokenKind (..))
-import Progmasoft.Tala.Pipeline.Stage
-import Progmasoft.Tala.Resolver.NameResolution
 import VoidTests (voidTests)
 
 main :: IO ()
@@ -193,6 +194,7 @@
     mapM_ (uncurry check) fallThroughTests
     mapM_ (uncurry check) runtimeCallTests
     mapM_ (uncurry check) consoleTests
+    mapM_ (uncurry check) testBuildTests
     mapM_ (uncurry check) effectTests
     mapM_ (uncurry check) qualifiedNameTests
     mapM_ (uncurry check) formatSweepTests
diff --git a/test/MemoizeTests.hs b/test/MemoizeTests.hs
--- a/test/MemoizeTests.hs
+++ b/test/MemoizeTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Tests for the callable that remembers its result, 'CoreMemoize'.
 
diff --git a/test/MonomorphizationTests.hs b/test/MonomorphizationTests.hs
--- a/test/MonomorphizationTests.hs
+++ b/test/MonomorphizationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 module MonomorphizationTests (monomorphizationTests) where
 
 import Data.List (isInfixOf)
diff --git a/test/NestingLimitTests.hs b/test/NestingLimitTests.hs
--- a/test/NestingLimitTests.hs
+++ b/test/NestingLimitTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Tests for the nesting limits of "Progmasoft.Tala.NestingLimits".
 
diff --git a/test/NumericTests.hs b/test/NumericTests.hs
--- a/test/NumericTests.hs
+++ b/test/NumericTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module NumericTests (numericTests) where
 
diff --git a/test/ParserContractTests.hs b/test/ParserContractTests.hs
--- a/test/ParserContractTests.hs
+++ b/test/ParserContractTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module ParserContractTests (parserContractTests) where
 
@@ -198,7 +198,7 @@
 
 statements :: String -> Maybe [Statement Identifier ()]
 statements source = case parseSource source of
-    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ _ (Block body) _ _]])) -> Just body
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ _ (Block body) _ _ _]])) -> Just body
     _ -> Nothing
 
 spanTests :: [(String, Bool)]
@@ -216,7 +216,10 @@
     , ("guard span runs from the keyword to the closing brace", guardSpan "guard (true) else { return 1; }")
     , ("nested block span includes both braces", blockSpan "{ int inner = 1; }")
     , ("empty nested block span includes both braces", blockSpan "{}")
-    , ("match statement span runs from the keyword to the closing brace", matchStatementSpan "match (1) { 1 -> { }, _ -> { } }")
+    ,
+        ( "match statement span runs from the keyword to the closing brace"
+        , matchStatementSpan "match (1) { 1 -> { }, _ -> { } }"
+        )
     , ("match statement without arms keeps its span", matchStatementSpan "match (1) { }")
     , ("match expression span excludes the binding around it", matchExpressionSpan "match (1) { 1 -> 2, _ -> 3 }")
     , ("if expression span runs from the keyword to the else block", ifExpressionSpan "if (true) { 1 } else { 2 }")
@@ -272,7 +275,8 @@
 -- and 31 to 35.
 valueBlockSpans :: Bool
 valueBlockSpans = case statements "class Program { int Value() {\nauto r = if (true) { 1 } else { 2 };\nreturn r; } }" of
-    Just (BindingStatement _ _ _ _ _ (ConditionalExpression _ _ (BlockExpression first _ _) (BlockExpression second _ _) _) : _) ->
+    Just
+        (BindingStatement _ _ _ _ _ (ConditionalExpression _ _ (BlockExpression first _ _) (BlockExpression second _ _) _) : _) ->
         exactSpan first 2 20 25 && exactSpan second 2 31 36
     _ -> False
 
@@ -517,7 +521,7 @@
 
 returnSyntax :: String -> Maybe TypeSyntax
 returnSyntax spelling = case parseSource (functionWithReturn spelling) of
-    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ syntax _ _ _ _]])) -> Just syntax
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ syntax _ _ _ _ _]])) -> Just syntax
     _ -> Nothing
 
 bindingSyntax :: String -> Maybe TypeSyntax
@@ -533,7 +537,7 @@
 
 parameterNameFor :: String -> Maybe Identifier
 parameterNameFor spelling = case parseSource ("class Program { void Apply(" ++ spelling ++ ") { return; } }") of
-    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _]])) ->
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _ _]])) ->
         Just (parameterName parameter)
     _ -> Nothing
 
@@ -543,7 +547,7 @@
         (CompilerInput "type-contract.tala" ("class Program { void Apply(_ " ++ spelling ++ " value) { return; } }")) of
         Right artifacts ->
             case syntaxDeclarations (typedSyntaxTree (artifactTypedAST artifacts)) of
-                [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _]] ->
+                [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _ _]] ->
                     Just (parameterAnnotation parameter)
                 _ -> Nothing
         Left _ -> Nothing
diff --git a/test/PatternTests.hs b/test/PatternTests.hs
--- a/test/PatternTests.hs
+++ b/test/PatternTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module PatternTests (patternTests) where
 
diff --git a/test/QualifiedNameTests.hs b/test/QualifiedNameTests.hs
--- a/test/QualifiedNameTests.hs
+++ b/test/QualifiedNameTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Names written through what declares them.
 
diff --git a/test/RuntimeCallTests.hs b/test/RuntimeCallTests.hs
--- a/test/RuntimeCallTests.hs
+++ b/test/RuntimeCallTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | The runtime call in Core, its catalog, and the reading of formats.
 
@@ -58,34 +58,58 @@
     , (TextNewline, 11, "tala_text_newline", [], TextResult, False)
     , (ConsoleWrite, 12, "tala_console_write", [TextParameter, CountParameter], NoResult, True)
     , (TextEquals, 13, "tala_text_equals", [TextParameter, TextParameter], TruthResult, False)
+    , (TestFail, 14, "tala_test_fail", [TextParameter], NoResult, True)
+    , (TestEnd, 15, "tala_test_end", [TextParameter], NoResult, True)
+    , (TestFinish, 16, "tala_test_finish", [], NoResult, True)
     ]
     where
         conversion value = [CountParameter, CountParameter, CountParameter, value]
 
 catalogTests :: [(String, Bool)]
 catalogTests =
-    [ ("the runtime catalog has thirteen functions", length runtimeFunctions == 13)
+    [ ("the runtime catalog has sixteen functions", length runtimeFunctions == 16)
     , ("every runtime function has a row in the tests", map (\(function, _, _, _, _, _) -> function) rows == runtimeFunctions)
-    , ("runtime identities are distinct", length (nub (map runtimeFunctionIdentity runtimeFunctions)) == length runtimeFunctions)
+    ,
+        ( "runtime identities are distinct"
+        , length (nub (map runtimeFunctionIdentity runtimeFunctions)) == length runtimeFunctions
+        )
     , ("runtime symbols are distinct", length (nub (map runtimeFunctionSymbol runtimeFunctions)) == length runtimeFunctions)
-    , ("runtime identities are the numbers from one", map runtimeFunctionIdentity runtimeFunctions == [1 .. 13])
+    , ("runtime identities are the numbers from one", map runtimeFunctionIdentity runtimeFunctions == [1 .. 16])
     , ("zero names no runtime function", runtimeFunctionOf 0 == Nothing)
     , ("a negative number names no runtime function", runtimeFunctionOf (-1) == Nothing)
-    , ("the number after the last names no runtime function", runtimeFunctionOf 14 == Nothing)
-    , ("only a console write is observable", filter runtimeObservable runtimeFunctions == [ConsoleWrite])
+    , ("the number after the last names no runtime function", runtimeFunctionOf 17 == Nothing)
+    ,
+        ( "only console writes and the test runtime are observable"
+        , filter runtimeObservable runtimeFunctions == [ConsoleWrite, TestFail, TestEnd, TestFinish]
+        )
     , -- The name of a runtime call in the typed tree: reserved, and
       -- distinct for every function.
       ("runtime names have negative symbols", all ((< 0) . symbolIdValue . resolvedSymbol . runtimeName) runtimeFunctions)
     , ("runtime names are distinct", length (nub (map runtimeName runtimeFunctions)) == length runtimeFunctions)
-    , ("a runtime name is read back as its function", all (\function -> runtimeFunctionOfName (runtimeName function) == Just function) runtimeFunctions)
+    ,
+        ( "a runtime name is read back as its function"
+        , all (\function -> runtimeFunctionOfName (runtimeName function) == Just function) runtimeFunctions
+        )
     , ("a source name is no runtime name", runtimeFunctionOfName (name 5 "Print") == Nothing)
-    , ("the names the language declares are no runtime names", all ((== Nothing) . runtimeFunctionOfName . declared) [builtinSystemSymbol, builtinConsoleSymbol])
+    ,
+        ( "the names the language declares are no runtime names"
+        , all ((== Nothing) . runtimeFunctionOfName . declared) [builtinSystemSymbol, builtinConsoleSymbol]
+        )
     , ("System and Console have symbols of their own", builtinSystemSymbol /= builtinConsoleSymbol)
-    , ("a runtime name begins with a character no identifier has", all ((== "$") . take 1 . identifierText . resolvedSpelling . runtimeName) runtimeFunctions)
+    ,
+        ( "a runtime name begins with a character no identifier has"
+        , all ((== "$") . take 1 . identifierText . resolvedSpelling . runtimeName) runtimeFunctions
+        )
     , -- The values the runtime header gives the flags and the targets.
-      ("the conversion flags are distinct bits", [flagLeft, flagZero, flagPlus, flagSpace, flagAlternate, flagGroup, flagHexadecimal] == [1, 2, 4, 8, 16, 32, 64])
+
+        ( "the conversion flags are distinct bits"
+        , [flagLeft, flagZero, flagPlus, flagSpace, flagAlternate, flagGroup, flagHexadecimal] == [1, 2, 4, 8, 16, 32, 64]
+        )
     , ("an absent width is minus one", absent == -1)
-    , ("the console targets are the four the runtime knows", [consoleOutput, consoleOutputLine, consoleError, consoleErrorLine] == [0, 1, 2, 3])
+    ,
+        ( "the console targets are the four the runtime knows"
+        , [consoleOutput, consoleOutputLine, consoleError, consoleErrorLine] == [0, 1, 2, 3]
+        )
     ]
         ++ concat
             [ [ ("runtime identity of " ++ show function, runtimeFunctionIdentity function == identity)
@@ -105,16 +129,31 @@
 -}
 acceptanceTests :: [(String, Bool)]
 acceptanceTests =
-    [ ("a signed parameter takes the signed integers up to 64 bits", accepted SignedParameter == ["byte", "short", "long", "int"])
-    , ("an unsigned parameter takes the unsigned integers up to 64 bits", accepted UnsignedParameter == ["ubyte", "ushort", "ulong", "uint"])
-    , ("a floating parameter takes the floating types up to 64 bits", accepted FloatingParameter == ["sfloat", "lfloat", "float"])
+    [
+        ( "a signed parameter takes the signed integers up to 64 bits"
+        , accepted SignedParameter == ["byte", "short", "long", "int"]
+        )
+    ,
+        ( "an unsigned parameter takes the unsigned integers up to 64 bits"
+        , accepted UnsignedParameter == ["ubyte", "ushort", "ulong", "uint"]
+        )
+    ,
+        ( "a floating parameter takes the floating types up to 64 bits"
+        , accepted FloatingParameter == ["sfloat", "lfloat", "float"]
+        )
     , ("a Boolean parameter takes bool", accepted BoolParameter == ["bool"])
     , ("a character parameter takes char", accepted CharParameter == ["char"])
     , ("a text parameter takes String", accepted TextParameter == ["String"])
     , ("a count is an int", accepted CountParameter == ["int"])
     , ("no parameter takes a callable", not (any (`runtimeAccepts` FunctionType [] intType) parameterKinds))
-    , ("no parameter takes a generic type", not (any (`runtimeAccepts` NamedType (QualifiedName [Identifier "int"]) [TypeTemplateArgument intType]) parameterKinds))
-    , ("no parameter takes a qualified name", not (any (`runtimeAccepts` NamedType (QualifiedName [Identifier "System", Identifier "int"]) []) parameterKinds))
+    ,
+        ( "no parameter takes a generic type"
+        , not (any (`runtimeAccepts` NamedType (QualifiedName [Identifier "int"]) [TypeTemplateArgument intType]) parameterKinds)
+        )
+    ,
+        ( "no parameter takes a qualified name"
+        , not (any (`runtimeAccepts` NamedType (QualifiedName [Identifier "System", Identifier "int"]) []) parameterKinds)
+        )
     ]
     where
         parameterKinds =
@@ -213,41 +252,89 @@
 coreTests :: [(String, Bool)]
 coreTests =
     [ ("Core accepts well-formed runtime calls", verifiesCore writingModule)
-    , ("Core wire carries runtime calls", (encodeCore defaultCoreWireLimits writingModule >>= decodeCore defaultCoreWireLimits) == Right writingModule)
-    , ("the reference evaluator runs the calls", runFunctionWriting 1000 writingModule "Run" [IntegerValue 42] == Just (IntegerValue 42, Written "Count:    42\n" ""))
+    ,
+        ( "Core wire carries runtime calls"
+        , (encodeCore defaultCoreWireLimits writingModule >>= decodeCore defaultCoreWireLimits) == Right writingModule
+        )
+    ,
+        ( "the reference evaluator runs the calls"
+        , runFunctionWriting 1000 writingModule "Run" [IntegerValue 42] == Just (IntegerValue 42, Written "Count:    42\n" "")
+        )
     , -- Every function of the catalog with arguments of its types.
       ("Core accepts a call of every runtime function", all (verifiesCore . wellFormed) runtimeFunctions)
     , -- What the first operand must be.
-      ("Core rejects an identity the catalog does not have", rejectsCore (evaluating (CorePrimitive CoreRuntimeCall [integer 999] unitType)))
+
+        ( "Core rejects an identity the catalog does not have"
+        , rejectsCore (evaluating (CorePrimitive CoreRuntimeCall [integer 999] unitType))
+        )
     , ("Core rejects the identity zero", rejectsCore (evaluating (CorePrimitive CoreRuntimeCall [integer 0] unitType)))
     , ("Core rejects a negative identity", rejectsCore (evaluating (CorePrimitive CoreRuntimeCall [integer (-12)] unitType)))
     , ("Core rejects a runtime call without operands", rejectsCore (evaluating (CorePrimitive CoreRuntimeCall [] unitType)))
     , -- The function is fixed when the program is compiled.
-      ("Core rejects an identity that is computed", rejectsCore (binding stringType (CorePrimitive CoreRuntimeCall [count] stringType)))
-    , ( "Core rejects an identity of another integer type"
-      , rejectsCore (binding stringType (CorePrimitive CoreRuntimeCall [CoreLiteral (CoreInteger 11) (namedType "uint")] stringType))
-      )
-    , ("Core rejects a Boolean as an identity", rejectsCore (evaluating (CorePrimitive CoreRuntimeCall [CoreLiteral (CoreBoolean True) boolType] unitType)))
+
+        ( "Core rejects an identity that is computed"
+        , rejectsCore (binding stringType (CorePrimitive CoreRuntimeCall [count] stringType))
+        )
+    ,
+        ( "Core rejects an identity of another integer type"
+        , rejectsCore
+            (binding stringType (CorePrimitive CoreRuntimeCall [CoreLiteral (CoreInteger 11) (namedType "uint")] stringType))
+        )
+    ,
+        ( "Core rejects a Boolean as an identity"
+        , rejectsCore (evaluating (CorePrimitive CoreRuntimeCall [CoreLiteral (CoreBoolean True) boolType] unitType))
+        )
     , -- The arguments are the ones the function takes.
       ("Core rejects a missing argument", rejectsCore (binding stringType (runtime TextConcat [text "a"] stringType)))
-    , ("Core rejects an extra argument", rejectsCore (binding stringType (runtime TextConcat [text "a", text "b", text "c"] stringType)))
-    , ("Core rejects an argument to a function that takes none", rejectsCore (binding stringType (runtime TextNewline [count] stringType)))
-    , ("Core rejects a number where a string is taken", rejectsCore (binding stringType (runtime TextConcat [text "a", count] stringType)))
-    , ("Core rejects a string where a number is taken", rejectsCore (binding stringType (runtime TextFromSigned [text "a"] stringType)))
-    , ("Core rejects a signed integer where an unsigned one is taken", rejectsCore (binding stringType (runtime TextFromUnsigned [count] stringType)))
-    , ( "Core rejects a width that is not an int"
-      , rejectsCore
+    ,
+        ( "Core rejects an extra argument"
+        , rejectsCore (binding stringType (runtime TextConcat [text "a", text "b", text "c"] stringType))
+        )
+    ,
+        ( "Core rejects an argument to a function that takes none"
+        , rejectsCore (binding stringType (runtime TextNewline [count] stringType))
+        )
+    ,
+        ( "Core rejects a number where a string is taken"
+        , rejectsCore (binding stringType (runtime TextConcat [text "a", count] stringType))
+        )
+    ,
+        ( "Core rejects a string where a number is taken"
+        , rejectsCore (binding stringType (runtime TextFromSigned [text "a"] stringType))
+        )
+    ,
+        ( "Core rejects a signed integer where an unsigned one is taken"
+        , rejectsCore (binding stringType (runtime TextFromUnsigned [count] stringType))
+        )
+    ,
+        ( "Core rejects a width that is not an int"
+        , rejectsCore
             ( binding
                 stringType
                 (runtime TextFormatSigned [integer 0, CoreLiteral (CoreInteger 5) (namedType "long"), integer absent, count] stringType)
             )
-      )
-    , ("Core rejects a write target that is a Boolean", rejectsCore (evaluating (runtime ConsoleWrite [text "a", CoreLiteral (CoreBoolean True) boolType] unitType)))
+        )
+    ,
+        ( "Core rejects a write target that is a Boolean"
+        , rejectsCore (evaluating (runtime ConsoleWrite [text "a", CoreLiteral (CoreBoolean True) boolType] unitType))
+        )
     , -- The call has the type the function returns.
-      ("Core rejects a string result typed as a number", rejectsCore (binding intType (runtime TextConcat [text "a", text "b"] intType)))
-    , ("Core rejects a write that yields a string", rejectsCore (binding stringType (runtime ConsoleWrite [text "a", integer 0] stringType)))
-    , ("Core rejects an equality that yields a string", rejectsCore (binding stringType (runtime TextEquals [text "a", text "b"] stringType)))
-    , ("Core accepts an equality that yields a Boolean", verifiesCore (binding boolType (runtime TextEquals [text "a", text "b"] boolType)))
+
+        ( "Core rejects a string result typed as a number"
+        , rejectsCore (binding intType (runtime TextConcat [text "a", text "b"] intType))
+        )
+    ,
+        ( "Core rejects a write that yields a string"
+        , rejectsCore (binding stringType (runtime ConsoleWrite [text "a", integer 0] stringType))
+        )
+    ,
+        ( "Core rejects an equality that yields a string"
+        , rejectsCore (binding stringType (runtime TextEquals [text "a", text "b"] stringType))
+        )
+    ,
+        ( "Core accepts an equality that yields a Boolean"
+        , verifiesCore (binding boolType (runtime TextEquals [text "a", text "b"] boolType))
+        )
     ]
 
 -- | A call of the function with an argument of a type each parameter takes.
@@ -272,11 +359,12 @@
 corePrepTests =
     [ ("CorePrep accepts well-formed runtime calls", preparedAndVerified writingModule)
     , ("CorePrep accepts a call of every runtime function", all (preparedAndVerified . wellFormed) runtimeFunctions)
-    , ( "CorePrep wire carries runtime calls"
-      , case prepareCore writingModule of
+    ,
+        ( "CorePrep wire carries runtime calls"
+        , case prepareCore writingModule of
             Right prepared -> (encodeCorePrep prepared >>= decodeCorePrep) == Right prepared
             Left _ -> False
-      )
+        )
     , -- The identity stays a literal: it is not bound to a temporary.
       ("CorePrep keeps the identity a literal", identityStaysLiteral)
     , ("CorePrep rejects an unknown identity", rejectsCorePrep stringType [literal 999])
@@ -284,9 +372,15 @@
     , ("CorePrep rejects an identity that is a variable", rejectsCorePrep stringType [CorePrepVariable countName intType])
     , ("CorePrep rejects a missing argument", rejectsCorePrep stringType [identity TextConcat, string "a"])
     , ("CorePrep rejects an extra argument", rejectsCorePrep stringType [identity TextNewline, string "a"])
-    , ("CorePrep rejects an argument of another type", rejectsCorePrep stringType [identity TextConcat, string "a", CorePrepVariable countName intType])
+    ,
+        ( "CorePrep rejects an argument of another type"
+        , rejectsCorePrep stringType [identity TextConcat, string "a", CorePrepVariable countName intType]
+        )
     , ("CorePrep rejects a result of another type", rejectsCorePrep intType [identity TextConcat, string "a", string "b"])
-    , ("CorePrep accepts the same call with its own types", acceptsCorePrep stringType [identity TextConcat, string "a", string "b"])
+    ,
+        ( "CorePrep accepts the same call with its own types"
+        , acceptsCorePrep stringType [identity TextConcat, string "a", string "b"]
+        )
     ]
     where
         literal value = CorePrepLiteral (CoreInteger value) intType
@@ -354,7 +448,10 @@
     , -- A string that nothing uses is only computed. Whether the optimizer
       -- removes it is its own business; it must not come to be written.
       ("a string that nothing uses is not written", writes (optimized unusedText) == Just "")
-    , ("optimized runtime calls still verify", all (maybe False verifiesCore . optimized) [writingModule, twoWrites, boundWrite, unusedText])
+    ,
+        ( "optimized runtime calls still verify"
+        , all (maybe False verifiesCore . optimized) [writingModule, twoWrites, boundWrite, unusedText]
+        )
     ]
     where
         write value = CoreEvaluate (runtime ConsoleWrite [text value, integer consoleOutputLine] unitType)
@@ -386,8 +483,9 @@
     [ ("a format without a percent sign is one literal", parseFormat "plain text" == Right [LiteralPiece "plain text"])
     , ("an empty format has no pieces", parseFormat "" == Right [])
     , ("a conversion alone is one piece", parseFormat "%d" == Right [ConversionPiece (plain SignedDecimal 1)])
-    , ( "text and conversions keep their order"
-      , parseFormat "a%db%sc"
+    ,
+        ( "text and conversions keep their order"
+        , parseFormat "a%db%sc"
             == Right
                 [ LiteralPiece "a"
                 , ConversionPiece (plain SignedDecimal 2)
@@ -395,9 +493,15 @@
                 , ConversionPiece (plain Text 5)
                 , LiteralPiece "c"
                 ]
-      )
-    , ("every conversion letter is read", map kindOf "duxfscb" == map Just [SignedDecimal, UnsignedDecimal, Hexadecimal, FixedPoint, Text, Character, Truth])
-    , ("a conversion is written with the letter it was read from", all (\letter -> fmap conversionLetter (kindOf letter) == Just letter) "duxfscb")
+        )
+    ,
+        ( "every conversion letter is read"
+        , map kindOf "duxfscb" == map Just [SignedDecimal, UnsignedDecimal, Hexadecimal, FixedPoint, Text, Character, Truth]
+        )
+    ,
+        ( "a conversion is written with the letter it was read from"
+        , all (\letter -> fmap conversionLetter (kindOf letter) == Just letter) "duxfscb"
+        )
     , -- %% and %n are not conversions of an argument.
       ("a doubled percent sign is a percent sign", parseFormat "100%%" == Right [LiteralPiece "100%"])
     , ("percent signs join the text around them", parseFormat "a%%b%%c" == Right [LiteralPiece "a%b%c"])
@@ -405,12 +509,21 @@
     , ("neither takes an argument", argumentCount "%%%n%%" == Just 0)
     , -- Flags, width and precision.
       ("flags are read in the order written", flagsOf "%-'d" == Just [LeftFlag, GroupFlag])
-    , ("a leading zero is a flag and not part of the width", parseFormat "%08d" == Right [ConversionPiece (Conversion SignedDecimal [ZeroFlag] (FixedSize 8) NoSize 1)])
+    ,
+        ( "a leading zero is a flag and not part of the width"
+        , parseFormat "%08d" == Right [ConversionPiece (Conversion SignedDecimal [ZeroFlag] (FixedSize 8) NoSize 1)]
+        )
     , ("a width may have several digits", widthOf "%120s" == Just (FixedSize 120))
     , ("a zero inside a width is a digit", widthOf "%10d" == Just (FixedSize 10))
-    , ("a precision follows a point", parseFormat "%.3f" == Right [ConversionPiece (Conversion FixedPoint [] NoSize (FixedSize 3) 1)])
+    ,
+        ( "a precision follows a point"
+        , parseFormat "%.3f" == Right [ConversionPiece (Conversion FixedPoint [] NoSize (FixedSize 3) 1)]
+        )
     , ("a precision of zero is a precision", precisionOf "%.0f" == Just (FixedSize 0))
-    , ("width and precision may both be written", parseFormat "%10.3f" == Right [ConversionPiece (Conversion FixedPoint [] (FixedSize 10) (FixedSize 3) 1)])
+    ,
+        ( "width and precision may both be written"
+        , parseFormat "%10.3f" == Right [ConversionPiece (Conversion FixedPoint [] (FixedSize 10) (FixedSize 3) 1)]
+        )
     , ("a star is a width from an argument", widthOf "%*d" == Just ArgumentSize)
     , ("a star after a point is a precision from an argument", precisionOf "%.*f" == Just ArgumentSize)
     , ("a conversion takes one argument", argumentCount "%d" == Just 1)
diff --git a/test/RuntimeText.hs b/test/RuntimeText.hs
--- a/test/RuntimeText.hs
+++ b/test/RuntimeText.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | A reference for the text functions of the runtime, used only by tests.
 
diff --git a/test/ScalarWireTests.hs b/test/ScalarWireTests.hs
--- a/test/ScalarWireTests.hs
+++ b/test/ScalarWireTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module ScalarWireTests (scalarWireTests) where
 
diff --git a/test/ShortCircuitTests.hs b/test/ShortCircuitTests.hs
--- a/test/ShortCircuitTests.hs
+++ b/test/ShortCircuitTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 module ShortCircuitTests (shortCircuitTests) where
 
 import Data.List (isPrefixOf, nub)
diff --git a/test/SourceSetTests.hs b/test/SourceSetTests.hs
--- a/test/SourceSetTests.hs
+++ b/test/SourceSetTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module SourceSetTests (sourceSetTests) where
 
diff --git a/test/SourceTextTests.hs b/test/SourceTextTests.hs
--- a/test/SourceTextTests.hs
+++ b/test/SourceTextTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module SourceTextTests (sourceTextTests) where
 
diff --git a/test/SourceWarningTests.hs b/test/SourceWarningTests.hs
--- a/test/SourceWarningTests.hs
+++ b/test/SourceWarningTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Tests for the source warnings, which today are the two spellings of the
 removed decrement operator.
diff --git a/test/SpecializationTests.hs b/test/SpecializationTests.hs
--- a/test/SpecializationTests.hs
+++ b/test/SpecializationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module SpecializationTests (specializationTests) where
 
diff --git a/test/StaticMemberCoreTests.hs b/test/StaticMemberCoreTests.hs
--- a/test/StaticMemberCoreTests.hs
+++ b/test/StaticMemberCoreTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Core-boundary tests for selected static method identities.
 
diff --git a/test/StaticMemberOverloadTests.hs b/test/StaticMemberOverloadTests.hs
--- a/test/StaticMemberOverloadTests.hs
+++ b/test/StaticMemberOverloadTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Arity and ordered parameter-vector tests for static member overloads.
 
diff --git a/test/StaticMemberParserTests.hs b/test/StaticMemberParserTests.hs
--- a/test/StaticMemberParserTests.hs
+++ b/test/StaticMemberParserTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Grammar-boundary tests for member selectors. These assert the parsed tree,
 not merely successful token consumption, so later stages receive an explicit
diff --git a/test/StaticMemberProjectTests.hs b/test/StaticMemberProjectTests.hs
--- a/test/StaticMemberProjectTests.hs
+++ b/test/StaticMemberProjectTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Project-boundary tests for static member binding across physical files.
 
diff --git a/test/StaticMemberSemanticTests.hs b/test/StaticMemberSemanticTests.hs
--- a/test/StaticMemberSemanticTests.hs
+++ b/test/StaticMemberSemanticTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 {- | Semantic conformance tests for type-qualified static method calls.
 
diff --git a/test/TemplateApplicationTests.hs b/test/TemplateApplicationTests.hs
--- a/test/TemplateApplicationTests.hs
+++ b/test/TemplateApplicationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateApplicationTests (templateApplicationTests) where
 
diff --git a/test/TemplateDeclarationTests.hs b/test/TemplateDeclarationTests.hs
--- a/test/TemplateDeclarationTests.hs
+++ b/test/TemplateDeclarationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateDeclarationTests (templateDeclarationTests) where
 
diff --git a/test/TemplateDiscoveryTests.hs b/test/TemplateDiscoveryTests.hs
--- a/test/TemplateDiscoveryTests.hs
+++ b/test/TemplateDiscoveryTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateDiscoveryTests (templateDiscoveryTests) where
 
@@ -233,7 +233,7 @@
         rewriteDeclaration declaration = case declaration of
             TypeDeclaration spanValue name annotation members ->
                 TypeDeclaration spanValue name (rewriteType annotation) (map rewriteDeclaration members)
-            FunctionDeclaration spanValue name annotation syntax parameters body isStatic access ->
+            FunctionDeclaration spanValue name annotation syntax parameters body isStatic access fact ->
                 FunctionDeclaration
                     spanValue
                     name
@@ -243,6 +243,7 @@
                     (rewriteBlock body)
                     isStatic
                     access
+                    fact
             TemplateTypeDeclaration {} -> declaration
             EnumDeclaration {} -> declaration
         rewriteParameter parameter = parameter {parameterAnnotation = rewriteType (parameterAnnotation parameter)}
diff --git a/test/TemplateInstantiationTests.hs b/test/TemplateInstantiationTests.hs
--- a/test/TemplateInstantiationTests.hs
+++ b/test/TemplateInstantiationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateInstantiationTests (templateInstantiationTests) where
 
diff --git a/test/TemplateManglingTests.hs b/test/TemplateManglingTests.hs
--- a/test/TemplateManglingTests.hs
+++ b/test/TemplateManglingTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateManglingTests (templateManglingTests) where
 
@@ -93,6 +93,7 @@
         (Block [])
         isStatic
         access
+        Nothing
 
 nestedMember :: Int -> String -> Type -> Declaration ResolvedName Type
 nestedMember unique name annotation = TypeDeclaration spanValue (resolved unique name) annotation []
diff --git a/test/TemplateMemberReachabilityTests.hs b/test/TemplateMemberReachabilityTests.hs
--- a/test/TemplateMemberReachabilityTests.hs
+++ b/test/TemplateMemberReachabilityTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateMemberReachabilityTests (templateMemberReachabilityTests) where
 
@@ -593,6 +593,7 @@
         )
         False
         PublicAccess
+        Nothing
 
 directTargets :: Declaration ResolvedName Type -> [SymbolId]
 directTargets FunctionDeclaration {declarationBody = Block statements} = concatMap statementTargets statements
diff --git a/test/TemplatePlanVerifierTests.hs b/test/TemplatePlanVerifierTests.hs
--- a/test/TemplatePlanVerifierTests.hs
+++ b/test/TemplatePlanVerifierTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplatePlanVerifierTests (templatePlanVerifierTests) where
 
diff --git a/test/TemplateSpecializationPlannerTests.hs b/test/TemplateSpecializationPlannerTests.hs
--- a/test/TemplateSpecializationPlannerTests.hs
+++ b/test/TemplateSpecializationPlannerTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateSpecializationPlannerTests (templateSpecializationPlannerTests) where
 
diff --git a/test/TemplateTests.hs b/test/TemplateTests.hs
--- a/test/TemplateTests.hs
+++ b/test/TemplateTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 module TemplateTests (templateTests) where
 
 import Data.Bits (shiftL)
diff --git a/test/TemplateVerifierTests.hs b/test/TemplateVerifierTests.hs
--- a/test/TemplateVerifierTests.hs
+++ b/test/TemplateVerifierTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TemplateVerifierTests (templateVerifierTests) where
 
@@ -199,6 +199,7 @@
                 (Block [ReturnStatement spanValue (Just (LiteralExpression spanValue (IntegerLiteral 1) intType))])
                 False
                 DefaultAccess
+                Nothing
         declaration = templateDeclaration 1 [typeParameter 2 "T"] [member]
         isExpected TemplateMemberHasInvalidSymbol {} = True
         isExpected _ = False
diff --git a/test/TestBuildTests.hs b/test/TestBuildTests.hs
new file mode 100644
--- /dev/null
+++ b/test/TestBuildTests.hs
@@ -0,0 +1,313 @@
+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+
+{- | Test builds, @#expect@ and the conditional directives.
+
+@Spec\/Language\/Attributes.tala@ specifies @[Fact]@, @#if Fact@ and
+@#expect@, and @Spec\/Language\/Decls.tala@ the conditional directives and
+regions. A test build is compiled for a target with @Fact@ holding; the
+driver adds a runner whose @Main@ calls every fact. The runner is run in the
+reference Core evaluator before and after optimization, and what it wrote is
+compared with text written by hand. The line terminator of the reference is
+a line feed.
+-}
+module TestBuildTests (testBuildTests) where
+
+import CoreInterpreter
+import Progmasoft.Tala.AST (Identifier (..), QualifiedName (..), ResolvedName (..))
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core (CoreFunction (..), CoreModule (..))
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Directives
+
+testBuildTests :: [(String, Bool)]
+testBuildTests =
+    concat
+        [ [ ("unoptimized test build writes the report of " ++ name, ran artifactCoreOf source == Just expected)
+          , ("optimized test build writes the report of " ++ name, ran artifactOptimizedCoreOf source == Just expected)
+          ]
+        | (name, source, expected) <- runs
+        ]
+        ++ [ ("test build rejects " ++ name ++ " with " ++ code, rejectedWith code (compileTests linux source))
+           | (name, source, code) <- testRejections
+           ]
+        ++ [ ("directives choose " ++ name, chooses environment source)
+           | (name, environment, source) <- selections
+           ]
+        ++ [ ("directives reject " ++ name ++ " with " ++ code, rejectedWith code (compilePlain linux source))
+           | (name, source, code) <- directiveRejections
+           ]
+        ++ [ ("an ordinary build leaves out the branch of #if Fact", rejectedWith "TAN0001" (compilePlain linux helperOnlyInTests))
+           ,
+               ( "a test build keeps the branch of #if Fact"
+               , ran artifactOptimizedCoreOf helperOnlyInTests == Just "PASS Probe.Uses\n1 test, 1 passed, 0 failed\n"
+               )
+           , ("an ordinary build compiles a [Fact] method as a method", accepted (compilePlain linux ordinaryWithFact))
+           ,
+               ( "a test build keeps Main for the runner alone"
+               , fmap (mains . projectEntryCore) (compileTests linux programWithFact) == Right 1
+               )
+           ,
+               ( "an entry keeps Main for itself alone"
+               , fmap
+                    (mains . artifactOptimizedCore)
+                    ( compileEntryToCorePrepWith
+                        linux
+                        (QualifiedName [Identifier "Demo", Identifier "Program"])
+                        (CompilerInput "Tests.tala" twoMains)
+                    )
+                    == Right 1
+               )
+           , ("test build rejects facts in two namespaces with TAE0020", rejectedWith "TAE0020" twoNamespaces)
+           ]
+    where
+        twoNamespaces =
+            compileTestsToCorePrep
+                linux
+                [ CompilerInput "One.tala" "namespace A; class One { [Fact] public static void F() { } }"
+                , CompilerInput "Two.tala" "namespace B; class Two { [Fact] public static void G() { } }"
+                ]
+
+linux, windows :: DirectiveEnvironment
+linux = DirectiveEnvironment Linux False
+windows = DirectiveEnvironment Windows False
+
+compileTests :: DirectiveEnvironment -> String -> Either [Diagnostic] ProjectFrontendArtifacts
+compileTests environment source = compileTestsToCorePrep environment [CompilerInput "Tests.tala" source]
+
+compilePlain :: DirectiveEnvironment -> String -> Either [Diagnostic] FrontendArtifacts
+compilePlain environment source = compileToCorePrepWith environment (CompilerInput "Tests.tala" source)
+
+artifactCoreOf, artifactOptimizedCoreOf :: ProjectFrontendArtifacts -> CoreModule
+artifactCoreOf = artifactCore . artifactFrontend . projectEntryNamespace
+artifactOptimizedCoreOf = projectEntryCore
+
+-- | What the runner of a test build writes to standard output.
+ran :: (ProjectFrontendArtifacts -> CoreModule) -> String -> Maybe String
+ran select source = case compileTests linux source of
+    Right artifacts -> case runFunctionWriting 200000 (select artifacts) "Main" [] of
+        Just (_, Written output "") -> Just output
+        _ -> Nothing
+    Left _ -> Nothing
+
+rejectedWith :: String -> Either [Diagnostic] value -> Bool
+rejectedWith code result = case result of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+
+accepted :: Either [Diagnostic] value -> Bool
+accepted = either (const False) (const True)
+
+-- | Whether the branch the source marks with @Chosen@ is the one compiled.
+chooses :: DirectiveEnvironment -> String -> Bool
+chooses environment source = case compilePlain environment (wrap source) of
+    Right artifacts -> case runFunctionWriting 200000 (artifactOptimizedCore artifacts) "Main" [] of
+        Just (_, Written "chosen\n" "") -> True
+        _ -> False
+    Left _ -> False
+    where
+        wrap body =
+            unlines
+                [ "namespace Probe;"
+                , "public class Program {"
+                , "    public static void Main() {"
+                , body
+                , "    }"
+                , "}"
+                ]
+
+suite :: [String] -> String
+suite members =
+    unlines
+        ( [ "namespace Probe;"
+          , "public class Checks {"
+          , "    public static int Twice(_ int v) { return v + v; }"
+          , "    public static int Log(_ int v) { Console.Println(v); return v; }"
+          ]
+            ++ members
+            ++ ["}"]
+        )
+
+runs :: [(String, String, String)]
+runs =
+    [
+        ( "a passing fact"
+        , suite ["    [Fact] public static void Doubles() { #expect Twice(2) == 4; }"]
+        , "PASS Checks.Doubles\n1 test, 1 passed, 0 failed\n"
+        )
+    ,
+        ( "a failing comparison with the value of its operand"
+        , suite
+            ["    [Fact] public static void Doubles() {", "        int count = Twice(1);", "        #expect count == 3;", "    }"]
+        , "Tests.tala:7:9: expectation failed: count == 3\n    count is 2\nFAIL Checks.Doubles (1 failed expectation)\n1 test, 0 passed, 1 failed\n"
+        )
+    ,
+        ( "a failed condition that is not a comparison"
+        , suite ["    [Fact] public static void Truth() { #expect not (Twice(1) == 2); }"]
+        , "Tests.tala:5:41: expectation failed: not (Twice(1) == 2)\nFAIL Checks.Truth (1 failed expectation)\n1 test, 0 passed, 1 failed\n"
+        )
+    ,
+        ( "every expectation after a false one"
+        , suite
+            ["    [Fact] public static void Both() {", "        #expect Twice(1) == 1;", "        #expect Twice(2) > 5;", "    }"]
+        , concat
+            [ "Tests.tala:6:9: expectation failed: Twice(1) == 1\n    Twice(1) is 2\n"
+            , "Tests.tala:7:9: expectation failed: Twice(2) > 5\n    Twice(2) is 4\n"
+            , "FAIL Checks.Both (2 failed expectations)\n1 test, 0 passed, 1 failed\n"
+            ]
+        )
+    ,
+        ( "a named fact and the order of the facts"
+        , suite
+            [ "    [Fact(\"two and two\")] public static void First() { #expect Twice(2) == 4; }"
+            , "    [Fact] public static void Second() { #expect Twice(3) \\= 6; }"
+            ]
+        , "PASS two and two\nTests.tala:6:42: expectation failed: Twice(3) \\= 6\n    Twice(3) is 6\nFAIL Checks.Second (1 failed expectation)\n2 tests, 1 passed, 1 failed\n"
+        )
+    ,
+        ( "an operand evaluated once, also for the report"
+        , suite ["    [Fact] public static void Once() { #expect Log(2) == 3; }"]
+        , "2\nTests.tala:5:40: expectation failed: Log(2) == 3\n    Log(2) is 2\nFAIL Checks.Once (1 failed expectation)\n1 test, 0 passed, 1 failed\n"
+        )
+    ,
+        ( "an expectation of a helper under #if Fact, for the fact that called it"
+        , suite
+            [ "#if Fact"
+            , "    public static void Positive(_ int v) { #expect v > 0; }"
+            , "#endif"
+            , "    [Fact] public static void Calls() { Positive(Twice(0) - 1); }"
+            ]
+        , "Tests.tala:6:44: expectation failed: v > 0\n    v is -1\nFAIL Checks.Calls (1 failed expectation)\n1 test, 0 passed, 1 failed\n"
+        )
+    ,
+        ( "a string comparison"
+        , suite ["    [Fact] public static void Text() { String s = \"a\" + \"b\"; #expect s == \"ab\"; }"]
+        , "PASS Checks.Text\n1 test, 1 passed, 0 failed\n"
+        )
+    ]
+
+testRejections :: [(String, String, String)]
+testRejections =
+    [ ("an instance fact", suite ["    [Fact] public void Instance() { }"], "TAE0016")
+    , ("a fact with a parameter", suite ["    [Fact] public static void Takes(_ int v) { }"], "TAE0017")
+    , ("a fact with a result", suite ["    [Fact] public static int Gives() { return 1; }"], "TAE0018")
+    , ("sources without a fact", suite [], "TAE0015")
+    , ("#expect as an expression", suite ["    [Fact] public static void Value() { bool b = #expect 1 == 1; }"], "TAP0055")
+    ]
+
+-- | The body of @Main@ writes @chosen@ in the branch that should be compiled.
+selections :: [(String, DirectiveEnvironment, String)]
+selections =
+    [ ("os(Windows) on Windows", windows, branches "os(Windows)")
+    , ("#else on Linux for os(Windows)", linux, unlines ["#if os(Windows)", wrong, "#else", right, "#endif"])
+    ,
+        ( "the first branch that holds"
+        , linux
+        , unlines ["#if os(macOS)", wrong, "#elseif family(Unix)", right, "#elseif os(Linux)", wrong, "#endif"]
+        )
+    , ("not, && and parentheses", linux, branches "family(Unix) && not (os(macOS) || os(FreeBSD))")
+    , ("|| on Windows", windows, branches "os(Linux) || family(Windows)")
+    , ("no branch of Fact in an ordinary build", linux, unlines [right, "#if Fact", wrong, "#endif"])
+    , ("true and false", linux, unlines ["#if false", wrong, "#elseif true", right, "#endif"])
+    ,
+        ( "nested groups"
+        , linux
+        , unlines ["#if family(Unix)", "#if os(Linux)", right, "#else", wrong, "#endif", "#else", wrong, "#endif"]
+        )
+    , ("a region", linux, unlines ["#region Output", right, "#endregion Output"])
+    ]
+    where
+        right = "Console.Println(\"chosen\");"
+        wrong = "Console.Println(\"wrong\");"
+        branches condition = unlines ["#if " ++ condition, right, "#else", wrong, "#endif"]
+
+directiveRejections :: [(String, String, String)]
+directiveRejections =
+    [ ("an unknown operating system", unlines ["#if os(ReactOS)", "#endif"], "TAP0046")
+    , ("#define", "#define DEBUG true", "TAP0053")
+    , ("a diagnostic directive", "#message \"Building\"", "TAP0053")
+    , ("an unknown directive", "#pragma once", "TAP0054")
+    , ("a directive symbol", unlines ["#if DEBUG", "#endif"], "TAP0047")
+    ,
+        ( "a split expression"
+        , unlines
+            [ "namespace A;"
+            , "class C { public static void Main() {"
+            , "int value ="
+            , "#if os(Windows)"
+            , "10"
+            , "#else"
+            , "20"
+            , "#endif"
+            , "; } }"
+            ]
+        , "TAP0043"
+        )
+    , ("#elseif after #else", unlines ["#if os(Windows)", "#else", "#elseif os(Linux)", "#endif"], "TAP0049")
+    , ("an #if without #endif", "#if os(Linux)", "TAP0051")
+    , ("#endif without #if", "#endif", "TAP0050")
+    , ("#else with a condition", unlines ["#if os(Linux)", "#else os(Windows)", "#endif"], "TAP0048")
+    , ("a region closed under another name", unlines ["#region Networking", "#endregion networking"], "TAP0052")
+    , ("a directive after code on its line", "namespace A; #if os(Linux)\n#endif", "TAP0042")
+    ,
+        ( "#expect outside test code"
+        , unlines ["namespace A;", "class C { public static void Main() { #expect 1 == 1; } }"]
+        , "TAP0044"
+        )
+    ]
+
+helperOnlyInTests :: String
+helperOnlyInTests =
+    unlines
+        [ "namespace Probe;"
+        , "class Probe {"
+        , "#if Fact"
+        , "    public static int Helper() { return 1; }"
+        , "#endif"
+        , "    [Fact] public static void Uses() { #expect Helper() == 1; }"
+        , "}"
+        ]
+
+-- | How many functions of a build are spelled @Main@.
+mains :: CoreModule -> Int
+mains core =
+    length
+        [ ()
+        | function <- coreModuleFunctions core
+        , resolvedSpelling (coreFunctionName function) == Identifier "Main"
+        ]
+
+programWithFact :: String
+programWithFact =
+    unlines
+        [ "namespace Smoke;"
+        , "class Program {"
+        , "    public static int Twice(_ int v) { return v + v; }"
+        , "    public static void Main() { }"
+        , "}"
+        , "class Checks {"
+        , "    [Fact] public static void Doubles() { Program.Main(); #expect Program.Twice(2) == 4; }"
+        , "}"
+        ]
+
+twoMains :: String
+twoMains =
+    unlines
+        [ "namespace Demo;"
+        , "class Program {"
+        , "    public static void Main() { Other.Main(); }"
+        , "}"
+        , "class Other {"
+        , "    public static void Main() { }"
+        , "}"
+        ]
+
+ordinaryWithFact :: String
+ordinaryWithFact =
+    unlines
+        [ "namespace Probe;"
+        , "class Probe {"
+        , "    [Fact] public static void Check() { #expect 1 + 1 == 2; }"
+        , "    public static void Main() { Check(); }"
+        , "}"
+        ]
diff --git a/test/TypeClassificationTests.hs b/test/TypeClassificationTests.hs
--- a/test/TypeClassificationTests.hs
+++ b/test/TypeClassificationTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module TypeClassificationTests (typeClassificationTests) where
 
diff --git a/test/VoidTests.hs b/test/VoidTests.hs
--- a/test/VoidTests.hs
+++ b/test/VoidTests.hs
@@ -1,5 +1,5 @@
 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
--- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
 
 module VoidTests (voidTests) where
 
